Add new files and update existing files

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-12-12 15:33:10 -03:00
commit 2f8cb0d776
135 changed files with 13454 additions and 3508 deletions

View file

@ -22,5 +22,5 @@ npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
/test-results/ /test-results/
/playwright-report/ /playwright-report/*/
/playwright/.cache/ /playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -111,13 +111,13 @@
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^3.3.2", "@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15", "autoprefixer": "^10.4.15",
"daisyui": "^3.6.3", "daisyui": "^4.0.4",
"postcss": "^8.4.29", "postcss": "^8.4.29",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3", "prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.3.0", "prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.4.9" "vite": "^4.5.1"
} }
} }

View file

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="./e2e/index.html">e2e report</a>
</li>
<li>
<a href="./onlyFront/index.html">frontEnd Only report</a>
</li>
</ul>
</body>
</html>

View file

@ -20,11 +20,13 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */ /* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined, workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. */ /* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000', // baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry", trace: "on-first-retry",
@ -69,9 +71,16 @@ export default defineConfig({
], ],
/* Run your local dev server before starting the tests */ /* Run your local dev server before starting the tests */
// webServer: { // webServer: [
// command: 'npm run start', // {
// url: 'http://127.0.0.1:3000', // command: "npm run backend",
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI, // reuseExistingServer: !process.env.CI,
// }, // },
// ],
}); });

78
src/frontend/run-tests.sh Executable file
View file

@ -0,0 +1,78 @@
#!/bin/bash
# Default value for the --ui flag
ui=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--ui)
ui=true
shift
;;
*)
echo "Unknown option: $key"
exit 1
;;
esac
shift
done
# Function to forcibly terminate a process by port
terminate_process_by_port() {
port="$1"
echo "Terminating process on port: $port"
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
echo "Process terminated."
}
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
# install playwright if there is not installed yet
npx playwright install
# Navigate to the project root directory (where the Makefile is located)
cd ../../
# Start the frontend using 'make frontend' in the background
make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10
# Navigate to the test directory
cd src/frontend
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
sleep 25
# Navigate back to the test directory
cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
fi
npx playwright show-report
# After the tests are finished, you can add cleanup or teardown logic here if needed
# The trap will automatically terminate processes by port on script exit

View file

@ -16,8 +16,8 @@ import {
FETCH_ERROR_MESSAGE, FETCH_ERROR_MESSAGE,
} from "./constants/constants"; } from "./constants/constants";
import { alertContext } from "./contexts/alertContext"; import { alertContext } from "./contexts/alertContext";
import { FlowsContext } from "./contexts/flowsContext";
import { locationContext } from "./contexts/locationContext"; import { locationContext } from "./contexts/locationContext";
import { TabsContext } from "./contexts/tabsContext";
import { typesContext } from "./contexts/typesContext"; import { typesContext } from "./contexts/typesContext";
import Router from "./routes"; import Router from "./routes";
@ -30,7 +30,7 @@ export default function App() {
setShowSideBar(true); setShowSideBar(true);
setIsStackedOpen(true); setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]); }, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext); const { hardReset } = useContext(FlowsContext);
const { const {
errorData, errorData,
@ -42,9 +42,7 @@ export default function App() {
successData, successData,
successOpen, successOpen,
setSuccessOpen, setSuccessOpen,
setErrorData,
loading, loading,
setLoading,
} = useContext(alertContext); } = useContext(alertContext);
const navigate = useNavigate(); const navigate = useNavigate();
const { fetchError } = useContext(typesContext); const { fetchError } = useContext(typesContext);

View file

@ -22,16 +22,25 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent"; import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent"; import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button"; import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs";
import { import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { postCustomComponentUpdate } from "../../../../controllers/API";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import {
cleanEdges,
convertObjToArray, convertObjToArray,
convertValuesToNumbers, convertValuesToNumbers,
hasDuplicateKeys, hasDuplicateKeys,
isValidConnection, isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils"; } from "../../../../utils/reactflowUtils";
import { import {
nodeColors, nodeColors,
@ -44,7 +53,6 @@ export default function ParameterComponent({
left, left,
id, id,
data, data,
setData,
tooltipTitle, tooltipTitle,
title, title,
color, color,
@ -53,14 +61,20 @@ export default function ParameterComponent({
required = false, required = false,
optionalHandle = null, optionalHandle = null,
info = "", info = "",
proxy,
showNode, showNode,
index = "",
}: ParameterComponentType): JSX.Element { }: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null); const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null); const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const { setErrorData, modalContextOpen } = useContext(alertContext);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0); const [position, setPosition] = useState(0);
const { setTabsState, tabId, flows } = useContext(TabsContext); const { setTabsState, tabId, flows, tabsState, updateFlow } =
useContext(FlowsContext);
const [dataRef, setDataRef] = useState(data);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null; const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -78,41 +92,114 @@ export default function ParameterComponent({
const groupedEdge = useRef(null); const groupedEdge = useRef(null);
useEffect(() => {
setDataRef(data);
}, [modalContextOpen]);
const { reactFlowInstance, setFilterEdge } = useContext(typesContext); const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled = let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ?? reactFlowInstance
false; ?.getEdges()
.some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(proxy ? { ...id, proxy } : id)
) ?? false;
const { data: myData } = useContext(typesContext); const { data: myData } = useContext(typesContext);
const { takeSnapshot } = useContext(undoRedoContext);
const handleUpdateValues = async (name: string, data: NodeDataType) => {
const code = data.node?.template["code"]?.value;
if (!code) {
console.error("Code not found in the template");
return;
}
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
};
const handleOnNewValue = ( const handleOnNewValue = (
newValue: string | string[] | boolean | Object[] newValue: string | string[] | boolean | Object[]
): void => { ): void => {
let newData = cloneDeep(data); if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
data.node!.template[name].value = newValue;
updateNodeInternals(data.id);
setDataRef((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue; newData.node!.template[name].value = newValue;
setData(newData); return newData;
});
// Set state to pending // Set state to pending
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { if (data.node!.template[name].value !== newValue) {
if (!prev[tabId]) { const tabs = cloneDeep(tabsState);
return prev; tabs[tabId].isPending = false;
} tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
return { setTabsState({
...prev, ...tabs,
[tabId]: {
...prev[tabId],
isPending: true,
formKeysData: prev[tabId].formKeysData,
},
};
}); });
}
renderTooltips(); renderTooltips();
}; };
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return;
if (data.node!.template[name].value !== newNodeClass.template[name].value) {
takeSnapshot();
}
data.node! = {
...newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
data.node!.template[name].value = code;
updateNodeInternals(data.id);
// Set state to pending
//@ts-ignore
if (data.node!.template[name].value !== code) {
const tabs = cloneDeep(tabsState);
tabs[tabId].isPending = false;
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
setTabsState({
...tabs,
});
}
renderTooltips();
let flow = flows.find((flow) => flow.id === tabId);
setTimeout(() => {
//timeout necessary because ReactFlow updates are not async
if (reactFlowInstance && flow && flow.data) {
cleanEdges({
flow: {
edges: flow.data!.edges,
nodes: flow.data!.nodes,
},
updateEdge: (edge) => {
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
}, 50);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => { useEffect(() => {
if (name === "openai_api_base") console.log(info);
// @ts-ignore // @ts-ignore
infoHtml.current = ( infoHtml.current = (
<div className="h-full w-full break-words"> <div className="h-full w-full break-words">
@ -166,6 +253,24 @@ export default function ParameterComponent({
</div> </div>
<span className="ps-2 text-xs text-foreground"> <span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}{" "} {nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span className="text-xs">
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs"> <span className="text-xs">
{" "} {" "}
{item.type === "" ? "" : " - "} {item.type === "" ? "" : " - "}
@ -181,6 +286,7 @@ export default function ParameterComponent({
)) ))
: item.type} : item.type}
</span> </span>
)}
</span> </span>
</span> </span>
</div> </div>
@ -197,19 +303,11 @@ export default function ParameterComponent({
}, [tooltipTitle, flow]); }, [tooltipTitle, flow]);
return !showNode ? ( return !showNode ? (
left && left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
<></> <></>
) : ( ) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip <ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"} styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0} delayDuration={0}
@ -219,7 +317,11 @@ export default function ParameterComponent({
<Handle <Handle
type={left ? "target" : "source"} type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right} position={left ? Position.Left : Position.Right}
id={id} id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) => isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!) isValidConnection(connection, reactFlowInstance!)
} }
@ -236,6 +338,8 @@ export default function ParameterComponent({
}} }}
></Handle> ></Handle>
</ShadTooltip> </ShadTooltip>
</div>
</Button>
) )
) : ( ) : (
<div <div
@ -250,7 +354,13 @@ export default function ParameterComponent({
(info !== "" ? " flex items-center" : "") (info !== "" ? " flex items-center" : "")
} }
> >
{title} {proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span> <span className="text-status-red">{required ? " *" : ""}</span>
<div className=""> <div className="">
{info !== "" && ( {info !== "" && (
@ -266,17 +376,7 @@ export default function ParameterComponent({
)} )}
</div> </div>
</div> </div>
{left && {left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
<></> <></>
) : ( ) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted"> <Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
@ -290,7 +390,11 @@ export default function ParameterComponent({
<Handle <Handle
type={left ? "target" : "source"} type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right} position={left ? Position.Left : Position.Right}
id={id} id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) => isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!) isValidConnection(connection, reactFlowInstance!)
} }
@ -331,9 +435,12 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node.template[name].value ?? ""} value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"textarea-" + index}
data-testid={"textarea-" + index}
/> />
) : ( ) : (
<InputComponent <InputComponent
id={"input-" + index}
disabled={disabled} disabled={disabled}
password={data.node?.template[name].password ?? false} password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
@ -344,6 +451,7 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? ( ) : left === true && type === "bool" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<ToggleShadComponent <ToggleShadComponent
id={"toggle-" + index}
disabled={disabled} disabled={disabled}
enabled={data.node?.template[name].value ?? false} enabled={data.node?.template[name].value ?? false}
setEnabled={(isEnabled) => { setEnabled={(isEnabled) => {
@ -357,30 +465,49 @@ export default function ParameterComponent({
<FloatComponent <FloatComponent
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
rangeSpec={data.node?.template[name].rangeSpec}
onChange={handleOnNewValue} onChange={handleOnNewValue}
/> />
</div> </div>
) : left === true && ) : left === true &&
type === "str" && type === "str" &&
data.node?.template[name].options ? ( data.node?.template[name].options ? (
<div className="mt-2 w-full"> // TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown <Dropdown
options={data.node.template[name].options} options={data.node.template[name].options}
onSelect={handleOnNewValue} onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"} value={data.node.template[name].value ?? "Choose an option"}
></Dropdown> id={"dropdown-" + index}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
)}
</div> </div>
) : left === true && type === "code" ? ( ) : left === true && type === "code" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<CodeAreaComponent <CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false} dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => { setNodeClass={handleNodeClass}
data.node = nodeClass;
}}
nodeClass={data.node} nodeClass={data.node}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"code-input-" + index}
/> />
</div> </div>
) : left === true && type === "file" ? ( ) : left === true && type === "file" ? (
@ -390,7 +517,6 @@ export default function ParameterComponent({
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes} fileTypes={data.node?.template[name].fileTypes}
suffixes={data.node?.template[name].suffixes}
onFileChange={(filePath: string) => { onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath; data.node!.template[name].file_path = filePath;
}} }}
@ -402,24 +528,21 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue} onChange={handleOnNewValue}
id={"int-input-" + index}
/> />
</div> </div>
) : left === true && type === "prompt" ? ( ) : left === true && type === "prompt" ? (
<div className="mt-2 w-full"> <div className="mt-2 w-full">
<PromptAreaComponent <PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name} field_name={name}
setNodeClass={(nodeClass) => { setNodeClass={handleNodeClass}
data.node = nodeClass;
const clone = cloneDeep(data);
clone.node = nodeClass;
setData(clone);
}}
nodeClass={data.node} nodeClass={data.node}
disabled={disabled} disabled={disabled}
value={data.node?.template[name].value ?? ""} value={data.node?.template[name].value ?? ""}
onChange={(e) => { onChange={handleOnNewValue}
handleOnNewValue(e); id={"prompt-input-" + index}
}} data-testid={"prompt-input-" + index}
/> />
</div> </div>
) : left === true && type === "NestedDict" ? ( ) : left === true && type === "NestedDict" ? (
@ -439,6 +562,7 @@ export default function ParameterComponent({
data.node!.template[name].value = newValue; data.node!.template[name].value = newValue;
handleOnNewValue(newValue); handleOnNewValue(newValue);
}} }}
id="div-dict-input"
/> />
</div> </div>
) : left === true && type === "dict" ? ( ) : left === true && type === "dict" ? (
@ -447,10 +571,10 @@ export default function ParameterComponent({
disabled={disabled} disabled={disabled}
editNode={false} editNode={false}
value={ value={
data.node!.template[name].value?.length === 0 || dataRef.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value !dataRef.node!.template[name].value
? [{ "": "" }] ? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value) : convertObjToArray(dataRef.node!.template[name].value)
} }
duplicateKey={errorDuplicateKey} duplicateKey={errorDuplicateKey}
onChange={(newValue) => { onChange={(newValue) => {

View file

@ -1,38 +1,54 @@
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow"; import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent"; import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent"; import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { priorityFields } from "../../constants/constants";
import { useSSE } from "../../contexts/SSEContext"; import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext"; import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { undoRedoContext } from "../../contexts/undoRedoContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent"; import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components"; import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils"; import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils"; import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils"; import { classNames, cn, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent"; import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({ export default function GenericNode({
data: olddata, data,
xPos,
yPos,
selected, selected,
}: { }: {
data: NodeDataType; data: NodeDataType;
selected: boolean; selected: boolean;
xPos: number;
yPos: number;
}): JSX.Element { }): JSX.Element {
const [data, setData] = useState(olddata); const { updateFlow, flows, tabId, saveCurrentFlow } =
const { updateFlow, flows, tabId } = useContext(TabsContext); useContext(FlowsContext);
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } = const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext); useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type]; const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] = const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null); useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
const [handles, setHandles] = useState<boolean[] | []>([]); const [handles, setHandles] = useState<boolean[] | []>([]);
let numberOfInputs: boolean[] = []; let numberOfInputs: boolean[] = [];
const { modalContextOpen } = useContext(alertContext);
const { takeSnapshot } = useContext(undoRedoContext);
function countHandles(): void { function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template) numberOfInputs = Object.keys(data.node!.template)
@ -65,37 +81,18 @@ export default function GenericNode({
useEffect(() => { useEffect(() => {
countHandles(); countHandles();
}, []); }, [data, data.node]);
// State for outline color // State for outline color
const { sseData, isBuilding } = useSSE(); const { sseData, isBuilding } = useSSE();
useEffect(() => {
olddata.node = data.node;
let myFlow = flows.find((flow) => flow.id === tabId);
if (reactFlowInstance && myFlow) {
let flow = cloneDeep(myFlow);
flow.data = reactFlowInstance.toObject();
cleanEdges({
flow: {
edges: flow.data.edges,
nodes: flow.data.nodes,
},
updateEdge: (edge) => {
flow.data!.edges = edge;
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
countHandles();
}, [data]);
useEffect(() => { useEffect(() => {
setTimeout(() => { setNodeDescription(data.node!.description);
updateNodeInternals(data.id); }, [data.node!.description]);
}, 300);
}, [showNode]); useEffect(() => {
setNodeName(data.node!.display_name);
}, [data.node!.display_name]);
// New useEffect to watch for changes in sseData and update validation status // New useEffect to watch for changes in sseData and update validation status
useEffect(() => { useEffect(() => {
@ -107,14 +104,25 @@ export default function GenericNode({
setValidationStatus(null); setValidationStatus(null);
} }
}, [sseData, data.id]); }, [sseData, data.id]);
const showNode = data.showNode ?? true;
const nameEditable = data.node?.flow || data.type === "CustomComponent";
return ( return (
<> <>
<NodeToolbar> <NodeToolbar>
<NodeToolbarComponent <NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data} data={data}
setData={setData} deleteNode={(id) => {
deleteNode={deleteNode} takeSnapshot();
setShowNode={setShowNode} deleteNode(id);
saveCurrentFlow();
}}
setShowNode={(show: boolean) => {
data.showNode = show;
}}
numberOfHandles={handles} numberOfHandles={handles}
showNode={showNode} showNode={showNode}
></NodeToolbarComponent> ></NodeToolbarComponent>
@ -123,10 +131,7 @@ export default function GenericNode({
<div <div
className={classNames( className={classNames(
selected ? "border border-ring" : "border", selected ? "border border-ring" : "border",
" transition-transform ", showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
showNode
? " w-96 scale-100 transform rounded-lg duration-500 ease-in-out "
: " transform-width w-26 h-26 scale-90 transform rounded-full duration-500 ",
"generic-node-div" "generic-node-div"
)} )}
> >
@ -137,6 +142,7 @@ export default function GenericNode({
)} )}
<div> <div>
<div <div
data-testid={"div-generic-node"}
className={ className={
"generic-node-div-title " + "generic-node-div-title " +
(!showNode (!showNode
@ -151,20 +157,55 @@ export default function GenericNode({
} }
> >
<IconComponent <IconComponent
name={name} name={data.node?.flow ? "group_components" : name}
className={ className={
"generic-node-icon " + "generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12") (!showNode ? "absolute inset-x-6 h-12 w-12" : "")
} }
iconColor={`${nodeColors[types[data.type]]}`} iconColor={`${nodeColors[types[data.type]]}`}
/> />
{showNode && ( {showNode && (
<div className="generic-node-tooltip-div"> <div className="generic-node-tooltip-div">
{nameEditable && inputName ? (
<div>
<InputComponent
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
updateNodeInternals(data.id);
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div>
) : (
<ShadTooltip content={data.node?.display_name}> <ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary"> <div
className="flex"
onDoubleClick={() => {
setInputName(true);
takeSnapshot();
}}
>
<div className="generic-node-tooltip-div pr-2 text-primary">
{data.node?.display_name} {data.node?.display_name}
</div> </div>
{nameEditable && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
/>
)}
</div>
</ShadTooltip> </ShadTooltip>
)}
</div> </div>
)} )}
</div> </div>
@ -178,18 +219,16 @@ export default function GenericNode({
data.node!.template[templateField].show && data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && ( !data.node!.template[templateField].advanced && (
<ParameterComponent <ParameterComponent
key={ index={idx.toString()}
(data.node!.template[ key={scapedJSONStringfy({
templateField inputTypes:
].input_types?.join(";") ?? data.node!.template[templateField].input_types,
data.node!.template[templateField].type) + type: data.node!.template[templateField].type,
"|" + id: data.id,
templateField + fieldName: templateField,
"|" + proxy: data.node!.template[templateField].proxy,
data.id })}
}
data={data} data={data}
setData={setData}
color={ color={
nodeColors[ nodeColors[
types[data.node?.template[templateField].type!] types[data.node?.template[templateField].type!]
@ -199,15 +238,10 @@ export default function GenericNode({
] ?? ] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={getFieldTitle(
data.node?.template[templateField].display_name data.node?.template!,
? data.node.template[templateField].display_name templateField
: data.node?.template[templateField].name )}
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
@ -217,33 +251,32 @@ export default function GenericNode({
data.node?.template[templateField].type data.node?.template[templateField].type
} }
required={ required={
data.node?.template[templateField].required data.node!.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
} }
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true} left={true}
type={data.node?.template[templateField].type} type={data.node?.template[templateField].type}
optionalHandle={ optionalHandle={
data.node?.template[templateField].input_types data.node?.template[templateField].input_types
} }
proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) )
)} )}
<ParameterComponent <ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join( key={scapedJSONStringfy({
"|" baseClasses: data.node!.base_classes,
)} id: data.id,
dataType: data.type,
})}
data={data} data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown} color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={ title={
data.node?.output_types && data.node?.output_types &&
@ -252,9 +285,11 @@ export default function GenericNode({
: data.type : data.type
} }
tooltipTitle={data.node?.base_classes.join("\n")} tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join( id={{
"|" baseClasses: data.node!.base_classes,
)} id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}
@ -282,9 +317,9 @@ export default function GenericNode({
) : ( ) : (
<div className="max-h-96 overflow-auto"> <div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string" {typeof validationStatus.params === "string"
? validationStatus.params ? `Duration: ${validationStatus.duration}\n${validationStatus.params}`
.split("\n") .split("\n")
.map((line: string, index: number) => ( .map((line, index) => (
<div key={index}>{line}</div> <div key={index}>{line}</div>
)) ))
: ""} : ""}
@ -329,54 +364,102 @@ export default function GenericNode({
<div <div
className={ className={
showNode showNode
? "generic-node-desc " + ? "overflow-hidden " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5") (data.node?.description === "" && !nameEditable
? "pb-5"
: "py-5")
: "" : ""
} }
> >
{data.node?.description !== "" && showNode && ( <div className="generic-node-desc">
<div className="generic-node-desc-text"> {showNode && nameEditable && inputDescription ? (
{data.node?.description} <Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
updateNodeInternals(data.id);
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
updateNodeInternals(data.id);
}
}}
/>
) : (
<div
className={cn(
"generic-node-desc-text truncate-multiline word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable
? "font-light italic"
: ""
)}
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
}}
>
{(data.node?.description === "" || !data.node?.description) &&
nameEditable
? "Double Click to Edit Description"
: data.node?.description}
</div> </div>
)} )}
</div>
<> <>
{Object.keys(data.node!.template) {Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_") .filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => {
if (priorityFields.has(a.toLowerCase())) {
return -1;
} else if (priorityFields.has(b.toLowerCase())) {
return 1;
} else {
return a.localeCompare(b);
}
})
.map((templateField: string, idx) => ( .map((templateField: string, idx) => (
<div key={idx}> <div key={idx}>
{data.node!.template[templateField].show && {data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? ( !data.node!.template[templateField].advanced ? (
<ParameterComponent <ParameterComponent
key={ index={idx.toString()}
(data.node!.template[templateField].input_types?.join( key={scapedJSONStringfy({
";" inputTypes:
) ?? data.node!.template[templateField].type) + data.node!.template[templateField].input_types,
"|" + type: data.node!.template[templateField].type,
templateField + id: data.id,
"|" + fieldName: templateField,
data.id proxy: data.node!.template[templateField].proxy,
} })}
data={data} data={data}
setData={setData}
color={ color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[ nodeColors[
data.node?.template[templateField].type! data.node?.template[templateField].type!
] ?? ] ??
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors.unknown nodeColors.unknown
} }
title={ title={getFieldTitle(
data.node?.template[templateField].display_name data.node?.template!,
? data.node.template[templateField].display_name templateField
: data.node?.template[templateField].name )}
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
info={data.node?.template[templateField].info} info={data.node?.template[templateField].info}
name={templateField} name={templateField}
tooltipTitle={ tooltipTitle={
@ -384,21 +467,20 @@ export default function GenericNode({
"\n" "\n"
) ?? data.node?.template[templateField].type ) ?? data.node?.template[templateField].type
} }
required={data.node?.template[templateField].required} required={data.node!.template[templateField].required}
id={ id={{
(data.node?.template[templateField].input_types?.join( inputTypes:
";" data.node!.template[templateField].input_types,
) ?? data.node?.template[templateField].type) + type: data.node!.template[templateField].type,
"|" + id: data.id,
templateField + fieldName: templateField,
"|" + }}
data.id
}
left={true} left={true}
type={data.node?.template[templateField].type} type={data.node?.template[templateField].type}
optionalHandle={ optionalHandle={
data.node?.template[templateField].input_types data.node?.template[templateField].input_types
} }
proxy={data.node?.template[templateField].proxy}
showNode={showNode} showNode={showNode}
/> />
) : ( ) : (
@ -414,22 +496,37 @@ export default function GenericNode({
> >
{" "} {" "}
</div> </div>
{data.node!.base_classes.length > 0 && (
<ParameterComponent <ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")} key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data} data={data}
setData={setData} color={
color={nodeColors[types[data.type]] ?? nodeColors.unknown} (data.node?.output_types &&
data.node.output_types.length > 0
? nodeColors[data.node.output_types[0]] ??
nodeColors[types[data.node.output_types[0]]]
: nodeColors[types[data.type]]) ?? nodeColors.unknown
}
title={ title={
data.node?.output_types && data.node.output_types.length > 0 data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|") ? data.node.output_types.join("|")
: data.type : data.type
} }
tooltipTitle={data.node?.base_classes.join("\n")} tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join("|")} id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")} type={data.node?.base_classes.join("|")}
left={false} left={false}
showNode={showNode} showNode={showNode}
/> />
)}
</> </>
</div> </div>
)} )}

View file

@ -30,7 +30,7 @@ export default function AlertDropdown({
}} }}
> >
<PopoverTrigger>{children}</PopoverTrigger> <PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="flex h-[500px] w-[500px] flex-col"> <PopoverContent className="nocopy nopan nodelete nodrag noundo flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground"> <div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications Notifications
<div className="flex gap-3 pr-3 "> <div className="flex gap-3 pr-3 ">

View file

@ -40,7 +40,7 @@ export default function ErrorAlert({
removeAlert(id); removeAlert(id);
}, 500); }, 500);
}} }}
className="error-build-message" className="error-build-message nocopy nopan nodelete nodrag noundo"
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View file

@ -36,7 +36,7 @@ export default function NoticeAlert({
setShow(false); setShow(false);
removeAlert(id); removeAlert(id);
}} }}
className="mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl" className="nocopy nopan nodelete nodrag noundo mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View file

@ -34,7 +34,7 @@ export default function SuccessAlert({
setShow(false); setShow(false);
removeAlert(id); removeAlert(id);
}} }}
className="success-alert" className="success-alert nocopy nopan nodelete nodrag noundo"
> >
<div className="flex"> <div className="flex">
<div className="flex-shrink-0"> <div className="flex-shrink-0">

View file

@ -21,6 +21,7 @@ export default function DropdownButton({
<DropdownMenu open={showOptions}> <DropdownMenu open={showOptions}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
id="new-project-btn"
variant="primary" variant="primary"
className="relative pr-10" className="relative pr-10"
onClick={(event) => { onClick={(event) => {

View file

@ -1,31 +1,19 @@
import React, { ChangeEvent, useEffect, useRef, useState } from "react"; import React, { ChangeEvent, useState } from "react";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label"; import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { readFlowsFromDatabase } from "../../controllers/API";
import { InputProps } from "../../types/components"; import { InputProps } from "../../types/components";
import { FlowType } from "../../types/flow"; import { cn } from "../../utils/utils";
export const EditFlowSettings: React.FC<InputProps> = ({ export const EditFlowSettings: React.FC<InputProps> = ({
name, name,
invalidName, invalidNameList,
setInvalidName,
description, description,
maxLength = 50, maxLength = 50,
flows,
tabId,
setName, setName,
setDescription, setDescription,
}: InputProps): JSX.Element => { }: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false); const [isMaxLength, setIsMaxLength] = useState(false);
const nameLists = useRef<string[]>([]);
useEffect(() => {
readFlowsFromDatabase().then((flows) => {
flows.forEach((flow: FlowType) => {
nameLists.current.push(flow.name);
});
});
}, []);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => { const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target; const { value } = event.target;
@ -34,54 +22,52 @@ export const EditFlowSettings: React.FC<InputProps> = ({
} else { } else {
setIsMaxLength(false); setIsMaxLength(false);
} }
if (invalidName !== undefined) { setName!(value);
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
}
setName(value);
}; };
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => { const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value); setDescription!(event.target.value);
}; };
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
const handleFocus = (event) => event.target.select();
return ( return (
<> <>
<Label> <Label>
<div className="edit-flow-arrangement"> <div className="edit-flow-arrangement">
<span className="font-medium">Name</span>{" "} <span className="font-medium">Name{setName ? "" : ":"}</span>{" "}
{isMaxLength && ( {isMaxLength && (
<span className="edit-flow-span">Character limit reached</span> <span className="edit-flow-span">Character limit reached</span>
)} )}
{invalidName && (
<span className="edit-flow-span">Name already in use</span>
)}
</div> </div>
{setName ? (
<Input <Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal" className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange} onChange={handleNameChange}
type="text" type="text"
name="name" name="name"
value={name ?? ""} value={name ?? ""}
placeholder="File name" placeholder="Flow name"
id="name" id="name"
maxLength={maxLength} maxLength={maxLength}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/> />
) : (
<span className="font-normal text-muted-foreground word-break-break-word">
{name}
</span>
)}
</Label> </Label>
<Label> <Label>
<div className="edit-flow-arrangement mt-3"> <div className="edit-flow-arrangement mt-3">
<span className="font-medium ">Description (optional)</span> <span className="font-medium ">
Description{setDescription ? " (optional)" : ":"}
</span>
</div> </div>
{setDescription ? (
<Textarea <Textarea
name="description" name="description"
id="description" id="description"
@ -90,7 +76,20 @@ export const EditFlowSettings: React.FC<InputProps> = ({
placeholder="Flow description" placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal" className="mt-2 max-h-[100px] font-normal"
rows={3} rows={3}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/> />
) : (
<span
className={cn(
"font-normal text-muted-foreground word-break-break-word",
description === "" ? "font-light italic" : ""
)}
>
{description === "" ? "No description" : description}
</span>
)}
</Label> </Label>
</> </>
); );

View file

@ -11,11 +11,12 @@ import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
export default function PaginatorComponent({ export default function PaginatorComponent({
pageSize = 10, pageSize = 12,
pageIndex = 1, pageIndex = 1,
rowsCount = [10, 20, 50, 100], rowsCount = [12, 24, 48, 96],
totalRowsCount = 0, totalRowsCount = 0,
paginate, paginate,
storeComponent = false,
}: PaginatorComponentType) { }: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize); const [size, setPageSize] = useState(pageSize);
const [maxIndex, setMaxPageIndex] = useState( const [maxIndex, setMaxPageIndex] = useState(
@ -30,7 +31,13 @@ export default function PaginatorComponent({
<> <>
<div className="flex items-center justify-between px-2"> <div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"></div> <div className="flex-1 text-sm text-muted-foreground"></div>
<div className="flex items-center space-x-6 lg:space-x-8"> <div
className={
storeComponent
? "flex items-center lg:space-x-8 "
: "flex items-center space-x-6 lg:space-x-8 "
}
>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p> <p className="text-sm font-medium">Rows per page</p>
<Select <Select
@ -54,7 +61,8 @@ export default function PaginatorComponent({
</Select> </Select>
</div> </div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium"> <div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pageIndex} of {maxIndex} Page {pageIndex}
{!storeComponent && <> of {maxIndex}</>}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button

View file

@ -11,6 +11,7 @@ const SanitizedHTMLWrapper = ({
return ( return (
<div <div
data-testid="edit-prompt-sanitized"
className={className} className={className}
dangerouslySetInnerHTML={{ __html: sanitizedHTML }} dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
suppressContentEditableWarning={suppressWarning} suppressContentEditableWarning={suppressWarning}

View file

@ -1,8 +1,16 @@
import { useContext } from "react"; import { useContext, useEffect, useState } from "react";
import { TabsContext } from "../../contexts/tabsContext"; import { alertContext } from "../../contexts/alertContext";
import { cardComponentPropsType } from "../../types/components"; import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils"; import { StoreContext } from "../../contexts/storeContext";
import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent from "../../utils/storeUtils";
import { cn } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { import {
Card, Card,
CardDescription, CardDescription,
@ -11,48 +19,326 @@ import {
CardTitle, CardTitle,
} from "../ui/card"; } from "../ui/card";
export const CardComponent = ({ export default function CollectionCardComponent({
flow, data,
id, authorized = true,
onDelete, disabled = false,
button, button,
}: cardComponentPropsType): JSX.Element => { onDelete,
const { removeFlow } = useContext(TabsContext); }: {
data: storeComponent;
authorized?: boolean;
disabled?: boolean;
button?: JSX.Element;
onDelete?: () => void;
}) {
const { addFlow } = useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { setValidApiKey } = useContext(StoreContext);
const isStore = false;
const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false);
const [liked_by_user, setLiked_by_user] = useState(
data?.liked_by_user ?? false
);
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0
);
const name = data.is_component ? "Component" : "Flow";
useEffect(() => {
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
setLikes_count(data?.liked_by_count ?? 0);
setDownloads_count(data?.downloads_count ?? 0);
}
}, [data, data.liked_by_count, data.liked_by_user, data.downloads_count]);
function handleInstall() {
const temp = downloads_count;
setDownloads_count((old) => Number(old) + 1);
setLoading(true);
getComponent(data.id)
.then((res) => {
const newFlow = cloneFLowWithParent(res, res.id, data.is_component);
addFlow(true, newFlow)
.then((id) => {
setSuccessData({
title: `${name} ${
isStore ? "Downloaded" : "Installed"
} Successfully.`,
});
setLoading(false);
})
.catch((error) => {
setLoading(false);
setErrorData({
title: `Error ${
isStore ? "downloading" : "installing"
} the ${name}`,
list: [error["response"]["data"]["detail"]],
});
});
})
.catch((err) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [err["response"]["data"]["detail"]],
});
setDownloads_count(temp);
});
}
function handleLike() {
setLoadingLike(true);
if (liked_by_user !== undefined || liked_by_user !== null) {
const temp = liked_by_user;
const tempNum = likes_count;
setLiked_by_user((prev) => !prev);
if (!temp) {
setLikes_count((prev) => Number(prev) + 1);
} else {
setLikes_count((prev) => Number(prev) - 1);
}
postLikeComponent(data.id)
.then((response) => {
setLoadingLike(false);
setLikes_count(response.data.likes_count);
setLiked_by_user(response.data.liked_by_user);
})
.catch((error) => {
setLoadingLike(false);
setLikes_count(tempNum);
setLiked_by_user(temp);
if (error.response.status === 403) {
setValidApiKey(false);
} else {
console.error(error);
setErrorData({
title: `Error liking ${name}.`,
list: [error["response"]["data"]["detail"]],
});
}
});
}
}
return ( return (
<Card className="group"> <Card
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
<div>
<CardHeader> <CardHeader>
<CardTitle className="card-component-title-display"> <div>
<span <CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
className={ <IconComponent
"card-component-image " + className={cn(
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length] "flex-shrink-0",
} data.is_component
></span> ? "mx-0.5 h-6 w-6 text-component-icon"
<span className="card-component-title-size">{flow.name}</span> : "h-7 w-7 flex-shrink-0 text-flow-icon"
{onDelete && ( )}
<button className="card-component-delete-button" onClick={onDelete}> name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
{data?.metadata?.total ?? 0}
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
{likes_count ?? 0}
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
{downloads_count ?? 0}
</span>
</ShadTooltip>
</div>
)}
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="card-component-delete-icon" className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/> />
</button> </DeleteConfirmationModal>
)} )}
</CardTitle> </CardTitle>
<CardDescription className="card-component-desc">
<div className="card-component-desc-text">
{flow.description}
{/* {flow.description} */}
</div> </div>
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
⛓︎ v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</div>
<CardFooter> <CardFooter>
<div className="card-component-footer-arrangement"> <div className="flex w-full items-center justify-between gap-2">
<div className="card-component-footer"></div> <div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<Button
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
>
<IconComponent
name="Heart"
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip
content={
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loading}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (loading || !authorized) {
return;
}
handleInstall();
}}
>
<IconComponent
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
className={cn(
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
</div>
)}
{button && button} {button && button}
</div> </div>
</div>
</CardFooter> </CardFooter>
</Card> </Card>
); );
}; }

View file

@ -0,0 +1,63 @@
import { useState } from "react";
import IconComponent from "../../components/genericIconComponent";
export default function CardsWrapComponent({
onFileDrop,
children,
dragMessage,
}: {
onFileDrop?: (e: any) => void;
children: JSX.Element | JSX.Element[];
dragMessage?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
if (onFileDrop) setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (onFileDrop) onFileDrop(e);
setIsDragging(false);
};
return (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<IconComponent name="ArrowUpToLine" className="h-12 w-12 stroke-1" />
{dragMessage ? dragMessage : "Drop your file here"}
</>
) : (
children
)}
</div>
);
}

View file

@ -7,9 +7,9 @@ import { typesContext } from "../../../contexts/typesContext";
import { postBuildInit } from "../../../controllers/API"; import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow"; import { FlowType } from "../../../types/flow";
import { TabsContext } from "../../../contexts/tabsContext"; import { FlowsContext } from "../../../contexts/flowsContext";
import { parsedDataType } from "../../../types/components"; import { parsedDataType } from "../../../types/components";
import { TabsState } from "../../../types/tabs"; import { FlowsState } from "../../../types/tabs";
import { validateNodes } from "../../../utils/reactflowUtils"; import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress"; import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent"; import IconComponent from "../../genericIconComponent";
@ -26,7 +26,7 @@ export default function BuildTrigger({
}): JSX.Element { }): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE(); const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext); const { setTabsState, saveFlow } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext); const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false); const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : ""; const eventClick = isBuilding ? "pointer-events-none" : "";
@ -76,12 +76,13 @@ export default function BuildTrigger({
} }
async function streamNodeData(flow: FlowType) { async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID // Step 1: Make a POST request to send the flow data and receive a unique session ID
const id = saveFlow(flow, true);
const response = await postBuildInit(flow); const response = await postBuildInit(flow);
const { flowId } = response.data; const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource // Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = []; let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`; const apiUrl = `/api/v1/build/stream/${flowId}`;
return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl); const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => { eventSource.onmessage = (event) => {
@ -92,17 +93,14 @@ export default function BuildTrigger({
const parsedData = JSON.parse(event.data); const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection // if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) { if (parsedData.end_of_stream) {
// Close the connection and finish
finished = true;
eventSource.close(); eventSource.close();
resolve(validationResults.every((result) => result));
return;
} else if (parsedData.log) { } else if (parsedData.log) {
// If the event is a log, log it // If the event is a log, log it
setSuccessData({ title: parsedData.log }); setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) { } else if (parsedData.input_keys !== undefined) {
//@ts-ignore //@ts-ignore
setTabsState((old: TabsState) => { setTabsState((old: FlowsState) => {
return { return {
...old, ...old,
[flowId]: { [flowId]: {
@ -121,20 +119,16 @@ export default function BuildTrigger({
eventSource.onerror = (error: any) => { eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error); console.error("EventSource failed:", error);
eventSource.close();
if (error.data) { if (error.data) {
const parsedData = JSON.parse(error.data); const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error }); setErrorData({ title: parsedData.error });
setIsBuilding(false); setIsBuilding(false);
} }
eventSource.close();
reject(new Error("Streaming failed"));
}; };
// Step 3: Wait for the stream to finish });
while (!finished) {
await new Promise((resolve) => setTimeout(resolve, 100));
finished = validationResults.length === flow.data!.nodes.length;
}
// Step 4: Return true if all nodes are valid, false otherwise
return validationResults.every((result) => result);
} }
function processStreamResult(parsedData: parsedDataType) { function processStreamResult(parsedData: parsedDataType) {

View file

@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
import ChatTrigger from "./chatTrigger"; import ChatTrigger from "./chatTrigger";
import * as _ from "lodash"; import * as _ from "lodash";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal"; import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow"; import { NodeType } from "../../types/flow";
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType): JSX.Element { export default function Chat({ flow }: ChatType): JSX.Element {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false); const [canOpen, setCanOpen] = useState(false);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext); const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
useEffect(() => { useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
@ -36,7 +36,7 @@ export default function Chat({ flow }: ChatType): JSX.Element {
// Define an async function within the useEffect hook // Define an async function within the useEffect hook
const fetchBuildStatus = async () => { const fetchBuildStatus = async () => {
const response = await getBuildStatus(flow.id); const response = await getBuildStatus(flow.id);
setIsBuilt(response.built); setIsBuilt(response.data.built);
}; };
// Call the async function // Call the async function

View file

@ -12,6 +12,8 @@ export default function CodeAreaComponent({
nodeClass, nodeClass,
dynamic, dynamic,
setNodeClass, setNodeClass,
id = "",
readonly = false,
}: CodeAreaComponentType) { }: CodeAreaComponentType) {
const [myValue, setMyValue] = useState( const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value) typeof value == "string" ? value : JSON.stringify(value)
@ -30,6 +32,7 @@ export default function CodeAreaComponent({
return ( return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal <CodeAreaModal
readonly={readonly}
dynamic={dynamic} dynamic={dynamic}
value={myValue} value={myValue}
nodeClass={nodeClass} nodeClass={nodeClass}
@ -41,6 +44,8 @@ export default function CodeAreaComponent({
> >
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<span <span
id={id}
data-testid={id}
className={ className={
editNode editNode
? "input-edit-node input-dialog" ? "input-edit-node input-dialog"

View file

@ -27,6 +27,7 @@ import {
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "../../components/ui/tabs"; } from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { codeTabsPropsType } from "../../types/components"; import { codeTabsPropsType } from "../../types/components";
@ -63,7 +64,7 @@ export default function CodeTabsComponent({
}, [flow]); }, [flow]);
useEffect(() => { useEffect(() => {
if (tweaks) { if (tweaks && data) {
unselectAllNodes({ unselectAllNodes({
data, data,
updateNodes: (nodes) => { updateNodes: (nodes) => {
@ -242,24 +243,10 @@ export default function CodeTabsComponent({
templateField.charAt(0) !== "_" && templateField.charAt(0) !== "_" &&
node.data.node.template[templateField] node.data.node.template[templateField]
.show && .show &&
(node.data.node.template[templateField] LANGFLOW_SUPPORTED_TYPES.has(
.type === "str" ||
node.data.node.template[templateField] node.data.node.template[templateField]
.type === "bool" || .type
node.data.node.template[templateField] )
.type === "float" ||
node.data.node.template[templateField]
.type === "code" ||
node.data.node.template[templateField]
.type === "prompt" ||
node.data.node.template[templateField]
.type === "file" ||
node.data.node.template[templateField]
.type === "int" ||
node.data.node.template[templateField]
.type === "dict" ||
node.data.node.template[templateField]
.type === "NestedDict")
) )
.map((templateField, indx) => { .map((templateField, indx) => {
return ( return (
@ -457,11 +444,6 @@ export default function CodeTabsComponent({
templateField templateField
].fileTypes ].fileTypes
} }
suffixes={
node.data.node.template[
templateField
].suffixes
}
onFileChange={( onFileChange={(
value: any value: any
) => { ) => {
@ -490,6 +472,11 @@ export default function CodeTabsComponent({
templateField templateField
].value ].value
} }
rangeSpec={
node.data.node.template[
templateField
].rangeSpec
}
onChange={(target) => { onChange={(target) => {
setData((old) => { setData((old) => {
let newInputList = let newInputList =
@ -604,6 +591,14 @@ export default function CodeTabsComponent({
].type === "prompt" ? ( ].type === "prompt" ? (
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
editNode={true} editNode={true}
disabled={false} disabled={false}
value={ value={
@ -646,6 +641,14 @@ export default function CodeTabsComponent({
<CodeAreaComponent <CodeAreaComponent
disabled={false} disabled={false}
editNode={true} editNode={true}
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
value={ value={
!node.data.node.template[ !node.data.node.template[
templateField templateField

View file

@ -10,6 +10,7 @@ export default function DictComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
id = "",
}: DictComponentType): JSX.Element { }: DictComponentType): JSX.Element {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -22,7 +23,6 @@ export default function DictComponent({
}, [value]); }, [value]);
const ref = useRef(value); const ref = useRef(value);
debugger;
return ( return (
<div <div
className={classNames( className={classNames(
@ -31,7 +31,7 @@ export default function DictComponent({
)} )}
> >
{ {
<div className="flex w-full gap-3"> <div className="flex w-full gap-3" data-testid={id}>
<DictAreaModal <DictAreaModal
value={ref.current} value={ref.current}
onChange={(obj) => { onChange={(obj) => {
@ -46,6 +46,7 @@ export default function DictComponent({
: "input-disable pointer-events-none cursor-pointer" : "input-disable pointer-events-none cursor-pointer"
} }
placeholder="Click to edit your dictionary..." placeholder="Click to edit your dictionary..."
data-testid="dict-input"
/> />
</DictAreaModal> </DictAreaModal>
</div> </div>

View file

@ -11,6 +11,7 @@ export default function Dropdown({
editNode = false, editNode = false,
numberOfOptions = 0, numberOfOptions = 0,
apiModal = false, apiModal = false,
id = "",
}: DropDownComponentType): JSX.Element { }: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState( let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value value === "" || !value ? "Choose an option" : value
@ -31,15 +32,19 @@ export default function Dropdown({
> >
{({ open }) => ( {({ open }) => (
<> <>
<div className={editNode ? "mt-1" : "relative mt-1"}> <div className={"relative mt-1"}>
<Listbox.Button <Listbox.Button
data-test={`${id ?? ""}`}
className={ className={
editNode editNode
? "dropdown-component-outline" ? "dropdown-component-outline"
: "dropdown-component-false-outline" : "dropdown-component-false-outline"
} }
> >
<span className="dropdown-component-display"> <span
className="dropdown-component-display"
data-testid={`${id ?? ""}-display`}
>
{internalValue} {internalValue}
</span> </span>
<span className={"dropdown-component-arrow"}> <span className={"dropdown-component-arrow"}>
@ -63,7 +68,7 @@ export default function Dropdown({
editNode editNode
? "dropdown-component-true-options nowheel custom-scroll" ? "dropdown-component-true-options nowheel custom-scroll"
: "dropdown-component-false-options nowheel custom-scroll", : "dropdown-component-false-options nowheel custom-scroll",
apiModal ? "mb-2 w-[250px]" : "absolute" apiModal ? "mb-2 w-[250px]" : "absolute w-full"
)} )}
> >
{options.map((option, id) => ( {options.map((option, id) => (
@ -86,6 +91,7 @@ export default function Dropdown({
selected ? "font-semibold" : "font-normal", selected ? "font-semibold" : "font-normal",
"block truncate " "block truncate "
)} )}
data-testid={`${option}-${id ?? ""}-option`}
> >
{option} {option}
</span> </span>

View file

@ -7,12 +7,12 @@ export default function FloatComponent({
value, value,
onChange, onChange,
disabled, disabled,
rangeSpec,
editNode = false, editNode = false,
}: FloatComponentType): JSX.Element { }: FloatComponentType): JSX.Element {
const step = 0.1; const step = rangeSpec?.step ?? 0.1;
const min = -2; const min = rangeSpec?.min ?? -2;
const max = 2; const max = rangeSpec?.max ?? 2;
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -23,6 +23,7 @@ export default function FloatComponent({
return ( return (
<div className="w-full"> <div className="w-full">
<Input <Input
id="float-input"
type="number" type="number"
step={step} step={step}
min={min} min={min}
@ -39,7 +40,9 @@ export default function FloatComponent({
disabled={disabled} disabled={disabled}
className={editNode ? "input-edit-node" : ""} className={editNode ? "input-edit-node" : ""}
placeholder={ placeholder={
editNode ? "Number -2 to 2" : "Type a number from minus two to two" editNode
? `Enter a value between ${min} and ${max}`
: `Enter a value between ${min} and ${max}`
} }
onChange={(event) => { onChange={(event) => {
onChange(event.target.value); onChange(event.target.value);

View file

@ -3,14 +3,31 @@ import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils"; import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef( const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => { (
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"]; const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
return ( return (
<TargetIcon <TargetIcon
strokeWidth={1.5}
className={className} className={className}
style={iconColor ? { color: iconColor } : {}} style={style}
ref={ref} ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/> />
); );
} }

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -8,7 +8,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "../../../ui/dropdown-menu"; } from "../../../ui/dropdown-menu";
import { Link, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import FlowSettingsModal from "../../../../modals/flowSettingsModal"; import FlowSettingsModal from "../../../../modals/flowSettingsModal";
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button"; import { Button } from "../../../ui/button";
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => { export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
const { addFlow } = useContext(TabsContext); const { addFlow } = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { undo, redo } = useContext(undoRedoContext); const { undo, redo } = useContext(undoRedoContext);
const [openSettings, setOpenSettings] = useState(false); const [openSettings, setOpenSettings] = useState(false);
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
function handleAddFlow() { function handleAddFlow() {
try { try {
addFlow(undefined, true).then((id) => { addFlow(true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
// saveFlowStyleInDataBase(); // saveFlowStyleInDataBase();
@ -38,9 +38,13 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
return ( return (
<div className="round-button-div"> <div className="round-button-div">
<Link to="/"> <button
onClick={() => {
navigate(-1);
}}
>
<IconComponent name="ChevronLeft" className="w-4" /> <IconComponent name="ChevronLeft" className="w-4" />
</Link> </button>
<div className="header-menu-bar"> <div className="header-menu-bar">
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>

View file

@ -6,7 +6,9 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext"; import { StoreContext } from "../../contexts/storeContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
@ -22,17 +24,18 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar"; import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element { export default function Header(): JSX.Element {
const { flows, tabId } = useContext(TabsContext); const { flows, tabId } = useContext(FlowsContext);
const { dark, setDark } = useContext(darkContext); const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext); const { notificationCenter } = useContext(alertContext);
const location = useLocation(); const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext); const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const { hasStore } = useContext(StoreContext);
const { stars, gradientIndex } = useContext(darkContext); const { stars, gradientIndex } = useContext(darkContext);
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div className="header-arrangement"> <div className="header-arrangement">
<div className="header-start-display"> <div className="header-start-display lg:w-[30%]">
<Link to="/"> <Link to="/">
<span className="ml-4 text-2xl">⛓️</span> <span className="ml-4 text-2xl">⛓️</span>
</Link> </Link>
@ -45,14 +48,19 @@ export default function Header(): JSX.Element {
<Link to="/"> <Link to="/">
<Button <Button
className="gap-2" className="gap-2"
variant={location.pathname === "/" ? "primary" : "secondary"} variant={
location.pathname === "/flows" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm" size="sm"
> >
<IconComponent name="Home" className="h-4 w-4" /> <IconComponent name="Home" className="h-4 w-4" />
<div className="flex-1">{USER_PROJECTS_HEADER}</div> <div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button> </Button>
</Link> </Link>
<Link to="/community"> {/* <Link to="/community">
<Button <Button
className="gap-2" className="gap-2"
variant={ variant={
@ -63,18 +71,30 @@ export default function Header(): JSX.Element {
<IconComponent name="Users2" className="h-4 w-4" /> <IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div> <div className="flex-1">Community Examples</div>
</Button> </Button>
</Link> */}
{hasStore && (
<Link to="/store">
<Button
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
>
<IconComponent name="Store" className="h-4 w-4" />
<div className="flex-1">Store</div>
</Button>
</Link> </Link>
)}
</div> </div>
<div className="header-end-division"> <div className="header-end-division lg:w-[30%]">
<div className="header-end-display"> <div className="header-end-display">
<a <a
href="https://github.com/logspace-ai/langflow" href="https://github.com/logspace-ai/langflow"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="header-github-link" className="header-github-link gap-2"
> >
<FaGithub className="mr-2 h-5 w-5" /> <FaGithub className="h-5 w-5" />
Star <div className="hidden lg:block">Star</div>
<div className="header-github-display">{stars}</div> <div className="header-github-display">{stars}</div>
</a> </a>
<a <a

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form"; import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components"; import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils"; import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
export default function InputComponent({ export default function InputComponent({
autoFocus = false,
onBlur,
value, value,
onChange, onChange,
disabled, disabled,
@ -15,9 +17,11 @@ export default function InputComponent({
editNode = false, editNode = false,
placeholder = "Type something...", placeholder = "Type something...",
className, className,
id = "",
blurOnEnter = false,
}: InputComponentType): JSX.Element { }: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false); const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -30,6 +34,10 @@ export default function InputComponent({
{isForm ? ( {isForm ? (
<Form.Control asChild> <Form.Control asChild>
<Input <Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"} type={password && !pwdVisible ? "password" : "text"}
value={value} value={value}
disabled={disabled} disabled={disabled}
@ -47,15 +55,26 @@ export default function InputComponent({
onChange={(e) => { onChange={(e) => {
onChange(e.target.value); onChange(e.target.value);
}} }}
onCopy={(e) => {
e.preventDefault();
}}
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.ctrlKey && e.key === "c") {
// Perform any actions you need when Ctrl+C is detected
}
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
</Form.Control> </Form.Control>
) : ( ) : (
<Input <Input
id={id}
ref={refInput}
type="text" type="text"
onBlur={onBlur}
value={value} value={value}
autoFocus={autoFocus}
disabled={disabled} disabled={disabled}
required={required} required={required}
className={classNames( className={classNames(
@ -73,6 +92,7 @@ export default function InputComponent({
}} }}
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}} }}
/> />
)} )}

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { uploadFile } from "../../controllers/API"; import { uploadFile } from "../../controllers/API";
import { FileComponentType } from "../../types/components"; import { FileComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
@ -9,7 +9,6 @@ export default function InputFileComponent({
value, value,
onChange, onChange,
disabled, disabled,
suffixes,
fileTypes, fileTypes,
onFileChange, onFileChange,
editNode = false, editNode = false,
@ -17,7 +16,7 @@ export default function InputFileComponent({
const [myValue, setMyValue] = useState(value); const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext); const { setErrorData } = useContext(alertContext);
const { tabId } = useContext(TabsContext); const { tabId } = useContext(FlowsContext);
// Clear component state // Clear component state
useEffect(() => { useEffect(() => {
@ -29,8 +28,9 @@ export default function InputFileComponent({
}, [disabled, onChange]); }, [disabled, onChange]);
function checkFileType(fileName: string): boolean { function checkFileType(fileName: string): boolean {
for (let index = 0; index < suffixes.length; index++) { if (fileTypes === undefined) return true;
if (fileName.endsWith(suffixes[index])) { for (let index = 0; index < fileTypes.length; index++) {
if (fileName.endsWith(fileTypes[index])) {
return true; return true;
} }
} }
@ -45,7 +45,7 @@ export default function InputFileComponent({
// Create a file input element // Create a file input element
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = suffixes.join(","); input.accept = fileTypes?.join(",");
input.style.display = "none"; // Hidden from view input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection input.multiple = false; // Allow only one file selection

View file

@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { FloatComponentType } from "../../types/components"; import { IntComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils"; import { handleKeyDown } from "../../utils/reactflowUtils";
import { Input } from "../ui/input"; import { Input } from "../ui/input";
@ -8,7 +8,8 @@ export default function IntComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
}: FloatComponentType): JSX.Element { id = "",
}: IntComponentType): JSX.Element {
const min = 0; const min = 0;
// Clear component state // Clear component state
@ -21,6 +22,7 @@ export default function IntComponent({
return ( return (
<div className="w-full"> <div className="w-full">
<Input <Input
id={id}
onKeyDown={(event) => { onKeyDown={(event) => {
if ( if (
event.key !== "Backspace" && event.key !== "Backspace" &&
@ -34,6 +36,8 @@ export default function IntComponent({
event.key !== "c" && event.key !== "c" &&
event.key !== "v" && event.key !== "v" &&
event.key !== "a" && event.key !== "a" &&
event.key !== "ArrowUp" &&
event.key !== "ArrowDown" &&
!/^[-]?\d*$/.test(event.key) !/^[-]?\d*$/.test(event.key)
) { ) {
event.preventDefault(); event.preventDefault();

View file

@ -12,8 +12,6 @@ export default function KeypairListComponent({
disabled, disabled,
editNode = false, editNode = false,
duplicateKey, duplicateKey,
advanced = false,
dataValue,
}: KeyPairListComponentType): JSX.Element { }: KeyPairListComponentType): JSX.Element {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
@ -55,6 +53,10 @@ export default function KeypairListComponent({
return ( return (
<div key={idx} className="flex w-full gap-2"> <div key={idx} className="flex w-full gap-2">
<Input <Input
data-testid={
editNode ? "editNodekeypair" + index : "keypair" + index
}
id={editNode ? "editNodekeypair" + index : "keypair" + index}
type="text" type="text"
value={key.trim()} value={key.trim()}
className={classNames( className={classNames(
@ -72,6 +74,16 @@ export default function KeypairListComponent({
/> />
<Input <Input
data-testid={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
id={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
type="text" type="text"
value={obj[key]} value={obj[key]}
className={editNode ? "input-edit-node" : ""} className={editNode ? "input-edit-node" : ""}
@ -88,6 +100,16 @@ export default function KeypairListComponent({
newInputList.push({ "": "" }); newInputList.push({ "": "" });
onChange(newInputList); onChange(newInputList);
}} }}
id={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
data-testid={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
> >
<IconComponent <IconComponent
name="Plus" name="Plus"
@ -101,6 +123,16 @@ export default function KeypairListComponent({
newInputList.splice(index, 1); newInputList.splice(index, 1);
onChange(newInputList); onChange(newInputList);
}} }}
data-testid={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
id={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
> >
<IconComponent <IconComponent
name="X" name="X"

View file

@ -0,0 +1,31 @@
import Header from "../headerComponent";
import { Separator } from "../ui/separator";
export default function PageLayout({
title,
description,
children,
button,
}: {
title: string;
description: string;
children: React.ReactNode;
button?: React.ReactNode;
}) {
return (
<div className="flex h-screen w-full flex-col">
<Header />
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5 py-8 pb-2">
<div className="flex w-full flex-col">
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
<div className="flex-shrink-0">{button && button}</div>
</div>
<Separator className="my-6 flex" />
{children}
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { TypeModal } from "../../constants/enums"; import { TypeModal } from "../../constants/enums";
import { postValidatePrompt } from "../../controllers/API";
import GenericModal from "../../modals/genericModal"; import GenericModal from "../../modals/genericModal";
import { PromptAreaComponentType } from "../../types/components"; import { PromptAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent"; import IconComponent from "../genericIconComponent";
@ -14,39 +13,32 @@ export default function PromptAreaComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
}: PromptAreaComponentType) { id = "",
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => { useEffect(() => {
if (disabled) { if (disabled) {
onChange(""); onChange("");
} }
}, [disabled]); }, [disabled]);
useEffect(() => {
if (value !== "" && !editNode) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node);
// need to update reactFlowInstance to re-render the nodes.
}
});
}
}, []);
return ( return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}> <div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal <GenericModal
id={id}
readonly={readonly}
type={TypeModal.PROMPT} type={TypeModal.PROMPT}
value={value} value={value}
buttonText="Check & Save" buttonText="Check & Save"
modalTitle="Edit Prompt" modalTitle="Edit Prompt"
setValue={(value: string) => { setValue={onChange}
onChange(value);
}}
nodeClass={nodeClass} nodeClass={nodeClass}
setNodeClass={setNodeClass} setNodeClass={setNodeClass}
> >
<div className="flex w-full items-center"> <div className="flex w-full items-center">
<span <span
id={id}
data-testid={id}
className={ className={
editNode editNode
? "input-edit-node input-dialog" ? "input-edit-node input-dialog"
@ -58,6 +50,7 @@ export default function PromptAreaComponent({
</span> </span>
{!editNode && ( {!editNode && (
<IconComponent <IconComponent
id={id}
name="ExternalLink" name="ExternalLink"
className={ className={
"icons-parameters-comp" + "icons-parameters-comp" +

View file

@ -0,0 +1,47 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "../../utils/utils";
import { buttonVariants } from "../ui/button";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
icon: React.ReactNode;
}[];
}
export default function SidebarNav({
className,
items,
...props
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"justify-start gap-2"
)}
>
{item.icon}
{item.title}
</Link>
))}
</nav>
);
}

View file

@ -0,0 +1,30 @@
import React, { ReactNode } from "react";
interface ElementStackProps {
children: ReactNode[];
}
const ElementStack: React.FC<ElementStackProps> = ({ children }) => {
return (
<div
className={`grid grid-cols-1`}
style={{ display: "grid", gridAutoFlow: "row" }}
>
{children.map((child, index) => (
<div
key={index}
style={{
gridColumn: 1,
gridRow: 1,
transform: `translateX(${index * 0.1}rem)`,
zIndex: children.length - index,
}}
>
{child}
</div>
))}
</div>
);
};
export default ElementStack;

View file

@ -0,0 +1,12 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { StoreContext } from "../../contexts/storeContext";
export const StoreGuard = ({ children }) => {
const { hasStore } = useContext(StoreContext);
if (!hasStore) {
return <Navigate to="/flows" replace />;
}
return children;
};

View file

@ -0,0 +1,111 @@
import { useContext, useEffect, useRef, useState } from "react";
import { darkContext } from "../../contexts/darkContext";
import { cn } from "../../utils/utils";
import { Badge } from "../ui/badge";
export function TagsSelector({
tags,
disabled = false,
loadingTags,
selectedTags,
setSelectedTags,
}: {
tags: { id: string; name: string }[];
disabled?: boolean;
loadingTags: boolean;
selectedTags: any[];
setSelectedTags: (tags: any[]) => void;
}) {
const updateTags = (tagName: string) => {
const index = selectedTags.indexOf(tagName);
let newArray =
index === -1
? [...selectedTags, tagName]
: selectedTags.filter((_, i) => i !== index);
setSelectedTags(newArray);
};
const { dark } = useContext(darkContext);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, loadingTags]); // Depend on divWidth
return (
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex min-w-min gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
key={idx}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
);
}

View file

@ -10,6 +10,7 @@ export default function TextAreaComponent({
onChange, onChange,
disabled, disabled,
editNode = false, editNode = false,
id = "",
}: TextAreaComponentType): JSX.Element { }: TextAreaComponentType): JSX.Element {
// Clear text area // Clear text area
useEffect(() => { useEffect(() => {
@ -19,17 +20,11 @@ export default function TextAreaComponent({
}, [disabled]); }, [disabled]);
return ( return (
<div className="flex w-full items-center"> <div
<Input className={
value={value} "flex w-full items-center " + (disabled ? "pointer-events-none" : "")
disabled={disabled} }
className={editNode ? "input-edit-node" : ""} >
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
}}
/>
<div>
<GenericModal <GenericModal
type={TypeModal.TEXT} type={TypeModal.TEXT}
buttonText="Finishing Editing" buttonText="Finishing Editing"
@ -39,8 +34,25 @@ export default function TextAreaComponent({
onChange(value); onChange(value);
}} }}
> >
<div className="flex w-full items-center" data-testid={"div-" + id}>
<Input
id={id}
data-testid={id}
value={value}
disabled={disabled}
className={
editNode
? "input-edit-node pointer-events-none "
: " pointer-events-none"
}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
}}
/>
{!editNode && ( {!editNode && (
<IconComponent <IconComponent
id={id}
name="ExternalLink" name="ExternalLink"
className={ className={
"icons-parameters-comp" + "icons-parameters-comp" +
@ -48,8 +60,8 @@ export default function TextAreaComponent({
} }
/> />
)} )}
</GenericModal>
</div> </div>
</GenericModal>
</div> </div>
); );
} }

View file

@ -6,6 +6,7 @@ export default function ToggleShadComponent({
setEnabled, setEnabled,
disabled, disabled,
size, size,
id = "",
}: ToggleComponentType): JSX.Element { }: ToggleComponentType): JSX.Element {
let scaleX, scaleY; let scaleX, scaleY;
switch (size) { switch (size) {
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
return ( return (
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}> <div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch <Switch
id={id}
style={{ style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`, transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}} }}

View file

@ -14,12 +14,14 @@ const badgeVariants = cva(
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground", "bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive: destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground", "bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground", outline: "text-primary/80 hover:bg-muted border-ring/60",
}, },
size: { size: {
sm: "h-4 text-xs", sm: "h-4 text-xs",
md: "h-5 text-sm", md: "h-5 text-sm",
lg: "h-6 text-base", lg: "h-6 text-base",
sq: "h-6 text-sm font-normal rounded-md",
xq: "h-5 text-xs font-normal rounded-md",
}, },
}, },
defaultVariants: { defaultVariants: {

View file

@ -14,15 +14,16 @@ const buttonVariants = cva(
outline: outline:
"border border-input hover:bg-accent hover:text-accent-foreground", "border border-input hover:bg-accent hover:text-accent-foreground",
primary: primary:
"border bg-background text-secondary-foreground hover:bg-background/80 dark:hover:bg-background/10 hover:shadow-sm", "border bg-background text-secondary-foreground hover:bg-secondary-foreground/5 dark:hover:bg-background/10 hover:shadow-sm",
secondary: secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80", "border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
ghost: "hover:bg-accent hover:text-accent-foreground", ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary", link: "underline-offset-4 hover:underline text-primary",
}, },
size: { size: {
default: "h-10 py-2 px-4", default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md", sm: "h-9 px-3 rounded-md",
xs: "py-1 px-1 rounded-md",
lg: "h-11 px-8 rounded-md", lg: "h-11 px-8 rounded-md",
}, },
}, },

View file

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div <div
ref={ref} ref={ref}
className={cn( className={cn(
"flex flex-col justify-between rounded-lg border bg-card text-card-foreground shadow-sm transition-all hover:shadow-lg", "flex flex-col justify-between rounded-lg border bg-muted text-card-foreground shadow-sm transition-all hover:shadow-lg",
className className
)} )}
{...props} {...props}

View file

@ -0,0 +1,79 @@
"use client";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Button } from "./button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "./command";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
export function Combobox({
items,
onChange,
}: {
items: { value: string; label: string }[];
onChange: (value: string[]) => void;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<string[]>([]);
React.useEffect(() => {
onChange(value);
}, [value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? items.find((framework) => value.includes(framework.value))?.label
: "Select filter..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search filters..." />
<CommandEmpty>No filters found.</CommandEmpty>
<CommandGroup>
{items.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue((old) => {
if (old.includes(currentValue)) {
return old.filter((item) => item !== currentValue);
}
return [...old, currentValue];
});
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(framework.value)
? "opacity-100"
: "opacity-0"
)}
/>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -0,0 +1,155 @@
"use client";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Dialog, DialogContent } from "./dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
};

View file

@ -18,14 +18,14 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", "flex h-8 items-center justify-between rounded-md border border-ring/60 px-4 py-2 text-sm text-primary ring-offset-background placeholder:text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<SelectPrimitive.Icon asChild> <SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" /> <ChevronDown className="ml-2 h-4 w-4" />
</SelectPrimitive.Icon> </SelectPrimitive.Icon>
</SelectPrimitive.Trigger> </SelectPrimitive.Trigger>
)); ));
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item <SelectPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
)} )}
{...props} {...props}

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
"overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1", "z-45 overflow-y-auto rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className className
)} )}
{...props} {...props}

View file

@ -517,16 +517,19 @@ export const ADMIN_HEADER_TITLE = "Admin Page";
export const ADMIN_HEADER_DESCRIPTION = export const ADMIN_HEADER_DESCRIPTION =
"Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts."; "Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts.";
export const BASE_URL_API = "/api/v1/";
/** /**
* URLs excluded from error retries. * URLs excluded from error retries.
* @constant * @constant
* *
*/ */
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [ export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code", `${BASE_URL_API}validate/code`,
"/api/v1/custom_component", `${BASE_URL_API}custom_component`,
"/api/v1/validate/prompt", `${BASE_URL_API}validate/prompt`,
"http://localhost:7860/login", `http://localhost:7860/login`,
`${BASE_URL_API}api_key/store`,
]; ];
export const skipNodeUpdate = [ export const skipNodeUpdate = [
@ -561,10 +564,6 @@ export const CONTROL_NEW_USER = {
is_superuser: false, is_superuser: false,
}; };
export const CONTROL_NEW_API_KEY = {
apikeyname: "",
};
export const tabsCode = []; export const tabsCode = [];
export function tabsArray(codes: string[], method: number) { export function tabsArray(codes: string[], method: number) {
@ -649,8 +648,6 @@ export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
export const FETCH_ERROR_DESCRIPION = export const FETCH_ERROR_DESCRIPION =
"Check if everything is working properly and try again."; "Check if everything is working properly and try again.";
export const BASE_URL_API = "/api/v1/";
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. "; export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
export const API_PAGE_PARAGRAPH_1 = export const API_PAGE_PARAGRAPH_1 =
@ -666,3 +663,17 @@ export const LAST_USED_SPAN_1 = "The last time this key was used.";
export const LAST_USED_SPAN_2 = export const LAST_USED_SPAN_2 =
"Accurate to within the hour from the most recent usage."; "Accurate to within the hour from the most recent usage.";
export const LANGFLOW_SUPPORTED_TYPES = new Set([
"str",
"bool",
"float",
"code",
"prompt",
"file",
"int",
"dict",
"NestedDict",
]);
export const priorityFields = new Set(["code", "template"]);

View file

@ -26,6 +26,8 @@ const initialValue: alertContextType = {
pushNotificationList: () => {}, pushNotificationList: () => {},
clearNotificationList: () => {}, clearNotificationList: () => {},
removeFromNotificationList: () => {}, removeFromNotificationList: () => {},
modalContextOpen: false,
setModalContextOpen: (open: boolean) => {},
}; };
export const alertContext = createContext<alertContextType>(initialValue); export const alertContext = createContext<alertContextType>(initialValue);
@ -49,6 +51,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
const [notificationCenter, setNotificationCenter] = useState(false); const [notificationCenter, setNotificationCenter] = useState(false);
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]); const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
const [isTweakPage, setIsTweakPage] = useState<boolean>(false); const [isTweakPage, setIsTweakPage] = useState<boolean>(false);
const [modalContextOpen, setModalContextOpen] = useState<boolean>(false);
const pushNotificationList = (notification: AlertItemType) => { const pushNotificationList = (notification: AlertItemType) => {
setNotificationList((old) => { setNotificationList((old) => {
let newNotificationList = _.cloneDeep(old); let newNotificationList = _.cloneDeep(old);
@ -141,6 +144,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
setSuccessData, setSuccessData,
successOpen, successOpen,
setSuccessOpen, setSuccessOpen,
modalContextOpen,
setModalContextOpen,
}} }}
> >
{children} {children}

View file

@ -19,6 +19,9 @@ const initialValue: AuthContextType = {
authenticationErrorCount: 0, authenticationErrorCount: 0,
autoLogin: false, autoLogin: false,
setAutoLogin: () => {}, setAutoLogin: () => {},
setApiKey: () => {},
apiKey: null,
storeApiKey: () => {},
}; };
export const AuthContext = createContext<AuthContextType>(initialValue); export const AuthContext = createContext<AuthContextType>(initialValue);
@ -36,6 +39,10 @@ export function AuthProvider({ children }): React.ReactElement {
const [userData, setUserData] = useState<Users | null>(null); const [userData, setUserData] = useState<Users | null>(null);
const [autoLogin, setAutoLogin] = useState<boolean>(false); const [autoLogin, setAutoLogin] = useState<boolean>(false);
const { setLoading } = useContext(alertContext); const { setLoading } = useContext(alertContext);
const [apiKey, setApiKey] = useState<string | null>(
cookies.get("apikey_tkn_lflw")
);
useEffect(() => { useEffect(() => {
const storedAccessToken = cookies.get("access_tkn_lflw"); const storedAccessToken = cookies.get("access_tkn_lflw");
if (storedAccessToken) { if (storedAccessToken) {
@ -43,6 +50,13 @@ export function AuthProvider({ children }): React.ReactElement {
} }
}, []); }, []);
useEffect(() => {
const apiKey = cookies.get("apikey_tkn_lflw");
if (apiKey) {
setApiKey(apiKey);
}
}, []);
useEffect(() => { useEffect(() => {
const isLoginPage = location.pathname.includes("login"); const isLoginPage = location.pathname.includes("login");
@ -74,7 +88,7 @@ export function AuthProvider({ children }): React.ReactElement {
setLoading(false); setLoading(false);
} }
}); });
}, []); }, [setUserData, setLoading, autoLogin, setIsAdmin]);
function getAuthentication() { function getAuthentication() {
const storedRefreshToken = cookies.get("refresh_tkn_lflw"); const storedRefreshToken = cookies.get("refresh_tkn_lflw");
@ -94,6 +108,7 @@ export function AuthProvider({ children }): React.ReactElement {
function logout() { function logout() {
cookies.remove("access_tkn_lflw", { path: "/" }); cookies.remove("access_tkn_lflw", { path: "/" });
cookies.remove("refresh_tkn_lflw", { path: "/" }); cookies.remove("refresh_tkn_lflw", { path: "/" });
cookies.remove("apikey_tkn_lflw", { path: "/" });
setIsAdmin(false); setIsAdmin(false);
setUserData(null); setUserData(null);
setAccessToken(null); setAccessToken(null);
@ -101,6 +116,11 @@ export function AuthProvider({ children }): React.ReactElement {
setIsAuthenticated(false); setIsAuthenticated(false);
} }
function storeApiKey(apikey: string) {
cookies.set("apikey_tkn_lflw", apikey, { path: "/" });
setApiKey(apikey);
}
return ( return (
// !! to convert string to boolean // !! to convert string to boolean
<AuthContext.Provider <AuthContext.Provider
@ -118,6 +138,9 @@ export function AuthProvider({ children }): React.ReactElement {
authenticationErrorCount: 0, authenticationErrorCount: 0,
setAutoLogin, setAutoLogin,
autoLogin, autoLogin,
setApiKey,
apiKey,
storeApiKey,
}} }}
> >
{children} {children}

View file

@ -1,5 +1,5 @@
import { AxiosError } from "axios"; import { AxiosError } from "axios";
import _ from "lodash"; import _, { cloneDeep } from "lodash";
import { import {
ReactNode, ReactNode,
createContext, createContext,
@ -8,12 +8,18 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow"; import {
Edge,
Node,
ReactFlowJsonObject,
XYPosition,
addEdge,
} from "reactflow";
import ShortUniqueId from "short-unique-id"; import ShortUniqueId from "short-unique-id";
import { skipNodeUpdate } from "../constants/constants";
import { import {
deleteFlowFromDatabase, deleteFlowFromDatabase,
downloadFlowsFromDatabase, downloadFlowsFromDatabase,
getVersion,
readFlowsFromDatabase, readFlowsFromDatabase,
saveFlowToDatabase, saveFlowToDatabase,
updateFlowInDatabase, updateFlowInDatabase,
@ -21,27 +27,47 @@ import {
} from "../controllers/API"; } from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api"; import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components"; import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow"; import {
import { TabsContextType, TabsState } from "../types/tabs"; FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { FlowsContextType, FlowsState } from "../types/tabs";
import { import {
addVersionToDuplicates, addVersionToDuplicates,
checkOldEdgesHandles,
createFlowComponent,
removeFileNameFromComponents,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds, updateIds,
updateTemplate, updateTemplate,
} from "../utils/reactflowUtils"; } from "../utils/reactflowUtils";
import { getRandomDescription, getRandomName } from "../utils/utils"; import {
createRandomKey,
getRandomDescription,
getRandomName,
} from "../utils/utils";
import { alertContext } from "./alertContext"; import { alertContext } from "./alertContext";
import { AuthContext } from "./authContext"; import { AuthContext } from "./authContext";
import { typesContext } from "./typesContext"; import { typesContext } from "./typesContext";
const uid = new ShortUniqueId({ length: 5 }); const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = { const FlowsContextInitialValue: FlowsContextType = {
tabId: "", tabId: "",
setTabId: (index: string) => {}, setTabId: (index: string) => {},
isLoading: true, isLoading: true,
flows: [], flows: [],
removeFlow: (id: string) => {}, removeFlow: (id: string) => {},
addFlow: async (flowData?: any) => "", addFlow: async (
newProject: boolean,
flowData?: FlowType,
override?: boolean
) => "",
updateFlow: (newFlow: FlowType) => {}, updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(), incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {}, downloadFlow: (flow: FlowType) => {},
@ -55,7 +81,8 @@ const TabsContextInitialValue: TabsContextType = {
lastCopiedSelection: null, lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {}, setLastCopiedSelection: (selection: any) => {},
tabsState: {}, tabsState: {},
setTabsState: (state: TabsState) => {}, setTabsState: (state: FlowsState) => {},
saveCurrentFlow: () => {},
getNodeId: (nodeType: string) => "", getNodeId: (nodeType: string) => "",
setTweak: (tweak: any) => {}, setTweak: (tweak: any) => {},
getTweak: [], getTweak: [],
@ -63,29 +90,35 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any }, selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
) => {}, ) => {},
saveComponent: async (component: NodeDataType, override: boolean) => "",
deleteComponent: (key: string) => {},
version: "",
nodesOnFlow: "",
setNodesOnFlow: (nodes: string) => "",
}; };
export const TabsContext = createContext<TabsContextType>( export const FlowsContext = createContext<FlowsContextType>(
TabsContextInitialValue FlowsContextInitialValue
); );
export function TabsProvider({ children }: { children: ReactNode }) { export function FlowsProvider({ children }: { children: ReactNode }) {
const { setErrorData, setNoticeData, setSuccessData } = const { setErrorData, setNoticeData, setSuccessData } =
useContext(alertContext); useContext(alertContext);
const { getAuthentication, isAuthenticated } = useContext(AuthContext); const { getAuthentication, isAuthenticated } = useContext(AuthContext);
const [tabId, setTabId] = useState(""); const [tabId, setTabId] = useState("");
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(false);
const [nodesOnFlow, setNodesOnFlow] = useState("");
const [flows, setFlows] = useState<Array<FlowType>>([]); const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uid()); const [id, setId] = useState(uid());
const { templates, reactFlowInstance } = useContext(typesContext); const { reactFlowInstance, setData, data } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState<{ const [lastCopiedSelection, setLastCopiedSelection] = useState<{
nodes: any; nodes: any;
edges: any; edges: any;
} | null>(null); } | null>(null);
const [tabsState, setTabsState] = useState<TabsState>({}); const [tabsState, setTabsState] = useState<FlowsState>({});
const [getTweak, setTweak] = useState<tweakType>([]); const [getTweak, setTweak] = useState<tweakType>([]);
useEffect(() => { useEffect(() => {
@ -103,9 +136,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function refreshFlows() { function refreshFlows() {
setIsLoading(true); setIsLoading(true);
getTabsDataFromDB().then((DbData) => { getTabsDataFromDB().then((DbData) => {
if (DbData && Object.keys(templates).length > 0) { if (DbData) {
try { try {
processDBData(DbData); processFlows(DbData, false);
updateStateWithDbData(DbData); updateStateWithDbData(DbData);
setIsLoading(false); setIsLoading(false);
} catch (e) {} } catch (e) {}
@ -117,31 +150,52 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks. // If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
if (getAuthentication() === true) { if (getAuthentication() === true) {
// get data from db // get data from db
//get tabs locally saved
// let tabsData = getLocalStorageTabsData();
refreshFlows(); refreshFlows();
} }
}, [templates, getAuthentication()]); }, [getAuthentication(), tabId]);
function getTabsDataFromDB() { function getTabsDataFromDB() {
//get tabs from db //get tabs from db
return readFlowsFromDatabase(); return readFlowsFromDatabase();
} }
function processDBData(DbData: FlowType[]) { function processFlows(DbData: FlowType[], skipUpdate = true) {
let savedComponents: { [key: string]: APIClassType } = {};
DbData.forEach((flow: FlowType) => { DbData.forEach((flow: FlowType) => {
try { try {
if (!flow.data) { if (!flow.data) {
return; return;
} }
processFlowEdges(flow); if (flow.data && flow.is_component) {
processFlowNodes(flow); (flow.data.nodes[0].data as NodeDataType).node!.display_name =
} catch (e) {} flow.name;
savedComponents[
createRandomKey(
(flow.data.nodes[0].data as NodeDataType).type,
uid()
)
] = _.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return;
}
if (!skipUpdate) processDataFromFlow(flow, false);
} catch (e) {
console.log(e);
}
});
setData((prev) => {
let newData = cloneDeep(prev);
newData["saved_components"] = cloneDeep(savedComponents);
return newData;
}); });
} }
function processFlowEdges(flow: FlowType) { function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return; if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => { flow.data.edges.forEach((edge) => {
edge.className = ""; edge.className = "";
edge.style = { stroke: "#555" }; edge.style = { stroke: "#555" };
@ -156,26 +210,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
node.data.node!.documentation = template["documentation"]; node.data.node!.documentation = template["documentation"];
} }
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
return;
}
if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template);
updateNodeBaseClasses(node, template);
updateNodeEdges(flow, node, template);
updateNodeDescription(node, template);
updateNodeTemplate(node, template);
updateNodeDocumentation(node, template);
}
});
}
function updateNodeBaseClasses(node: NodeType, template: APIClassType) { function updateNodeBaseClasses(node: NodeType, template: APIClassType) {
node.data.node!.base_classes = template["base_classes"]; node.data.node!.base_classes = template["base_classes"];
} }
@ -187,11 +221,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
) { ) {
flow.data!.edges.forEach((edge) => { flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) { if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle let sourceHandleObject: sourceHandleType = scapeJSONParse(
?.split("|") edge.sourceHandle!
.slice(0, 2) );
.concat(template["base_classes"]) sourceHandleObject.baseClasses = template["base_classes"];
.join("|"); edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
} }
}); });
} }
@ -227,9 +262,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
flowName: string, flowName: string,
flowDescription?: string flowDescription?: string
) { ) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
// create a data URI with the current flow data // create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify({ ...flow, name: flowName, description: flowDescription }) JSON.stringify({
...clonedFlow,
name: flowName,
description: flowDescription,
})
)}`; )}`;
// create a link element and set its properties // create a link element and set its properties
@ -243,9 +284,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download // simulate a click on the link element to trigger the download
link.click(); link.click();
setNoticeData({
title: "Warning: Critical data, JSON file may include API keys.",
});
} }
function downloadFlows() { function downloadFlows() {
@ -273,46 +311,70 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* If the file type is application/json, the file is read and parsed into a JSON object. * If the file type is application/json, the file is read and parsed into a JSON object.
* The resulting JSON object is passed to the addFlow function. * The resulting JSON object is passed to the addFlow function.
*/ */
async function uploadFlow( async function uploadFlow({
newProject?: boolean, newProject,
file?: File file,
): Promise<String | undefined> { isComponent = false,
position = { x: 10, y: 10 },
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
position?: XYPosition;
}): Promise<String | never> {
return new Promise(async (resolve, reject) => {
let id; let id;
if (file) { if (file) {
let text = await file.text(); let text = await file.text();
let fileData = JSON.parse(text); let fileData = JSON.parse(text);
if (
newProject &&
((!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent))
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
if (fileData.flows) { if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => { fileData.flows.forEach((flow: FlowType) => {
id = addFlow(flow, newProject); id = addFlow(newProject, flow, undefined, position);
}); });
resolve("");
} else {
id = await addFlow(newProject, fileData, undefined, position);
resolve(id);
}
} }
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
id = await addFlow(flow, newProject);
} else { } else {
// create a file input // create a file input
const input = document.createElement("input"); const input = document.createElement("input");
input.type = "file"; input.type = "file";
input.accept = ".json"; input.accept = ".json";
// add a change event listener to the file input // add a change event listener to the file input
id = await new Promise((resolve) => {
input.onchange = async (e: Event) => { input.onchange = async (e: Event) => {
if ( if (
(e.target as HTMLInputElement).files![0].type === "application/json" (e.target as HTMLInputElement).files![0].type === "application/json"
) { ) {
const currentfile = (e.target as HTMLInputElement).files![0]; const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text(); let text = await currentfile.text();
let flow: FlowType = JSON.parse(text); let fileData: FlowType = await JSON.parse(text);
const flowId = await addFlow(flow, newProject);
resolve(flowId); if (
(!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent)
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
id = await addFlow(newProject, fileData);
resolve(id);
}
} }
}; };
// trigger the file input click event to open the file dialog // trigger the file input click event to open the file dialog
input.click(); input.click();
});
} }
return id; });
} }
function uploadFlows() { function uploadFlows() {
@ -343,19 +405,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks. * Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove. * @param {string} id - The id of the flow to remove.
*/ */
function removeFlow(id: string) { async function removeFlow(id: string) {
const index = flows.findIndex((flow) => flow.id === id); const index = flows.findIndex((flow) => flow.id === id);
if (index >= 0) { if (index >= 0) {
deleteFlowFromDatabase(id).then(() => { await deleteFlowFromDatabase(id);
//removes component from data if there is any
setFlows(flows.filter((flow) => flow.id !== id)); setFlows(flows.filter((flow) => flow.id !== id));
}); processFlows(flows.filter((flow) => flow.id !== id));
} }
} }
/** /**
* Add a new flow to the list of flows. * Add a new flow to the list of flows.
* @param flow Optional flow to add. * @param flow Optional flow to add.
*/ */
function paste( function paste(
selectionInstance: { nodes: Node[]; edges: Edge[] }, selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number } position: { x: number; y: number; paneX?: number; paneY?: number }
@ -376,7 +438,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const insidePosition = position.paneX const insidePosition = position.paneX
? { x: position.paneX + position.x, y: position.paneY! + position.y } ? { x: position.paneX + position.x, y: position.paneY! + position.y }
: reactFlowInstance!.project({ x: position.x, y: position.y }); : reactFlowInstance!.screenToFlowPosition({
x: position.x,
y: position.y,
});
selectionInstance.nodes.forEach((node: NodeType) => { selectionInstance.nodes.forEach((node: NodeType) => {
// Generate a unique node ID // Generate a unique node ID
@ -404,19 +469,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}); });
reactFlowInstance!.setNodes(nodes); reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => { selectionInstance.edges.forEach((edge: Edge) => {
let source = idsMap[edge.source]; let source = idsMap[edge.source];
let target = idsMap[edge.target]; let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|"); const sourceHandleObject: sourceHandleType = scapeJSONParse(
let sourceHandle = edge.sourceHandle!
sourceHandleSplitted[0] + );
"|" + let sourceHandle = scapedJSONStringfy({
source + ...sourceHandleObject,
"|" + id: source,
sourceHandleSplitted.slice(2).join("|"); });
let targetHandleSplitted = edge.targetHandle!.split("|"); sourceHandleObject.id = source;
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target; edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id = let id =
"reactflow__edge-" + "reactflow__edge-" +
source + source +
@ -431,12 +505,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle, sourceHandle,
targetHandle, targetHandle,
id, id,
data: cloneDeep(edge.data),
style: { stroke: "#555" }, style: { stroke: "#555" },
className: className:
targetHandle.split("|")[0] === "Text" targetHandleObject.type === "Text"
? "stroke-gray-800 " ? "stroke-gray-800 "
: "stroke-gray-900 ", : "stroke-gray-900 ",
animated: targetHandle.split("|")[0] === "Text", animated: targetHandleObject.type === "Text",
selected: false, selected: false,
}, },
edges.map((edge) => ({ ...edge, selected: false })) edges.map((edge) => ({ ...edge, selected: false }))
@ -446,24 +521,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
const addFlow = async ( const addFlow = async (
newProject: Boolean,
flow?: FlowType, flow?: FlowType,
newProject?: Boolean override?: boolean,
position?: XYPosition
): Promise<String | undefined> => { ): Promise<String | undefined> => {
if (newProject) { if (newProject) {
let flowData = extractDataFromFlow(flow!); let flowData = flow
if (flowData.description == "") { ? processDataFromFlow(flow)
flowData.description = getRandomDescription(); : { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
}
// Create a new flow with a default name if no flow is provided. // Create a new flow with a default name if no flow is provided.
if (override) {
deleteComponent(flow!.name);
const newFlow = createNewFlow(flowData, flow!); const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow); const { id } = await saveFlowToDatabase(newFlow);
processFlowNodes(newFlow); newFlow.id = id;
//setTimeout to prevent update state with wrong state
setTimeout(() => {
addFlowToLocalState(newFlow);
}, 200);
// addFlowToLocalState(newFlow);
return;
}
const flowName = addVersionToDuplicates(newFlow, flows); const newFlow = createNewFlow(flowData, flow!);
newFlow.name = flowName; const newName = addVersionToDuplicates(newFlow, flows);
newFlow.name = newName;
try { try {
const { id } = await saveFlowToDatabase(newFlow); const { id } = await saveFlowToDatabase(newFlow);
// Change the id to the new id. // Change the id to the new id.
@ -481,76 +568,59 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} else { } else {
paste( paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges }, { nodes: flow!.data!.nodes, edges: flow!.data!.edges },
{ x: 10, y: 10 } position ?? { x: 10, y: 10 }
); );
} }
}; };
const extractDataFromFlow = (flow: FlowType) => { const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null; let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) { if (data) {
processFlowEdges(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges); updateEdges(data.edges);
updateNodes(data.nodes, data.edges); // updateNodes(data.nodes, data.edges);
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
} }
return { data, description }; return data;
}; };
const updateEdges = (edges: Edge[]) => { const updateEdges = (edges: Edge[]) => {
if (edges)
edges.forEach((edge) => { edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
edge.className = edge.className =
(edge.targetHandle!.split("|")[0] === "Text" (targetHandleObject.type === "Text"
? "stroke-gray-800 " ? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection"; : "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle!.split("|")[0] === "Text"; edge.animated = targetHandleObject.type === "Text";
});
};
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
return;
}
if (Object.keys(template["template"]).length > 0) {
node.data.node.base_classes = template["base_classes"];
edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge
.sourceHandle!.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
}
});
node.data.node.description = template["description"];
node.data.node.template = updateTemplate(
template["template"] as unknown as APITemplateType,
node.data.node.template as APITemplateType
);
}
}); });
}; };
const createNewFlow = ( const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string }, flowData: ReactFlowJsonObject | null,
flow: FlowType flow: FlowType
) => ({ ) => ({
description: flowData.description, description: flow?.description ?? getRandomDescription(),
name: flow?.name ?? getRandomName(), name: flow?.name ?? getRandomName(),
data: flowData.data, data: flowData,
id: "", id: "",
is_component: flow?.is_component ?? false,
}); });
const addFlowToLocalState = (newFlow: FlowType) => { const addFlowToLocalState = (newFlow: FlowType) => {
let newFlows: FlowType[] = [];
setFlows((prevState) => { setFlows((prevState) => {
newFlows = newFlows.concat(prevState);
newFlows.push(newFlow);
return [...prevState, newFlow]; return [...prevState, newFlow];
}); });
processFlows(newFlows);
}; };
/** /**
@ -566,10 +636,20 @@ export function TabsProvider({ children }: { children: ReactNode }) {
newFlows[index].data = newFlow.data; newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name; newFlows[index].name = newFlow.name;
} }
newFlow = {
...newFlow,
};
return newFlows; return newFlows;
}); });
} }
function saveCurrentFlow() {
const currentFlow = flows.find((flow) => flow.id === tabId);
if (currentFlow && reactFlowInstance && currentFlow.data) {
updateFlow({ ...currentFlow, data: reactFlowInstance?.toObject()! });
}
}
async function saveFlow(newFlow: FlowType, silent?: boolean) { async function saveFlow(newFlow: FlowType, silent?: boolean) {
try { try {
// updates flow in db // updates flow in db
@ -608,16 +688,41 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} }
} }
function saveComponent(component: NodeDataType, override: boolean) {
component.node!.official = false;
return addFlow(true, createFlowComponent(component, version), override);
}
function deleteComponent(key: string) {
let componentFlow = flows.find(
(componentFlow) =>
componentFlow.is_component && componentFlow.name === key
);
if (componentFlow) {
removeFlow(componentFlow.id);
}
}
const [isBuilt, setIsBuilt] = useState(false); const [isBuilt, setIsBuilt] = useState(false);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return ( return (
<TabsContext.Provider <FlowsContext.Provider
value={{ value={{
version,
saveFlow, saveFlow,
isBuilt, isBuilt,
setIsBuilt, setIsBuilt,
lastCopiedSelection, lastCopiedSelection,
setLastCopiedSelection, setLastCopiedSelection,
saveCurrentFlow,
hardReset, hardReset,
tabId, tabId,
setTabId, setTabId,
@ -637,9 +742,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
getTweak, getTweak,
setTweak, setTweak,
isLoading, isLoading,
saveComponent,
deleteComponent,
nodesOnFlow,
setNodesOnFlow,
}} }}
> >
{children} {children}
</TabsContext.Provider> </FlowsContext.Provider>
); );
} }

View file

@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext"; import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext"; import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext"; import { DarkProvider } from "./darkContext";
import { FlowsProvider } from "./flowsContext";
import { LocationProvider } from "./locationContext"; import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext"; import { StoreProvider } from "./storeContext";
import { TypesProvider } from "./typesContext"; import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext"; import { UndoRedoProvider } from "./undoRedoContext";
@ -26,9 +28,11 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<LocationProvider> <LocationProvider>
<ApiInterceptor /> <ApiInterceptor />
<SSEProvider> <SSEProvider>
<TabsProvider> <FlowsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider> <UndoRedoProvider>
</TabsProvider> <StoreProvider>{children}</StoreProvider>
</UndoRedoProvider>
</FlowsProvider>
</SSEProvider> </SSEProvider>
</LocationProvider> </LocationProvider>
</TypesProvider> </TypesProvider>

View file

@ -0,0 +1,74 @@
import { createContext, useContext, useEffect, useState } from "react";
import { checkHasApiKey, checkHasStore } from "../controllers/API";
import { storeContextType } from "../types/contexts/store";
import { AuthContext } from "./authContext";
//store context to share user components and update them
const initialValue = {
hasStore: true,
setHasStore: () => {},
validApiKey: false,
setValidApiKey: () => {},
hasApiKey: false,
setHasApiKey: () => {},
loadingApiKey: true,
};
export const StoreContext = createContext<storeContextType>(initialValue);
export function StoreProvider({ children }) {
const [hasStore, setHasStore] = useState(false);
const [loadingApiKey, setLoadingApiKey] = useState(true);
const [hasApiKey, setHasApiKey] = useState(true);
const [validApiKey, setValidApiKey] = useState(false);
const [storeChecked, setStoreChecked] = useState(false);
const { apiKey } = useContext(AuthContext);
useEffect(() => {
const fetchStoreData = async () => {
try {
if (storeChecked) return;
const res = await checkHasStore();
setHasStore(res?.enabled ?? false);
setStoreChecked(true);
} catch (e) {
console.log(e);
}
};
fetchStoreData();
}, []);
const fetchApiData = async () => {
setLoadingApiKey(true);
try {
const res = await checkHasApiKey();
setHasApiKey(res?.has_api_key ?? false);
setValidApiKey(res?.is_valid ?? false);
setLoadingApiKey(false);
} catch (e) {
setLoadingApiKey(false);
console.log(e);
}
};
useEffect(() => {
fetchApiData();
}, [storeChecked, apiKey]);
return (
<StoreContext.Provider
value={{
hasStore,
setHasStore,
hasApiKey,
setHasApiKey,
validApiKey,
setValidApiKey,
loadingApiKey,
}}
>
{children}
</StoreContext.Provider>
);
}

View file

@ -1,3 +1,4 @@
import _ from "lodash";
import { import {
createContext, createContext,
ReactNode, ReactNode,
@ -5,7 +6,7 @@ import {
useEffect, useEffect,
useState, useState,
} from "react"; } from "react";
import { Edge, Node, ReactFlowInstance } from "reactflow"; import { ReactFlowInstance } from "reactflow";
import { getAll, getHealth } from "../controllers/API"; import { getAll, getHealth } from "../controllers/API";
import { APIKindType } from "../types/api"; import { APIKindType } from "../types/api";
import { typesContextType } from "../types/typesContext"; import { typesContextType } from "../types/typesContext";
@ -59,11 +60,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Make sure to only update the state if the component is still mounted. // Make sure to only update the state if the component is still mounted.
if (isMounted && result?.status === 200) { if (isMounted && result?.status === 200) {
setLoading(false); setLoading(false);
setData(result.data); let { data } = _.cloneDeep(result);
setData((old) => ({ ...old, ...data }));
setTemplates( setTemplates(
Object.keys(result.data).reduce((acc, curr) => { Object.keys(data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => { Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c]; //prevent wrong overwriting of the component template by a group of the same type
if (!data[curr][c].flow) acc[c] = data[curr][c];
}); });
return acc; return acc;
}, {}) }, {})
@ -71,13 +74,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Set the types by reducing over the keys of the result data and updating the accumulator. // Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes( setTypes(
// Reverse the keys so the tool world does not overlap // Reverse the keys so the tool world does not overlap
Object.keys(result.data) Object.keys(data)
.reverse() .reverse()
.reduce((acc, curr) => { .reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => { Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = curr; acc[c] = curr;
// Add the base classes to the accumulator as well. // Add the base classes to the accumulator as well.
result.data[curr][c].base_classes?.forEach((b) => { data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr; acc[b] = curr;
}); });
}); });
@ -87,6 +90,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
} }
} catch (error) { } catch (error) {
console.error("An error has occurred while fetching types."); console.error("An error has occurred while fetching types.");
console.log(error);
await getHealth().catch((e) => { await getHealth().catch((e) => {
setFetchError(true); setFetchError(true);
}); });
@ -94,31 +98,25 @@ export function TypesProvider({ children }: { children: ReactNode }) {
} }
function deleteNode(idx: string | Array<string>) { function deleteNode(idx: string | Array<string>) {
reactFlowInstance!.setNodes( if (reactFlowInstance === null) return;
reactFlowInstance! const edges = reactFlowInstance!
.getNodes()
.filter((node: Node) =>
typeof idx === "string" ? node.id !== idx : !idx.includes(node.id)
)
);
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges() .getEdges()
.filter((edge) => .filter((edge) =>
typeof idx === "string" typeof idx === "string"
? edge.source !== idx && edge.target !== idx ? edge.source == idx || edge.target == idx
: !idx.includes(edge.source) && !idx.includes(edge.target) : idx.includes(edge.source) || idx.includes(edge.target)
)
); );
reactFlowInstance!.deleteElements({
nodes:
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
edges,
});
} }
function deleteEdge(idx: string | Array<string>) { function deleteEdge(idx: string | Array<string>) {
reactFlowInstance!.setEdges( reactFlowInstance!.deleteElements({
reactFlowInstance! edges:
.getEdges() typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
.filter((edge: Edge) => });
typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id)
)
);
} }
return ( return (

View file

@ -13,7 +13,7 @@ import {
undoRedoContextType, undoRedoContextType,
} from "../types/typesContext"; } from "../types/typesContext";
import { isWrappedWithClass } from "../utils/utils"; import { isWrappedWithClass } from "../utils/utils";
import { TabsContext } from "./tabsContext"; import { FlowsContext } from "./flowsContext";
const initialValue = { const initialValue = {
undo: () => {}, undo: () => {},
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
export const undoRedoContext = createContext<undoRedoContextType>(initialValue); export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
export function UndoRedoProvider({ children }) { export function UndoRedoProvider({ children }) {
const { tabId, flows } = useContext(TabsContext); const { tabId, flows } = useContext(FlowsContext);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => [])); const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => [])); const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
@ -52,15 +52,23 @@ export function UndoRedoProvider({ children }) {
const takeSnapshot = useCallback(() => { const takeSnapshot = useCallback(() => {
// push the current graph to the past state // push the current graph to the past state
setPast((old) => { let newPast = cloneDeep(past);
let newPast = cloneDeep(old); let newState = {
newPast[tabIndex] = old[tabIndex].slice( nodes: cloneDeep(getNodes()),
old[tabIndex].length - defaultOptions.maxHistorySize + 1, edges: cloneDeep(getEdges()),
old[tabIndex].length };
if (
past[tabIndex] &&
JSON.stringify(past[tabIndex][past[tabIndex].length - 1]) !==
JSON.stringify(newState)
) {
newPast[tabIndex] = past[tabIndex].slice(
past[tabIndex].length - defaultOptions.maxHistorySize + 1,
past[tabIndex].length
); );
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() }); newPast[tabIndex].push(newState);
return newPast; }
}); setPast(newPast);
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches // whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
setFuture((old) => { setFuture((old) => {

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api"; import { api } from "../../controllers/API/api";
import { import {
APIObjectType, APIObjectType,
Component,
LoginType, LoginType,
Users, Users,
changeUser, changeUser,
@ -12,6 +13,7 @@ import {
} from "../../types/api/index"; } from "../../types/api/index";
import { UserInputType } from "../../types/components"; import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow"; import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import { import {
APIClassType, APIClassType,
BuildStatusTypeAPI, BuildStatusTypeAPI,
@ -70,10 +72,10 @@ export async function postValidatePrompt(
template: string, template: string,
frontend_node: APIClassType frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> { ): Promise<AxiosResponse<PromptTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/prompt`, { return api.post(`${BASE_URL_API}validate/prompt`, {
name: name, name,
template: template, template,
frontend_node: frontend_node, frontend_node,
}); });
} }
@ -112,12 +114,14 @@ export async function saveFlowToDatabase(newFlow: {
data: ReactFlowJsonObject | null; data: ReactFlowJsonObject | null;
description: string; description: string;
style?: FlowStyleType; style?: FlowStyleType;
is_component?: boolean;
}): Promise<FlowType> { }): Promise<FlowType> {
try { try {
const response = await api.post(`${BASE_URL_API}flows/`, { const response = await api.post(`${BASE_URL_API}flows/`, {
name: newFlow.name, name: newFlow.name,
data: newFlow.data, data: newFlow.data,
description: newFlow.description, description: newFlow.description,
is_component: newFlow.is_component,
}); });
if (response.status !== 201) { if (response.status !== 201) {
@ -353,7 +357,21 @@ export async function postCustomComponent(
code: string, code: string,
apiClass: APIClassType apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> { ): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component`, { code }); // let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
code,
frontend_node: apiClass,
});
}
export async function postCustomComponentUpdate(
code: string,
field: string
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
field,
});
} }
export async function onLogin(user: LoginType) { export async function onLogin(user: LoginType) {
@ -522,3 +540,323 @@ export async function deleteApiKey(api_key: string) {
throw error; throw error;
} }
} }
export async function addApiKeyStore(key: string) {
try {
const res = await api.post(`${BASE_URL_API}api_key/store`, {
api_key: key,
});
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
/**
* Saves a new flow to the database.
*
* @param {FlowType} newFlow - The flow data to save.
* @returns {Promise<any>} The saved flow data.
* @throws Will throw an error if saving fails.
*/
export async function saveFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
status: publicFlow ? "Public" : "Private",
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches the flows from the store.
* @returns {Promise<>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function getFlowsStore(): Promise<AxiosResponse<FlowType[]>> {
return await api.get(`${BASE_URL_API}store/`);
}
export async function getStoreComponents({
component_id = null,
page = 1,
limit = 9999999,
is_component = null,
sort = "-count(liked_by)",
tags = [] || null,
liked = null,
isPrivate = null,
search = null,
filterByUser = null,
fields = null,
}: {
component_id?: string | null;
page?: number;
limit?: number;
is_component?: boolean | null;
sort?: string;
tags?: string[] | null;
liked?: boolean | null;
isPrivate?: boolean | null;
search?: string | null;
filterByUser?: boolean | null;
fields?: Array<string> | null;
}): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (component_id !== undefined && component_id !== null) {
queryParams.push(`component_id=${component_id}`);
}
if (search !== undefined && search !== null) {
queryParams.push(`search=${search}`);
}
if (isPrivate !== undefined && isPrivate !== null) {
queryParams.push(`private=${isPrivate}`);
}
if (tags !== undefined && tags !== null && tags.length > 0) {
queryParams.push(`tags=${tags.join(encodeURIComponent(","))}`);
}
if (fields !== undefined && fields !== null && fields.length > 0) {
queryParams.push(`fields=${fields.join(encodeURIComponent(","))}`);
}
if (sort !== undefined && sort !== null) {
queryParams.push(`sort=${sort}`);
} else {
queryParams.push(`sort=-count(liked_by)`); // default sort
}
if (liked !== undefined && liked !== null) {
queryParams.push(`liked=${liked}`);
}
if (filterByUser !== undefined && filterByUser !== null) {
queryParams.push(`filter_by_user=${filterByUser}`);
}
if (page !== undefined) {
queryParams.push(`page=${page ?? 1}`);
}
if (limit !== undefined) {
queryParams.push(`limit=${limit ?? 9999999}`);
}
if (is_component !== null && is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function postStoreComponents(component: Component) {
try {
const res = await api.post(`${BASE_URL_API}store/components/`, component);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function searchComponent(
query: string | null,
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (query !== undefined && query !== null) {
queryParams.push(`search=${query}`);
}
if (page !== undefined && page !== null) {
queryParams.push(`page=${page}`);
}
if (limit !== undefined && limit !== null) {
queryParams.push(`limit=${limit}`);
}
if (status !== undefined && status !== null) {
queryParams.push(`status=${status}`);
}
if (tags !== undefined && tags !== null) {
queryParams.push(`tags=${tags}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasApiKey() {
try {
const res = await api.get(`${BASE_URL_API}store/check/api_key`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasStore() {
try {
const res = await api.get(`${BASE_URL_API}store/check/`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getCountComponents(is_component?: boolean | null) {
try {
let url = `${BASE_URL_API}store/components/count`;
const queryParams: any = [];
if (is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getStoreTags() {
try {
const res = await api.get(`${BASE_URL_API}store/tags`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export const postLikeComponent = (componentId: string) => {
return api.post(`${BASE_URL_API}store/users/likes/${componentId}`);
};
/**
* Updates an existing flow in the Store.
*
* @param {FlowType} updatedFlow - The updated flow data.
* @returns {Promise<any>} The updated flow data.
* @throws Will throw an error if the update fails.
*/
export async function updateFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false,
id: string
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}

View file

@ -0,0 +1,31 @@
const SvgAWS = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
id="Layer_1"
x={0}
y={0}
style={{
enableBackground: "new 0 0 304 182",
}}
viewBox="0 0 304 182"
{...props}
>
<style>{".st1{fill-rule:evenodd;clip-rule:evenodd;fill:#f90}"}</style>
<path
d="M86.4 66.4c0 3.7.4 6.7 1.1 8.9.8 2.2 1.8 4.6 3.2 7.2.5.8.7 1.6.7 2.3 0 1-.6 2-1.9 3L83.2 92c-.9.6-1.8.9-2.6.9-1 0-2-.5-3-1.4-1.4-1.5-2.6-3.1-3.6-4.7-1-1.7-2-3.6-3.1-5.9-7.8 9.2-17.6 13.8-29.4 13.8-8.4 0-15.1-2.4-20-7.2-4.9-4.8-7.4-11.2-7.4-19.2 0-8.5 3-15.4 9.1-20.6 6.1-5.2 14.2-7.8 24.5-7.8 3.4 0 6.9.3 10.6.8 3.7.5 7.5 1.3 11.5 2.2v-7.3c0-7.6-1.6-12.9-4.7-16-3.2-3.1-8.6-4.6-16.3-4.6-3.5 0-7.1.4-10.8 1.3-3.7.9-7.3 2-10.8 3.4-1.6.7-2.8 1.1-3.5 1.3-.7.2-1.2.3-1.6.3-1.4 0-2.1-1-2.1-3.1v-4.9c0-1.6.2-2.8.7-3.5.5-.7 1.4-1.4 2.8-2.1 3.5-1.8 7.7-3.3 12.6-4.5C41 1.9 46.2 1.3 51.7 1.3c11.9 0 20.6 2.7 26.2 8.1 5.5 5.4 8.3 13.6 8.3 24.6v32.4zM45.8 81.6c3.3 0 6.7-.6 10.3-1.8 3.6-1.2 6.8-3.4 9.5-6.4 1.6-1.9 2.8-4 3.4-6.4.6-2.4 1-5.3 1-8.7v-4.2c-2.9-.7-6-1.3-9.2-1.7-3.2-.4-6.3-.6-9.4-.6-6.7 0-11.6 1.3-14.9 4-3.3 2.7-4.9 6.5-4.9 11.5 0 4.7 1.2 8.2 3.7 10.6 2.4 2.5 5.9 3.7 10.5 3.7zm80.3 10.8c-1.8 0-3-.3-3.8-1-.8-.6-1.5-2-2.1-3.9L96.7 10.2c-.6-2-.9-3.3-.9-4 0-1.6.8-2.5 2.4-2.5h9.8c1.9 0 3.2.3 3.9 1 .8.6 1.4 2 2 3.9l16.8 66.2 15.6-66.2c.5-2 1.1-3.3 1.9-3.9.8-.6 2.2-1 4-1h8c1.9 0 3.2.3 4 1 .8.6 1.5 2 1.9 3.9l15.8 67 17.3-67c.6-2 1.3-3.3 2-3.9.8-.6 2.1-1 3.9-1h9.3c1.6 0 2.5.8 2.5 2.5 0 .5-.1 1-.2 1.6-.1.6-.3 1.4-.7 2.5l-24.1 77.3c-.6 2-1.3 3.3-2.1 3.9-.8.6-2.1 1-3.8 1h-8.6c-1.9 0-3.2-.3-4-1-.8-.7-1.5-2-1.9-4L156 23l-15.4 64.4c-.5 2-1.1 3.3-1.9 4-.8.7-2.2 1-4 1h-8.6zm128.5 2.7c-5.2 0-10.4-.6-15.4-1.8-5-1.2-8.9-2.5-11.5-4-1.6-.9-2.7-1.9-3.1-2.8-.4-.9-.6-1.9-.6-2.8v-5.1c0-2.1.8-3.1 2.3-3.1.6 0 1.2.1 1.8.3.6.2 1.5.6 2.5 1 3.4 1.5 7.1 2.7 11 3.5 4 .8 7.9 1.2 11.9 1.2 6.3 0 11.2-1.1 14.6-3.3 3.4-2.2 5.2-5.4 5.2-9.5 0-2.8-.9-5.1-2.7-7-1.8-1.9-5.2-3.6-10.1-5.2L246 52c-7.3-2.3-12.7-5.7-16-10.2-3.3-4.4-5-9.3-5-14.5 0-4.2.9-7.9 2.7-11.1 1.8-3.2 4.2-6 7.2-8.2 3-2.3 6.4-4 10.4-5.2 4-1.2 8.2-1.7 12.6-1.7 2.2 0 4.5.1 6.7.4 2.3.3 4.4.7 6.5 1.1 2 .5 3.9 1 5.7 1.6 1.8.6 3.2 1.2 4.2 1.8 1.4.8 2.4 1.6 3 2.5.6.8.9 1.9.9 3.3v4.7c0 2.1-.8 3.2-2.3 3.2-.8 0-2.1-.4-3.8-1.2-5.7-2.6-12.1-3.9-19.2-3.9-5.7 0-10.2.9-13.3 2.8-3.1 1.9-4.7 4.8-4.7 8.9 0 2.8 1 5.2 3 7.1 2 1.9 5.7 3.8 11 5.5l14.2 4.5c7.2 2.3 12.4 5.5 15.5 9.6 3.1 4.1 4.6 8.8 4.6 14 0 4.3-.9 8.2-2.6 11.6-1.8 3.4-4.2 6.4-7.3 8.8-3.1 2.5-6.8 4.3-11.1 5.6-4.5 1.4-9.2 2.1-14.3 2.1z"
style={{
fill: "#252f3e",
}}
/>
<path
d="M273.5 143.7c-32.9 24.3-80.7 37.2-121.8 37.2-57.6 0-109.5-21.3-148.7-56.7-3.1-2.8-.3-6.6 3.4-4.4 42.4 24.6 94.7 39.5 148.8 39.5 36.5 0 76.6-7.6 113.5-23.2 5.5-2.5 10.2 3.6 4.8 7.6z"
className="st1"
/>
<path
d="M287.2 128.1c-4.2-5.4-27.8-2.6-38.5-1.3-3.2.4-3.7-2.4-.8-4.5 18.8-13.2 49.7-9.4 53.3-5 3.6 4.5-1 35.4-18.6 50.2-2.7 2.3-5.3 1.1-4.1-1.9 4-9.9 12.9-32.2 8.7-37.5z"
className="st1"
/>
</svg>
);
export default SvgAWS;

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
<style type="text/css">
.st0{fill:#252F3E;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
</style>
<g>
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2
c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8
c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8
c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4
c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1
c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6
c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7
c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6
C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4
c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1
h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5
c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1
c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8
c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3
c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5
c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1
c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1
c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2
c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1
c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6
c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
<g>
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4
c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5
c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,8 @@
import React, { forwardRef } from "react";
import SvgAWS from "./AWS";
export const AWSIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgAWS ref={ref} {...props} />;
}
);

View file

@ -1,7 +1,8 @@
import { InfinityIcon } from "lucide-react"; import { Code } from "lucide-react";
import { forwardRef } from "react"; import { forwardRef } from "react";
import ForwardedIconComponent from "../../components/genericIconComponent";
export const GradientSparkles = forwardRef< export const GradientInfinity = forwardRef<
SVGSVGElement, SVGSVGElement,
React.PropsWithChildren<{}> React.PropsWithChildren<{}>
>((props, ref) => { >((props, ref) => {
@ -15,7 +16,63 @@ export const GradientSparkles = forwardRef<
</linearGradient> </linearGradient>
</defs> </defs>
</svg> </svg>
<InfinityIcon stroke="url(#grad1)" ref={ref} {...props} /> <Code stroke="url(#grad1)" ref={ref} {...props} />
</> </>
); );
}); });
export const GradientSave = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent
name="Save"
stroke="url(#grad2)"
ref={ref}
{...props}
/>
</>
);
});
export const GradientGroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
</>
);
};
export const GradientUngroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad4" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Ungroup" stroke="url(#grad4)" {...props} />
</>
);
};

View file

@ -0,0 +1,9 @@
const SvgShare = (props) => (
<svg width="1em" height="1em" {...props} xmlns="http://www.w3.org/2000/svg">
<path
d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z"
fill="currentColor"
></path>
</svg>
);
export default SvgShare;

View file

@ -0,0 +1,8 @@
import React, { forwardRef } from "react";
import SvgShare from "./Share";
export const ShareIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgShare ref={ref} {...props} />;
}
);

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,17 @@
const SvgShare2 = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="1em"
height="1em"
viewBox="0 0 485.213 485.212"
fill="currentColor"
{...props}
>
<g>
<path d="M394.236,212.282L272.934,333.584V272.93c0,0-121.304-30.324-181.955,60.654c0-100.483,81.469-181.956,181.955-181.956 V90.978L394.236,212.282z M485.212,242.606c0,133.976-108.627,242.606-242.604,242.606c-133.994,0-242.606-108.631-242.606-242.606 C0.001,108.628,108.613,0,242.607,0C376.585,0,485.212,108.628,485.212,242.606z M454.89,242.606 c0-117.038-95.241-212.279-212.282-212.279c-117.055,0-212.28,95.241-212.28,212.279c0,117.039,95.225,212.28,212.28,212.28 C359.648,454.886,454.89,359.645,454.89,242.606z" />
</g>
</svg>
);
export default SvgShare2;

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgShare2 from "./Share2";
export const Share2Icon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgShare2 ref={ref} {...props} />;
});

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -13,9 +13,12 @@ import {
// import "ace-builds/webpack-resolver"; // import "ace-builds/webpack-resolver";
import CodeTabsComponent from "../../components/codeTabsComponent"; import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants"; import {
EXPORT_CODE_DIALOG,
LANGFLOW_SUPPORTED_TYPES,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api"; import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components"; import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index"; import { FlowType, NodeType } from "../../types/flow/index";
@ -45,7 +48,7 @@ const ApiModal = forwardRef(
const [activeTab, setActiveTab] = useState("0"); const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]); const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]); const tweaksList = useRef<string[]>([]);
const { setTweak, getTweak, tabsState } = useContext(TabsContext); const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
const pythonApiCode = getPythonApiCode( const pythonApiCode = getPythonApiCode(
flow, flow,
autoLogin, autoLogin,
@ -99,15 +102,9 @@ const ApiModal = forwardRef(
(templateField) => (templateField) =>
templateField.charAt(0) !== "_" && templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show && node.data.node.template[templateField].show &&
(node.data.node.template[templateField].type === "str" || LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type === "bool" || node.data.node.template[templateField].type
node.data.node.template[templateField].type === "float" || )
node.data.node.template[templateField].type === "code" ||
node.data.node.template[templateField].type === "prompt" ||
node.data.node.template[templateField].type === "file" ||
node.data.node.template[templateField].type === "int" ||
node.data.node.template[templateField].type === "dict" ||
node.data.node.template[templateField].type === "NestedDict")
) )
.map((n, i) => { .map((n, i) => {
arrNodesWithValues.push(node["id"]); arrNodesWithValues.push(node["id"]);
@ -146,9 +143,9 @@ const ApiModal = forwardRef(
); );
if (existingTweak) { if (existingTweak) {
existingTweak[tw][template["name"]] = changes as string; existingTweak[tw][template["name"]!] = changes as string;
if (existingTweak[tw][template["name"]] == template.value) { if (existingTweak[tw][template["name"]!] == template.value) {
tweak.current.forEach((element) => { tweak.current.forEach((element) => {
if (element[tw] && Object.keys(element[tw])?.length === 0) { if (element[tw] && Object.keys(element[tw])?.length === 0) {
tweak.current = tweak.current.filter((obj) => { tweak.current = tweak.current.filter((obj) => {
@ -161,7 +158,7 @@ const ApiModal = forwardRef(
} else { } else {
const newTweak = { const newTweak = {
[tw]: { [tw]: {
[template["name"]]: changes, [template["name"]!]: changes,
}, },
} as uniqueTweakType; } as uniqueTweakType;
tweak.current.push(newTweak); tweak.current.push(newTweak);

View file

@ -1,30 +1,65 @@
import { useState } from "react"; import React, { useEffect, useState } from "react";
import ShadTooltip from "../../components/ShadTooltipComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { ConfirmationModalType } from "../../types/components"; import { ConfirmationModalType, ContentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils"; import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
export default function ConfirmationModal({ const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>;
};
const Trigger: React.FC<ContentProps> = ({
children,
tolltipContent,
side,
}) => {
return tolltipContent ? (
<ShadTooltip side={side} content={tolltipContent}>
<div className="h-full w-full">{children}</div>
</ShadTooltip>
) : (
<div className="h-full w-full">{children}</div>
);
};
function ConfirmationModal({
title, title,
asChild, asChild,
titleHeader, titleHeader,
modalContent,
modalContentTitle, modalContentTitle,
cancelText, cancelText,
confirmationText, confirmationText,
children, children,
destructive = false,
icon, icon,
data, data,
index, index,
onConfirm, onConfirm,
size,
open,
onClose,
onCancel,
}: ConfirmationModalType) { }: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon]; const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false);
useEffect(() => {
if (open) setModalOpen(open);
}, [open]);
useEffect(() => {
if (onClose) onClose!(modalOpen);
}, [modalOpen]);
const triggerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Trigger
);
const ContentChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Content
);
const [open, setOpen] = useState(false);
return ( return (
<BaseModal size="x-small" open={open} setOpen={setOpen}> <BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger> <BaseModal.Trigger asChild={asChild}>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}> <BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span> <span className="pr-2">{title}</span>
<Icon <Icon
name="icon" name="icon"
@ -33,20 +68,21 @@ export default function ConfirmationModal({
/> />
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
{modalContentTitle != "" && ( {modalContentTitle && modalContentTitle != "" && (
<> <>
<strong>{modalContentTitle}</strong> <strong>{modalContentTitle}</strong>
<br></br> <br></br>
</> </>
)} )}
<span>{modalContent}</span> {ContentChild}
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>
<Button <Button
className="ml-3" className="ml-3"
variant={destructive ? "destructive" : "default"}
onClick={() => { onClick={() => {
setOpen(false); setModalOpen(false);
onConfirm(index, data); onConfirm(index, data);
}} }}
> >
@ -54,9 +90,11 @@ export default function ConfirmationModal({
</Button> </Button>
<Button <Button
className=""
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setOpen(false); if (onCancel) onCancel();
setModalOpen(false);
}} }}
> >
{cancelText} {cancelText}
@ -65,3 +103,7 @@ export default function ConfirmationModal({
</BaseModal> </BaseModal>
); );
} }
ConfirmationModal.Content = Content;
ConfirmationModal.Trigger = Trigger;
export default ConfirmationModal;

View file

@ -0,0 +1,62 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { Trash2 } from "lucide-react";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
export default function DeleteConfirmationModal({
children,
onConfirm,
description,
}: {
children: JSX.Element;
onConfirm: () => void;
description?: string;
}) {
return (
<Dialog>
<DialogTrigger tabIndex={-1}>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex items-center">
<span className="pr-2">Delete</span>
<Trash2
className="h-6 w-6 pl-1 text-foreground"
strokeWidth={1.5}
/>
</div>
</DialogTitle>
</DialogHeader>
<span>
Are you sure you want to delete this {description ?? "component"}?
<br></br>
This action cannot be undone.
</span>
<DialogFooter>
<DialogClose>
<Button className="mr-3" variant="outline">
Cancel
</Button>
<Button
type="submit"
variant="destructive"
onClick={() => {
onConfirm();
}}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -1,12 +1,7 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { import { forwardRef, useContext, useEffect, useState } from "react";
ReactNode, import { useUpdateNodeInternals } from "reactflow";
forwardRef, import ShadTooltip from "../../components/ShadTooltipComponent";
useContext,
useEffect,
useRef,
useState,
} from "react";
import CodeAreaComponent from "../../components/codeAreaComponent"; import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent"; import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent"; import Dropdown from "../../components/dropdownComponent";
@ -30,15 +25,20 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "../../components/ui/table"; } from "../../components/ui/table";
import { limitScrollFieldsModal } from "../../constants/constants"; import {
import { TabsContext } from "../../contexts/tabsContext"; LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext"; import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow"; import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs"; import { FlowsState } from "../../types/tabs";
import { import {
convertObjToArray, convertObjToArray,
convertValuesToNumbers, convertValuesToNumbers,
hasDuplicateKeys, hasDuplicateKeys,
scapedJSONStringfy,
} from "../../utils/reactflowUtils"; } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils"; import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -47,66 +47,68 @@ const EditNodeModal = forwardRef(
( (
{ {
data, data,
setData,
nodeLength, nodeLength,
children,
open, open,
onClose, setOpen,
}: { }: {
data: NodeDataType; data: NodeDataType;
setData: (data: NodeDataType) => void;
nodeLength: number; nodeLength: number;
children: ReactNode; open: boolean;
open?: boolean; setOpen: (open: boolean) => void;
onClose?: (close: boolean) => void;
}, },
ref ref
) => { ) => {
const [modalOpen, setModalOpen] = useState(open ?? false); const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data); const [myData, setMyData] = useState(data);
const { setTabsState, tabId } = useContext(TabsContext); const { setTabsState, tabId } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext); const { reactFlowInstance } = useContext(typesContext);
let disabled = const { setModalContextOpen } = useContext(alertContext);
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === data.id) ?? false;
function changeAdvanced(n) { function changeAdvanced(n) {
myData.current.node!.template[n].advanced = setMyData((old) => {
!myData.current.node!.template[n].advanced; let newData = cloneDeep(old);
setAdv(!adv); newData.node!.template[n].advanced =
!newData.node!.template[n].advanced;
return newData;
});
} }
const handleOnNewValue = (newValue: any, name) => { const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue; setMyData((old) => {
setDataValue(newValue); let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
updateNodeInternals(data.id);
}; };
useEffect(() => { useEffect(() => {
myData.current = data; // reset data to what it is on node when opening modal if (open) {
onClose!(modalOpen); setMyData(data); // reset data to what it is on node when opening modal
}, [modalOpen]); }
setModalContextOpen(open);
}, [open]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false); const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [adv, setAdv] = useState<boolean | null>(null);
const [dataValue, setDataValue] = useState(data);
return ( return (
<BaseModal <BaseModal
key={data.id} key={data.id}
size="large-h-full" size="large-h-full"
open={modalOpen} open={open}
setOpen={setModalOpen} setOpen={setOpen}
onChangeOpenModal={(open) => { onChangeOpenModal={(open) => {
myData.current = data; setMyData(data);
}} }}
> >
<BaseModal.Trigger>{children}</BaseModal.Trigger> <BaseModal.Trigger>
<BaseModal.Header description={myData.current.node?.description!}> <></>
<span className="pr-2">{myData.current.type}</span> </BaseModal.Trigger>
<Badge variant="secondary">ID: {myData.current.id}</Badge> <BaseModal.Header description={myData.node?.description!}>
<span className="pr-2">{myData.type}</span>
<Badge variant="secondary">ID: {myData.id}</Badge>
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<div className="flex pb-2"> <div className="flex pb-2">
@ -123,7 +125,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box", "edit-node-modal-box",
nodeLength > limitScrollFieldsModal nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll" ? "overflow-scroll overflow-x-hidden custom-scroll"
: "overflow-hidden" : ""
)} )}
> >
{nodeLength > 0 && ( {nodeLength > 0 && (
@ -139,152 +141,178 @@ const EditNodeModal = forwardRef(
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody className="p-0"> <TableBody className="p-0">
{Object.keys(myData.current.node!.template) {Object.keys(myData.node!.template)
.filter( .filter(
(templateParam) => (templateParam) =>
templateParam.charAt(0) !== "_" && templateParam.charAt(0) !== "_" &&
myData.current.node?.template[templateParam].show && myData.node?.template[templateParam].show &&
(myData.current.node.template[templateParam] LANGFLOW_SUPPORTED_TYPES.has(
.type === "str" || myData.node.template[templateParam].type
myData.current.node.template[templateParam]
.type === "bool" ||
myData.current.node.template[templateParam]
.type === "float" ||
myData.current.node.template[templateParam]
.type === "code" ||
myData.current.node.template[templateParam]
.type === "prompt" ||
myData.current.node.template[templateParam]
.type === "file" ||
myData.current.node.template[templateParam]
.type === "int" ||
myData.current.node.template[templateParam]
.type === "dict" ||
myData.current.node.template[templateParam]
.type === "NestedDict")
) )
.map((templateParam, index) => ( )
.map((templateParam, index) => {
let id = {
inputTypes:
myData.node!.template[templateParam].input_types,
type: myData.node!.template[templateParam].type,
id: myData.id,
fieldName: templateParam,
};
let disabled =
reactFlowInstance?.getEdges().some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(
myData.node!.template[templateParam].proxy
? {
...id,
proxy:
myData.node?.template[templateParam]
.proxy,
}
: id
)
) ?? false;
return (
<TableRow key={index} className="h-10"> <TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3"> <TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
{myData.current.node?.template[templateParam].name <ShadTooltip
? myData.current.node.template[templateParam] content={
.name myData.node?.template[templateParam].proxy
: myData.current.node?.template[templateParam] ? myData.node?.template[templateParam]
.display_name} .proxy?.id
: null
}
>
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
</span>
</ShadTooltip>
</TableCell> </TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground "> <TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam] {myData.node?.template[templateParam].type ===
.type === "str" && "str" &&
!myData.current.node.template[templateParam] !myData.node.template[templateParam].options ? (
.options ? (
<div className="mx-auto"> <div className="mx-auto">
{myData.current.node.template[templateParam] {myData.node.template[templateParam]
.list ? ( .list ? (
<InputListComponent <InputListComponent
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
value={ value={
!myData.current.node.template[ !myData.node.template[templateParam]
templateParam .value ||
].value || myData.node.template[templateParam]
myData.current.node.template[ .value === ""
templateParam
].value === ""
? [""] ? [""]
: myData.current.node.template[ : myData.node.template[
templateParam templateParam
].value ].value
} }
onChange={(value: string[]) => { onChange={(value: string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(
value,
templateParam
);
}} }}
/> />
) : myData.current.node.template[ ) : myData.node.template[templateParam]
templateParam .multiline ? (
].multiline ? (
<TextAreaComponent <TextAreaComponent
id={"textarea-edit-" + index}
data-testid={"textarea-edit-" + index}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(
handleOnNewValue(value, templateParam); value: string | string[]
) => {
handleOnNewValue(
value,
templateParam
);
}} }}
/> />
) : ( ) : (
<InputComponent <InputComponent
id={"input-" + index}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
password={ password={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .password ?? false
].password ?? false
} }
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value) => { onChange={(value) => {
handleOnNewValue(value, templateParam); handleOnNewValue(
value,
templateParam
);
}} }}
/> />
)} )}
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "NestedDict" ? ( .type === "NestedDict" ? (
<div className=" w-full"> <div className=" w-full">
<DictComponent <DictComponent
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.current.node!.template[ myData.node!.template[
templateParam templateParam
].value.toString() === "{}" ]?.value?.toString() === "{}"
? { ? {
yourkey: "value", yourkey: "value",
} }
: myData.current.node!.template[ : myData.node!.template[templateParam]
templateParam .value
].value
} }
onChange={(newValue) => { onChange={(newValue) => {
myData.current.node!.template[ myData.node!.template[
templateParam templateParam
].value = newValue; ].value = newValue;
handleOnNewValue(newValue, templateParam); handleOnNewValue(
newValue,
templateParam
);
}} }}
id="editnode-div-dict-input"
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "dict" ? ( .type === "dict" ? (
<div <div
className={classNames( className={classNames(
"max-h-48 w-full overflow-auto custom-scroll", "max-h-48 w-full overflow-auto custom-scroll",
myData.current.node!.template[templateParam] myData.node!.template[templateParam].value
.value?.length > 1 ?.length > 1
? "my-3" ? "my-3"
: "" : ""
)} )}
> >
<KeypairListComponent <KeypairListComponent
dataValue={dataValue}
advanced={adv}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.current.node!.template[ myData.node!.template[templateParam]
templateParam .value?.length === 0 ||
].value?.length === 0 || !myData.node!.template[templateParam]
!myData.current.node!.template[ .value
templateParam
].value
? [{ "": "" }] ? [{ "": "" }]
: convertObjToArray( : convertObjToArray(
myData.current.node!.template[ myData.node!.template[
templateParam templateParam
].value ].value
) )
@ -293,7 +321,7 @@ const EditNodeModal = forwardRef(
onChange={(newValue) => { onChange={(newValue) => {
const valueToNumbers = const valueToNumbers =
convertValuesToNumbers(newValue); convertValuesToNumbers(newValue);
myData.current.node!.template[ myData.node!.template[
templateParam templateParam
].value = valueToNumbers; ].value = valueToNumbers;
setErrorDuplicateKey( setErrorDuplicateKey(
@ -306,16 +334,16 @@ const EditNodeModal = forwardRef(
}} }}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "bool" ? ( .type === "bool" ? (
<div className="ml-auto"> <div className="ml-auto">
{" "} {" "}
<ToggleShadComponent <ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled} disabled={disabled}
enabled={ enabled={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value
].value
} }
setEnabled={(isEnabled) => { setEnabled={(isEnabled) => {
handleOnNewValue( handleOnNewValue(
@ -326,84 +354,79 @@ const EditNodeModal = forwardRef(
size="small" size="small"
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "float" ? ( .type === "float" ? (
<div className="mx-auto"> <div className="mx-auto">
<FloatComponent <FloatComponent
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
rangeSpec={
myData.node!.template[templateParam]
.rangeSpec
}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value) => { onChange={(value) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "str" && .type === "str" &&
myData.current.node.template[templateParam] myData.node.template[templateParam]
.options ? ( .options ? (
<div className="mx-auto"> <div className="mx-auto">
<Dropdown <Dropdown
numberOfOptions={nodeLength} numberOfOptions={nodeLength}
editNode={true} editNode={true}
options={ options={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .options
].options
} }
onSelect={(value) => onSelect={(value) =>
handleOnNewValue(value, templateParam) handleOnNewValue(value, templateParam)
} }
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? "Choose an option"
].value ?? "Choose an option"
} }
id={"dropdown-edit-" + index}
></Dropdown> ></Dropdown>
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "int" ? ( .type === "int" ? (
<div className="mx-auto"> <div className="mx-auto">
<IntComponent <IntComponent
id={"edit-int-input-" + index}
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value) => { onChange={(value) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "file" ? ( .type === "file" ? (
<div className="mx-auto"> <div className="mx-auto">
<InputFileComponent <InputFileComponent
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
fileTypes={ fileTypes={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .fileTypes
].fileTypes
}
suffixes={
myData.current.node.template[
templateParam
].suffixes
} }
onFileChange={(filePath: string) => { onFileChange={(filePath: string) => {
data.node!.template[ data.node!.template[
@ -412,31 +435,44 @@ const EditNodeModal = forwardRef(
}} }}
></InputFileComponent> ></InputFileComponent>
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "prompt" ? ( .type === "prompt" ? (
<div className="mx-auto"> <div className="mx-auto">
<PromptAreaComponent <PromptAreaComponent
readonly={
myData.node?.flow ? true : false
}
field_name={templateParam} field_name={templateParam}
editNode={true} editNode={true}
disabled={disabled} disabled={disabled}
nodeClass={myData.current.node} nodeClass={myData.node}
setNodeClass={(nodeClass) => { setNodeClass={(nodeClass) => {
myData.current.node = nodeClass; myData.node = nodeClass;
}} }}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
id={"prompt-area-edit" + index}
data-testid={
"modal-prompt-input-" + index
}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "code" ? ( .type === "code" ? (
<div className="mx-auto"> <div className="mx-auto">
<CodeAreaComponent <CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node.template[templateParam]
.dynamic
? true
: false
}
dynamic={ dynamic={
data.node!.template[templateParam] data.node!.template[templateParam]
.dynamic ?? false .dynamic ?? false
@ -448,16 +484,16 @@ const EditNodeModal = forwardRef(
disabled={disabled} disabled={disabled}
editNode={true} editNode={true}
value={ value={
myData.current.node.template[ myData.node.template[templateParam]
templateParam .value ?? ""
].value ?? ""
} }
onChange={(value: string | string[]) => { onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam); handleOnNewValue(value, templateParam);
}} }}
id={"code-area-edit" + index}
/> />
</div> </div>
) : myData.current.node?.template[templateParam] ) : myData.node?.template[templateParam]
.type === "Any" ? ( .type === "Any" ? (
"-" "-"
) : ( ) : (
@ -467,10 +503,13 @@ const EditNodeModal = forwardRef(
<TableCell className="p-0 text-right"> <TableCell className="p-0 text-right">
<div className="items-center text-center"> <div className="items-center text-center">
<ToggleShadComponent <ToggleShadComponent
id={
"show" +
myData.node?.template[templateParam].name
}
enabled={ enabled={
!myData.current.node?.template[ !myData.node?.template[templateParam]
templateParam .advanced
].advanced
} }
setEnabled={(e) => { setEnabled={(e) => {
changeAdvanced(templateParam); changeAdvanced(templateParam);
@ -481,7 +520,8 @@ const EditNodeModal = forwardRef(
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} );
})}
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
@ -492,12 +532,12 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer> <BaseModal.Footer>
<Button <Button
id={"saveChangesBtn"}
className="mt-3" className="mt-3"
onClick={() => { onClick={() => {
const newData = cloneDeep(myData.current); data.node = myData.node;
myData.current = newData;
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { setTabsState((prev: FlowsState) => {
return { return {
...prev, ...prev,
[tabId]: { [tabId]: {
@ -506,7 +546,7 @@ const EditNodeModal = forwardRef(
}, },
}; };
}); });
setModalOpen(false); setOpen(false);
}} }}
type="submit" type="submit"
> >

View file

@ -3,14 +3,9 @@ import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input"; import { Input } from "../../components/ui/input";
import { CONTROL_NEW_API_KEY } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { createApiKey } from "../../controllers/API"; import { createApiKey } from "../../controllers/API";
import { import { ApiKeyType } from "../../types/components";
ApiKeyInputType,
ApiKeyType,
inputHandlerEventType,
} from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils"; import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
@ -27,19 +22,11 @@ export default function SecretKeyModal({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? ""); const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
const [apiKeyValue, setApiKeyValue] = useState(""); const [apiKeyValue, setApiKeyValue] = useState("");
const [inputState, setInputState] =
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
const [renderKey, setRenderKey] = useState(false); const [renderKey, setRenderKey] = useState(false);
const [textCopied, setTextCopied] = useState(true); const [textCopied, setTextCopied] = useState(true);
const { setSuccessData } = useContext(alertContext); const { setSuccessData } = useContext(alertContext);
const inputRef = useRef<HTMLInputElement | null>(null); const inputRef = useRef<HTMLInputElement | null>(null);
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
useEffect(() => { useEffect(() => {
if (open) { if (open) {
setRenderKey(false); setRenderKey(false);
@ -101,14 +88,7 @@ export default function SecretKeyModal({
</span> </span>
<div className="flex pt-3"> <div className="flex pt-3">
<div className="w-full"> <div className="w-full">
<Input <Input ref={inputRef} readOnly={true} value={apiKeyValue} />
ref={inputRef}
onChange={(event) => {
setApiKeyValue(event.target.value);
}}
readOnly={true}
value={apiKeyValue}
/>
</div> </div>
<div> <div>
@ -153,7 +133,6 @@ export default function SecretKeyModal({
<Form.Control asChild> <Form.Control asChild>
<input <input
onChange={({ target: { value } }) => { onChange={({ target: { value } }) => {
handleInput({ target: { name: "apikeyname", value } });
setApiKeyName(value); setApiKeyName(value);
}} }}
value={apiKeyName} value={apiKeyName}

View file

@ -0,0 +1,124 @@
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { StoreContext } from "../../contexts/storeContext";
import { addApiKeyStore } from "../../controllers/API";
import { StoreApiKeyType } from "../../types/components";
import BaseModal from "../baseModal";
export default function StoreApiKeyModal({
children,
disabled = false,
}: StoreApiKeyType) {
if (disabled) return <>{children}</>;
const [open, setOpen] = useState(false);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { storeApiKey } = useContext(AuthContext);
const { hasApiKey, validApiKey } = useContext(StoreContext);
const [apiKeyValue, setApiKeyValue] = useState("");
const handleSaveKey = () => {
if (apiKeyValue) {
addApiKeyStore(apiKeyValue).then(
() => {
setSuccessData({
title: "Success! Your API Key has been saved.",
});
storeApiKey(apiKeyValue);
setOpen(false);
},
(error) => {
setErrorData({
title: "There was an error saving the API Key, please try again.",
list: [error["response"]["data"]["detail"]],
});
}
);
}
};
return (
<BaseModal size="small-h-full" open={open && !disabled} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
(hasApiKey && !validApiKey
? "Your API key is not valid. "
: !hasApiKey
? "You don't have an API key. "
: "") + "Insert your Langflow API key."
}
>
<span className="pr-2">API Key</span>
<IconComponent
name="Key"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
}}
>
<div className="grid gap-5">
<Form.Field name="apikey">
<div className="flex items-center justify-between gap-2">
<Form.Control asChild>
<Input
//fake api key
value={apiKeyValue}
type="password"
onChange={({ target: { value } }) => {
setApiKeyValue(value);
}}
placeholder="Insert your API Key"
/>
</Form.Control>
</div>
</Form.Field>
</div>
<div className="flex items-end justify-between">
<span className="pr-1 text-xs text-muted-foreground">
Don’t have an API key? Sign up at{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
<div className="">
<Button
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Form.Submit asChild>
<Button
className="mt-8"
onClick={() => {
handleSaveKey();
}}
>
Save
</Button>
</Form.Submit>
</div>
</div>
</Form.Root>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -67,7 +67,8 @@ interface BaseModalProps {
| "large" | "large"
| "large-h-full" | "large-h-full"
| "small-h-full" | "small-h-full"
| "medium-h-full"; | "medium-h-full"
| "smaller-h-full";
disable?: boolean; disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void; onChangeOpenModal?: (open?: boolean) => void;
@ -98,11 +99,15 @@ function BaseModal({
switch (size) { switch (size) {
case "x-small": case "x-small":
minWidth = "min-w-[20vw]"; minWidth = "min-w-[20vw]";
height = "h-[10vh]"; height = " ";
break; break;
case "smaller": case "smaller":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
height = "h-[27vh]"; height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break; break;
case "small": case "small":
minWidth = "min-w-[40vw]"; minWidth = "min-w-[40vw]";
@ -145,9 +150,7 @@ function BaseModal({
<div className="truncate-doubleline word-break-break-word"> <div className="truncate-doubleline word-break-break-word">
{headerChild} {headerChild}
</div> </div>
<div className={`mt-2 flex flex-col ${height!} w-full `}> <div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div>
{ContentChild}
</div>
{ContentFooter && ( {ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div> <div className="flex flex-row-reverse">{ContentFooter}</div>
)} )}

View file

@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants"; import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext"; import { darkContext } from "../../contexts/darkContext";
@ -23,6 +24,7 @@ export default function CodeAreaModal({
setNodeClass, setNodeClass,
children, children,
dynamic, dynamic,
readonly = false,
}: codeAreaModalPropsType): JSX.Element { }: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value); const [code, setCode] = useState(value);
const { dark } = useContext(darkContext); const { dark } = useContext(darkContext);
@ -86,8 +88,7 @@ export default function CodeAreaModal({
.then((apiReturn) => { .then((apiReturn) => {
const { data } = apiReturn; const { data } = apiReturn;
if (data) { if (data) {
setNodeClass(data); setNodeClass(data, code);
setValue(code);
setError({ detail: { error: undefined, traceback: undefined } }); setError({ detail: { error: undefined, traceback: undefined } });
setOpen(false); setOpen(false);
} }
@ -143,9 +144,15 @@ export default function CodeAreaModal({
/> />
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<Input
value={code}
className="absolute left-[500%] top-[500%]"
id="codeValue"
/>
<div className="flex h-full w-full flex-col transition-all"> <div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full"> <div className="h-full w-full">
<AceEditor <AceEditor
readOnly={readonly}
value={code} value={code}
mode="python" mode="python"
height={height ?? "100%"} height={height ?? "100%"}
@ -180,7 +187,13 @@ export default function CodeAreaModal({
</div> </div>
</div> </div>
<div className="flex h-fit w-full justify-end"> <div className="flex h-fit w-full justify-end">
<Button className="mt-3" onClick={handleClick} type="submit"> <Button
className="mt-3"
onClick={handleClick}
type="submit"
id="checkAndSaveBtn"
disabled={readonly}
>
Check & Save Check & Save
</Button> </Button>
</div> </div>

View file

@ -54,6 +54,7 @@ export default function DictAreaModal({
/> />
<div className="flex h-fit w-full justify-end"> <div className="flex h-fit w-full justify-end">
<Button <Button
data-testid="save-dict-button"
className="mt-3" className="mt-3"
type="submit" type="submit"
onClick={() => { onClick={() => {

View file

@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox"; import { Checkbox } from "../../components/ui/checkbox";
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants"; import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { removeApiKeys } from "../../utils/reactflowUtils"; import { removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
const ExportModal = forwardRef( const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => { (props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(TabsContext); const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
const [checked, setChecked] = useState(false); const { reactFlowInstance } = useContext(typesContext);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const flow = flows.find((f) => f.id === tabId); const flow = flows.find((f) => f.id === tabId);
useEffect(() => { useEffect(() => {
setName(flow!.name); setName(flow!.name);
@ -22,7 +26,7 @@ const ExportModal = forwardRef(
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<BaseModal size="smaller" open={open} setOpen={setOpen}> <BaseModal size="smaller-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{props.children}</BaseModal.Trigger> <BaseModal.Trigger>{props.children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}> <BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
<span className="pr-2">Export</span> <span className="pr-2">Export</span>
@ -36,14 +40,13 @@ const ExportModal = forwardRef(
<EditFlowSettings <EditFlowSettings
name={name} name={name}
description={description} description={description}
flows={flows}
tabId={tabId}
setName={setName} setName={setName}
setDescription={setDescription} setDescription={setDescription}
/> />
<div className="mt-3 flex items-center space-x-2"> <div className="mt-3 flex items-center space-x-2">
<Checkbox <Checkbox
id="terms" id="terms"
checked={checked}
onCheckedChange={(event: boolean) => { onCheckedChange={(event: boolean) => {
setChecked(event); setChecked(event);
}} }}
@ -52,20 +55,42 @@ const ExportModal = forwardRef(
Save with my API keys Save with my API keys
</label> </label>
</div> </div>
<span className=" text-xs text-destructive ">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>
<Button <Button
onClick={() => { onClick={() => {
if (checked) if (checked) {
downloadFlow( downloadFlow(
flows.find((flow) => flow.id === tabId)!, {
id: tabId,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!, name!,
description description
); );
else setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
});
} else
downloadFlow( downloadFlow(
removeApiKeys(flows.find((flow) => flow.id === tabId)!), removeApiKeys({
id: tabId,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!, name!,
description description
); );

View file

@ -3,15 +3,16 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants"; import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { FlowSettingsPropsType } from "../../types/components"; import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import BaseModal from "../baseModal"; import BaseModal from "../baseModal";
export default function FlowSettingsModal({ export default function FlowSettingsModal({
open, open,
setOpen, setOpen,
}: FlowSettingsPropsType): JSX.Element { }: FlowSettingsPropsType): JSX.Element {
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext); const { flows, tabId, saveFlow } = useContext(FlowsContext);
const flow = flows.find((f) => f.id === tabId); const flow = flows.find((f) => f.id === tabId);
useEffect(() => { useEffect(() => {
setName(flow!.name); setName(flow!.name);
@ -19,7 +20,6 @@ export default function FlowSettingsModal({
}, [flow!.name, flow!.description]); }, [flow!.name, flow!.description]);
const [name, setName] = useState(flow!.name); const [name, setName] = useState(flow!.name);
const [description, setDescription] = useState(flow!.description); const [description, setDescription] = useState(flow!.description);
const [invalidName, setInvalidName] = useState(false);
function handleClick(): void { function handleClick(): void {
let savedFlow = flows.find((flow) => flow.id === tabId); let savedFlow = flows.find((flow) => flow.id === tabId);
@ -28,6 +28,17 @@ export default function FlowSettingsModal({
saveFlow(savedFlow!); saveFlow(savedFlow!);
setOpen(false); setOpen(false);
} }
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== flow!.name));
}, [flows]);
return ( return (
<BaseModal open={open} setOpen={setOpen} size="smaller"> <BaseModal open={open} setOpen={setOpen} size="smaller">
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}> <BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
@ -36,19 +47,20 @@ export default function FlowSettingsModal({
</BaseModal.Header> </BaseModal.Header>
<BaseModal.Content> <BaseModal.Content>
<EditFlowSettings <EditFlowSettings
invalidName={invalidName} invalidNameList={nameLists}
setInvalidName={setInvalidName}
name={name} name={name}
description={description} description={description}
flows={flows}
tabId={tabId}
setName={setName} setName={setName}
setDescription={setDescription} setDescription={setDescription}
/> />
</BaseModal.Content> </BaseModal.Content>
<BaseModal.Footer> <BaseModal.Footer>
<Button disabled={invalidName} onClick={handleClick} type="submit"> <Button
disabled={nameLists.includes(name) && name !== flow!.name}
onClick={handleClick}
type="submit"
>
Save Save
</Button> </Button>
</BaseModal.Footer> </BaseModal.Footer>

View file

@ -24,9 +24,9 @@ import {
import { Textarea } from "../../components/ui/textarea"; import { Textarea } from "../../components/ui/textarea";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants"; import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API"; import { getBuildStatus } from "../../controllers/API";
import { TabsState } from "../../types/tabs"; import { FlowsState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils"; import { validateNodes } from "../../utils/reactflowUtils";
export default function FormModal({ export default function FormModal({
@ -38,7 +38,7 @@ export default function FormModal({
setOpen: (open: boolean) => void; setOpen: (open: boolean) => void;
flow: FlowType; flow: FlowType;
}): JSX.Element { }): JSX.Element {
const { tabsState, setTabsState } = useContext(TabsContext); const { tabsState, setTabsState } = useContext(FlowsContext);
const [chatValue, setChatValue] = useState(() => { const [chatValue, setChatValue] = useState(() => {
try { try {
const { formKeysData } = tabsState[flow.id]; const { formKeysData } = tabsState[flow.id];
@ -393,7 +393,7 @@ export default function FormModal({
const message = inputs; const message = inputs;
addChatHistory(message!, true, chatKey!, template.current); addChatHistory(message!, true, chatKey!, template.current);
sendAll({ sendAll({
...reactFlowInstance?.toObject()!, ...flow.data!,
inputs: inputs!, inputs: inputs!,
chatHistory, chatHistory,
name: flow.name, name: flow.name,
@ -401,7 +401,7 @@ export default function FormModal({
chatKey: chatKey!, chatKey: chatKey!,
}); });
//@ts-ignore //@ts-ignore
setTabsState((old: TabsState) => { setTabsState((old: FlowsState) => {
if (!chatKey) return old; if (!chatKey) return old;
let newTabsState = _.cloneDeep(old); let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![chatKey] = ""; newTabsState[id.current].formKeysData.input_keys![chatKey] = "";
@ -522,7 +522,7 @@ export default function FormModal({
} }
onChange={(e) => { onChange={(e) => {
//@ts-ignore //@ts-ignore
setTabsState((old: TabsState) => { setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old); let newTabsState = _.cloneDeep(old);
newTabsState[ newTabsState[
id.current id.current
@ -634,7 +634,7 @@ export default function FormModal({
setChatValue={(value) => { setChatValue={(value) => {
setChatValue(value); setChatValue(value);
//@ts-ignore //@ts-ignore
setTabsState((old: TabsState) => { setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old); let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![ newTabsState[id.current].formKeysData.input_keys![
chatKey! chatKey!

View file

@ -30,6 +30,8 @@ export default function GenericModal({
nodeClass, nodeClass,
setNodeClass, setNodeClass,
children, children,
id = "",
readonly = false,
}: genericModalPropsType): JSX.Element { }: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText); const [myButtonText] = useState(buttonText);
const [myModalTitle] = useState(modalTitle); const [myModalTitle] = useState(modalTitle);
@ -38,7 +40,7 @@ export default function GenericModal({
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const [isEdit, setIsEdit] = useState(true); const [isEdit, setIsEdit] = useState(true);
const [wordsHighlight, setWordsHighlight] = useState<string[]>([]); const [wordsHighlight, setWordsHighlight] = useState<string[]>([]);
const { setErrorData, setSuccessData, setNoticeData } = const { setErrorData, setSuccessData, setNoticeData, setModalContextOpen } =
useContext(alertContext); useContext(alertContext);
const ref = useRef(); const ref = useRef();
const divRef = useRef(null); const divRef = useRef(null);
@ -104,15 +106,14 @@ export default function GenericModal({
: "code-nohighlight"; : "code-nohighlight";
} }
// Function need some review, working for now
function validatePrompt(closeModal: boolean): void { function validatePrompt(closeModal: boolean): void {
//nodeClass is always null on tweaks //nodeClass is always null on tweaks
postValidatePrompt(field_name, inputValue, nodeClass!) postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => { .then((apiReturn) => {
// if field_name is an empty string, then we need to set it // if field_name is an empty string, then we need to set it
// to the first key of the custom_fields object // to the first key of the custom_fields object
if (field_name === "") { if (field_name === "") {
console.log(apiReturn.data?.frontend_node?.custom_fields);
field_name = Array.isArray( field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""] apiReturn.data?.frontend_node?.custom_fields?.[""]
) )
@ -121,38 +122,23 @@ export default function GenericModal({
} }
if (apiReturn.data) { if (apiReturn.data) {
let inputVariables = apiReturn.data.input_variables ?? []; let inputVariables = apiReturn.data.input_variables ?? [];
if (inputVariables && inputVariables.length === 0) { if (!inputVariables || inputVariables.length === 0) {
setIsEdit(true); setIsEdit(true);
setNoticeData({ setNoticeData({
title: "Your template does not have any variables.", title: "Your template does not have any variables.",
}); });
setModalOpen(false); setModalOpen(false);
} else {
if ( if (
JSON.stringify(apiReturn.data?.frontend_node) !== JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({}) JSON.stringify({})
) ) {
setNodeClass!(apiReturn.data?.frontend_node); setNodeClass!(apiReturn.data?.frontend_node, inputValue);
setModalOpen(closeModal); setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
}
} else {
setIsEdit(false); setIsEdit(false);
setSuccessData({ setSuccessData({
title: "Prompt is ready", title: "Prompt is ready",
}); });
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
} }
} }
} else { } else {
@ -172,6 +158,10 @@ export default function GenericModal({
}); });
} }
useEffect(() => {
setModalContextOpen(modalOpen);
}, [modalOpen]);
return ( return (
<BaseModal <BaseModal
onChangeOpenModal={(open) => {}} onChangeOpenModal={(open) => {}}
@ -193,7 +183,9 @@ export default function GenericModal({
} }
})()} })()}
> >
<span className="pr-2">{myModalTitle}</span> <span className="pr-2" data-testid="modal-title">
{myModalTitle}
</span>
<IconComponent <IconComponent
name="FileText" name="FileText"
className="h-6 w-6 pl-1 text-primary " className="h-6 w-6 pl-1 text-primary "
@ -208,8 +200,10 @@ export default function GenericModal({
"flex h-full w-full" "flex h-full w-full"
)} )}
> >
{type === TypeModal.PROMPT && isEdit ? ( {type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea <Textarea
id={"modal-" + id}
data-testid={"modal-" + id}
ref={divRefPrompt} ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1" className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue} value={inputValue}
@ -226,7 +220,7 @@ export default function GenericModal({
handleKeyDown(e, inputValue, ""); handleKeyDown(e, inputValue, "");
}} }}
/> />
) : type === TypeModal.PROMPT && !isEdit ? ( ) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
<SanitizedHTMLWrapper <SanitizedHTMLWrapper
className={getClassByNumberLength()} className={getClassByNumberLength()}
content={coloredContent} content={coloredContent}
@ -248,6 +242,9 @@ export default function GenericModal({
onKeyDown={(e) => { onKeyDown={(e) => {
handleKeyDown(e, value, ""); handleKeyDown(e, value, "");
}} }}
readOnly={readonly}
id={"text-area-modal"}
data-testid={"text-area-modal"}
/> />
) : ( ) : (
<></> <></>
@ -284,7 +281,7 @@ export default function GenericModal({
className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm" className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm"
> >
<div className="relative bottom-[1px]"> <div className="relative bottom-[1px]">
<span> <span id={"badge" + index.toString()}>
{word.replace(/[{}]/g, "").length > 59 {word.replace(/[{}]/g, "").length > 59
? word.replace(/[{}]/g, "").slice(0, 56) + ? word.replace(/[{}]/g, "").slice(0, 56) +
"..." "..."
@ -304,6 +301,9 @@ export default function GenericModal({
)} )}
</div> </div>
<Button <Button
data-testid="genericModalBtnSave"
id="genericModalBtnSave"
disabled={readonly}
onClick={() => { onClick={() => {
switch (myModalType) { switch (myModalType) {
case TypeModal.TEXT: case TypeModal.TEXT:

View file

@ -1,101 +0,0 @@
import { buttonBoxPropsType } from "../../../types/components";
import { classNames } from "../../../utils/utils";
export default function ButtonBox({
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
}: buttonBoxPropsType): JSX.Element {
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
let textHeight: number;
let textWidth: number;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
}
return (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"button-box-modal-div",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
>
<div
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<div className="mb-auto mt-auto w-full">
<h3
className={classNames(
"w-full font-semibold text-background truncate-multiline word-break-break-word",
titleFontSize,
marginTop
)}
>
{title}
</h3>
</div>
</div>
</button>
);
}

View file

@ -1,190 +0,0 @@
import {
ArrowLeftIcon,
ArrowUpTrayIcon,
ComputerDesktopIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline";
import { useContext, useRef, useState } from "react";
import LoadingComponent from "../../components/loadingComponent";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
import { classNames } from "../../utils/utils";
import ButtonBox from "./buttonBox";
export default function ImportModal(): JSX.Element {
const [open, setOpen] = useState(true);
const { setErrorData } = useContext(alertContext);
const ref = useRef();
const [showExamples, setShowExamples] = useState(false);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
const { uploadFlow, addFlow } = useContext(TabsContext);
function handleExamples(): void {
setLoadingExamples(true);
getExamples()
.then((result) => {
setLoadingExamples(false);
setExamples(result);
})
.catch((error) =>
setErrorData({
title: "there was an error loading examples, please try again",
list: [error.message],
})
);
}
const [modalOpen, setModalOpen] = useState(false);
return (
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger>
<DialogContent
className={classNames(
showExamples
? "h-[600px] lg:max-w-[650px]"
: "h-[450px] lg:max-w-[650px]"
)}
>
<DialogHeader>
<DialogTitle className="flex items-center">
{showExamples && (
<>
<div className="dialog-header-modal-div">
<button
type="button"
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
onClick={() => {
setShowExamples(false);
}}
>
<ArrowLeftIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</button>
</div>
</>
)}
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
{showExamples ? "Select an example" : "Import"}
</span>
<ArrowUpTrayIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</DialogTitle>
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
</DialogHeader>
<div
className={classNames(
"dialog-modal-examples-div",
showExamples && !loadingExamples
? "dialog-modal-example-true"
: "dialog-modal-example-false"
)}
>
{!showExamples && (
<div className="dialog-modal-button-box-div">
<ButtonBox
size="big"
bgColor="bg-medium-emerald "
description="Prebuilt Examples"
icon={<DocumentDuplicateIcon className="document-icon" />}
onClick={() => {
setShowExamples(true);
handleExamples();
}}
textColor="text-medium-emerald "
title="Examples"
></ButtonBox>
<ButtonBox
size="big"
bgColor="bg-almost-dark-blue "
description="Import from Local"
icon={<ComputerDesktopIcon className="document-icon" />}
onClick={() => {
uploadFlow();
setModalOpen(false);
}}
textColor="text-almost-dark-blue "
title="Local File"
></ButtonBox>
</div>
)}
{showExamples && loadingExamples && (
<div className="loading-component-div">
<LoadingComponent remSize={30} />
</div>
)}
{showExamples &&
!loadingExamples &&
examples.map((example, index) => {
return (
<div key={example.name} className="m-2">
{" "}
<ButtonBox
size="small"
bgColor="bg-medium-emerald "
description={example.description ?? "Prebuilt Examples"}
icon={
<DocumentDuplicateIcon
strokeWidth={1.5}
className="h-6 w-6 flex-shrink-0"
/>
}
onClick={() => {
addFlow(example, false);
setModalOpen(false);
}}
textColor="text-medium-emerald "
title={example.name}
></ButtonBox>
</div>
);
})}
</div>
<DialogFooter>
<div className="dialog-modal-footer">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
className="dialog-modal-footer-link "
rel="noreferrer"
>
<svg
width="24"
viewBox="0 0 98 96"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
fill="currentColor"
/>
</svg>
<span className="ml-2 ">Langflow Examples</span>
</a>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,298 @@
import { Loader2 } from "lucide-react";
import { ReactNode, useContext, useEffect, useMemo, useState } from "react";
import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { typesContext } from "../../contexts/typesContext";
import {
getStoreComponents,
getStoreTags,
saveFlowStore,
updateFlowStore,
} from "../../controllers/API";
import { FlowType } from "../../types/flow";
import {
downloadNode,
removeApiKeys,
removeFileNameFromComponents,
} from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import ConfirmationModal from "../ConfirmationModal";
import BaseModal from "../baseModal";
export default function ShareModal({
component,
is_component,
children,
open,
setOpen,
disabled,
}: {
children?: ReactNode;
is_component: boolean;
component: FlowType;
open?: boolean;
setOpen?: (open: boolean) => void;
disabled?: boolean;
}): JSX.Element {
const { version, addFlow, downloadFlow } = useContext(FlowsContext);
const { hasApiKey, hasStore } = useContext(StoreContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { reactFlowInstance } = useContext(typesContext);
const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "component" : "flow";
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [loadingTags, setLoadingTags] = useState<boolean>(false);
const [sharePublic, setSharePublic] = useState(true);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [unavaliableNames, setUnavaliableNames] = useState<
{ id: string; name: string }[]
>([]);
const { saveFlow, flows, tabId } = useContext(FlowsContext);
const [loadingNames, setLoadingNames] = useState(false);
const name = component?.name ?? "";
const description = component?.description ?? "";
useEffect(() => {
if (open || internalOpen) {
if (hasApiKey && hasStore) {
handleGetTags();
handleGetNames();
}
}
}, [open, internalOpen, hasApiKey, hasStore]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags().then((res) => {
setTags(res);
setLoadingTags(false);
});
}
async function handleGetNames() {
setLoadingNames(true);
const unavaliableNames: Array<{ id: string; name: string }> = [];
await getStoreComponents({
fields: ["name", "id", "is_component"],
filterByUser: true,
}).then((res) => {
res?.results?.forEach((element: any) => {
if ((element.is_component ?? false) === is_component)
unavaliableNames.push({ name: element.name, id: element.id });
});
setUnavaliableNames(unavaliableNames);
setLoadingNames(false);
});
}
const handleShareComponent = async (update = false) => {
//remove file names from flows before sharing
removeFileNameFromComponents(component);
const flow: FlowType = removeApiKeys({
id: component!.id,
data: component!.data,
description,
name,
last_tested_version: version,
is_component: is_component,
});
function successShare() {
if (!is_component) {
saveFlow(flow!, true);
}
setSuccessData({
title: `${is_component ? "Component" : "Flow"} shared successfully!`,
});
}
if (!update)
saveFlowStore(flow!, getTagsIds(selectedTags, tags), sharePublic).then(
successShare,
(err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
});
};
const handleUpdateComponent = () => {
handleShareComponent(true);
if (setOpen) setOpen(false);
else internalSetOpen(false);
};
const handleExportComponent = () => {
component = removeApiKeys(component);
downloadNode(component);
};
let modalConfirmation = useMemo(() => {
return (
<>
<ConfirmationModal
open={openConfirmationModal}
title={`Replace`}
cancelText="Cancel"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={() => {
handleUpdateComponent();
setOpenConfirmationModal(false);
}}
onCancel={() => {
setOpenConfirmationModal(false);
}}
>
<ConfirmationModal.Content>
<span>
It seems {name} already exists. Do you want to replace it with the
current?
</span>
<br></br>
<span className=" text-xs text-destructive ">
Warning: This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</>
);
}, [
unavaliableNames,
name,
loadingNames,
handleShareComponent,
openConfirmationModal,
]);
return (
<>
<BaseModal
size="smaller-h-full"
open={(!disabled && open) ?? internalOpen}
setOpen={setOpen ?? internalSetOpen}
>
<BaseModal.Trigger asChild>
{children ? children : <></>}
</BaseModal.Trigger>
<BaseModal.Header
description={`Share your ${nameComponent} to the Langflow Store.`}
>
<span className="pr-2">Share</span>
<IconComponent
name="Share3"
className="-m-0.5 h-6 w-6 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div className="w-full rounded-lg border border-border p-4">
<EditFlowSettings name={name} description={description} />
</div>
<div className="mt-3 flex h-8 w-full">
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={false}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
</div>
<div className="mb-2 mt-5 flex items-center space-x-2">
<Checkbox
id="public"
checked={sharePublic}
onCheckedChange={(event: boolean) => {
setSharePublic(event);
}}
/>
<label htmlFor="public" className="export-modal-save-api text-sm ">
Make {nameComponent} public
</label>
</div>
<span className=" text-xs text-destructive ">
<b>Warning:</b> API keys in designated fields are removed when
sharing.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full justify-between gap-2">
<Button
type="button"
variant="outline"
className="gap-2"
onClick={() => {
handleExportComponent();
(setOpen || internalSetOpen)(false);
}}
>
<IconComponent name="Download" className="h-4 w-4" />
Export
</Button>
<Button
disabled={loadingNames}
type="button"
className={is_component ? "w-40" : "w-28"}
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
);
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
} else {
setOpenConfirmationModal(true);
}
}}
>
{loadingNames ? (
<>
<div className="center">
<Loader2 className="m-auto h-4 w-4 animate-spin"></Loader2>
</div>
</>
) : (
<>
Share{" "}
{!loadingNames && (!is_component ? "Flow" : "Component")}
</>
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
<>{modalConfirmation}</>
</>
);
}

View file

@ -22,7 +22,7 @@ import {
} from "../../constants/constants"; } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { import {
addUser, addUser,
deleteUser, deleteUser,
@ -44,7 +44,7 @@ export default function AdminPage() {
const { userData } = useContext(AuthContext); const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0); const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext); const { setTabId } = useContext(FlowsContext);
// set null id // set null id
useEffect(() => { useEffect(() => {
@ -312,7 +312,6 @@ export default function AdminPage() {
title="Edit" title="Edit"
titleHeader={`${user.username}`} titleHeader={`${user.username}`}
modalContentTitle="Attention!" modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel" cancelText="Cancel"
confirmationText="Confirm" confirmationText="Confirm"
icon={"UserCog2"} icon={"UserCog2"}
@ -326,12 +325,20 @@ export default function AdminPage() {
); );
}} }}
> >
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit"> <div className="flex w-fit">
<Checkbox <Checkbox
id="is_active" id="is_active"
checked={user.is_active} checked={user.is_active}
/> />
</div> </div>
</ConfirmationModal.Trigger>
</ConfirmationModal> </ConfirmationModal>
</TableCell> </TableCell>
<TableCell className="relative left-1 truncate py-2 text-align-last-left"> <TableCell className="relative left-1 truncate py-2 text-align-last-left">
@ -340,7 +347,6 @@ export default function AdminPage() {
title="Edit" title="Edit"
titleHeader={`${user.username}`} titleHeader={`${user.username}`}
modalContentTitle="Attention!" modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel" cancelText="Cancel"
confirmationText="Confirm" confirmationText="Confirm"
icon={"UserCog2"} icon={"UserCog2"}
@ -354,12 +360,20 @@ export default function AdminPage() {
); );
}} }}
> >
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit"> <div className="flex w-fit">
<Checkbox <Checkbox
id="is_superuser" id="is_superuser"
checked={user.is_superuser} checked={user.is_superuser}
/> />
</div> </div>
</ConfirmationModal.Trigger>
</ConfirmationModal> </ConfirmationModal>
</TableCell> </TableCell>
<TableCell className="truncate py-2 "> <TableCell className="truncate py-2 ">
@ -402,7 +416,6 @@ export default function AdminPage() {
title="Delete" title="Delete"
titleHeader="Delete User" titleHeader="Delete User"
modalContentTitle="Attention!" modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel" cancelText="Cancel"
confirmationText="Delete" confirmationText="Delete"
icon={"UserMinus2"} icon={"UserMinus2"}
@ -412,12 +425,20 @@ export default function AdminPage() {
handleDeleteUser(user); handleDeleteUser(user);
}} }}
> >
<ConfirmationModal.Content>
<span>
Are you sure you want to delete this user?
This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<ShadTooltip content="Delete" side="top"> <ShadTooltip content="Delete" side="top">
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer"
/> />
</ShadTooltip> </ShadTooltip>
</ConfirmationModal.Trigger>
</ConfirmationModal> </ConfirmationModal>
</div> </div>
</TableCell> </TableCell>

View file

@ -224,7 +224,6 @@ export default function ApiKeysPage() {
title="Delete" title="Delete"
titleHeader="Delete User" titleHeader="Delete User"
modalContentTitle="Attention!" modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this key? This action cannot be undone."
cancelText="Cancel" cancelText="Cancel"
confirmationText="Delete" confirmationText="Delete"
icon={"UserMinus2"} icon={"UserMinus2"}
@ -234,15 +233,19 @@ export default function ApiKeysPage() {
handleDeleteKey(keys); handleDeleteKey(keys);
}} }}
> >
<ShadTooltip <ConfirmationModal.Content>
content="Delete" <span>
side="top" Are you sure you want to delete
> this key? This action cannot be
undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<IconComponent <IconComponent
name="Trash2" name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer" className="ml-2 h-4 w-4 cursor-pointer"
/> />
</ShadTooltip> </ConfirmationModal.Trigger>
</ConfirmationModal> </ConfirmationModal>
</div> </div>
</TableCell> </TableCell>

View file

@ -1,116 +0,0 @@
import { useContext, useEffect, useState } from "react";
import { Button } from "../../components/ui/button";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { useNavigate } from "react-router-dom";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element {
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
useContext(TabsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const { setErrorData } = useContext(alertContext);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
// Show community examples on screen
function handleExamples(): void {
setLoadingExamples(true);
getExamples()
.then((result) => {
setLoadingExamples(false);
setExamples(result);
})
.catch((error) =>
setErrorData({
title: "there was an error loading examples, please try again",
list: [error.message],
})
);
}
const navigate = useNavigate();
// Show community examples on page start
useEffect(() => {
handleExamples();
}, []);
return (
<>
<Header />
<div className="community-page-arrangement">
<div className="community-page-nav-arrangement">
<span className="community-page-nav-title">
<IconComponent name="Users2" className="w-6" />
Community Examples
</span>
<div className="community-page-nav-button">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
rel="noreferrer"
>
<Button variant="primary">
<IconComponent
name="GithubIcon"
className="main-page-nav-button"
/>
Add Your Example
</Button>
</a>
</div>
</div>
<span className="community-page-description-text">
Discover and learn from shared examples by the Langflow community. We
welcome new example contributions that can help our community explore
new and powerful features.
</span>
<div className="community-pages-flows-panel">
{loadingExamples ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
examples.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
onClick={() => {
addFlow(flow, true).then((id) => {
navigate("/flow/" + id);
});
}}
>
<IconComponent
name="GitFork"
className="main-page-nav-button"
/>
Fork Example
</Button>
}
/>
))
)}
</div>
</div>
</>
);
}

View file

@ -8,12 +8,13 @@ export default function DisclosureComponent({
openDisc, openDisc,
}: DisclosureComponentType): JSX.Element { }: DisclosureComponentType): JSX.Element {
return ( return (
<Disclosure as="div" key={title}> <Disclosure as="div" defaultOpen={openDisc} key={title}>
{({ open }) => ( {({ open }) => (
<> <>
<div> <div>
<Disclosure.Button className="components-disclosure-arrangement"> <Disclosure.Button className="components-disclosure-arrangement">
<div className="flex gap-4"> <div className="flex gap-4">
{/* BUG ON THIS ICON */}
<Icon strokeWidth={1.5} size={22} className="text-primary" /> <Icon strokeWidth={1.5} size={22} className="text-primary" />
<span className="components-disclosure-title">{title}</span> <span className="components-disclosure-title">{title}</span>
</div> </div>
@ -34,9 +35,7 @@ export default function DisclosureComponent({
</div> </div>
</Disclosure.Button> </Disclosure.Button>
</div> </div>
<Disclosure.Panel as="div" static={openDisc}> <Disclosure.Panel as="div">{children}</Disclosure.Panel>
{children}
</Disclosure.Panel>
</> </>
)} )}
</Disclosure> </Disclosure>

View file

@ -13,10 +13,8 @@ import ReactFlow, {
Controls, Controls,
Edge, Edge,
EdgeChange, EdgeChange,
Node,
NodeChange, NodeChange,
NodeDragHandler, NodeDragHandler,
OnEdgesDelete,
OnSelectionChangeParams, OnSelectionChangeParams,
SelectionDragHandler, SelectionDragHandler,
addEdge, addEdge,
@ -27,17 +25,25 @@ import ReactFlow, {
} from "reactflow"; } from "reactflow";
import GenericNode from "../../../../CustomNodes/GenericNode"; import GenericNode from "../../../../CustomNodes/GenericNode";
import Chat from "../../../../components/chatComponent"; import Chat from "../../../../components/chatComponent";
import Loading from "../../../../components/ui/loading";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { locationContext } from "../../../../contexts/locationContext"; import { locationContext } from "../../../../contexts/locationContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext"; import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api"; import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow"; import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs"; import { FlowsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils"; import {
import { isWrappedWithClass } from "../../../../utils/utils"; generateFlow,
generateNodeFromFlow,
isValidConnection,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent"; import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent"; import ExtraSidebar from "../extraSidebarComponent";
const nodeTypes = { const nodeTypes = {
@ -54,7 +60,6 @@ export default function Page({
let { let {
updateFlow, updateFlow,
uploadFlow, uploadFlow,
addFlow,
getNodeId, getNodeId,
paste, paste,
lastCopiedSelection, lastCopiedSelection,
@ -63,7 +68,8 @@ export default function Page({
saveFlow, saveFlow,
setTabsState, setTabsState,
tabId, tabId,
} = useContext(TabsContext); saveCurrentFlow,
} = useContext(FlowsContext);
const { const {
types, types,
reactFlowInstance, reactFlowInstance,
@ -76,16 +82,24 @@ export default function Page({
const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { takeSnapshot } = useContext(undoRedoContext); const { takeSnapshot } = useContext(undoRedoContext);
const { nodesOnFlow, setNodesOnFlow } = useContext(FlowsContext);
const [position, setPosition] = useState({ x: 0, y: 0 }); const position = useRef({ x: 0, y: 0 });
const [lastSelection, setLastSelection] = const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null); useState<OnSelectionChangeParams | null>(null);
useEffect(() => { const saveCurrentFlowTimeout = () => {
// this effect is used to attach the global event handlers setTimeout(() => {
saveCurrentFlow();
}, 500); // need to do this because ReactFlow is not asynchronous.
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => { const onKeyDown = (event: KeyboardEvent) => {
if (!isWrappedWithClass(event, "nocopy")) { if (
!isWrappedWithClass(event, "nocopy") &&
window.getSelection()?.toString().length === 0
) {
if ( if (
(event.ctrlKey || event.metaKey) && (event.ctrlKey || event.metaKey) &&
event.key === "c" && event.key === "c" &&
@ -100,10 +114,10 @@ export default function Page({
lastCopiedSelection lastCopiedSelection
) { ) {
event.preventDefault(); event.preventDefault();
let bounds = reactFlowWrapper.current?.getBoundingClientRect(); takeSnapshot();
paste(lastCopiedSelection, { paste(lastCopiedSelection, {
x: position.x - bounds!.left, x: position.current.x,
y: position.y - bounds!.top, y: position.current.y,
}); });
} }
if ( if (
@ -120,13 +134,16 @@ export default function Page({
lastSelection lastSelection
) { ) {
event.preventDefault(); event.preventDefault();
takeSnapshot();
deleteNode(lastSelection.nodes.map((node) => node.id)); deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id)); deleteEdge(lastSelection.edges.map((edge) => edge.id));
saveCurrentFlowTimeout();
} }
} }
}; };
const handleMouseMove = (event) => { const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY }); position.current = { x: event.clientX, y: event.clientY };
}; };
document.addEventListener("keydown", onKeyDown); document.addEventListener("keydown", onKeyDown);
@ -136,7 +153,12 @@ export default function Page({
document.removeEventListener("keydown", onKeyDown); document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mousemove", handleMouseMove);
}; };
}, [position, lastCopiedSelection, lastSelection]); }, [
lastCopiedSelection,
lastSelection,
takeSnapshot,
saveCurrentFlowTimeout,
]);
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false); const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
@ -145,63 +167,54 @@ export default function Page({
const [nodes, setNodes, onNodesChange] = useNodesState( const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? [] flow.data?.nodes ?? []
); );
const [edges, setEdges, onEdgesChange] = useEdgesState( const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? [] flow.data?.edges ?? []
); );
const { setViewport } = useReactFlow(); const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true); const edgeUpdateSuccessful = useRef(true);
const [loading, setLoading] = useState(true);
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => { useEffect(() => {
if (reactFlowInstance && flow) { setLoading(true);
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [edges]);
//update flow when tabs change
useEffect(() => {
setNodes(flow?.data?.nodes ?? []); setNodes(flow?.data?.nodes ?? []);
setEdges(flow?.data?.edges ?? []); setEdges(flow?.data?.edges ?? []);
if (reactFlowInstance) { setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 });
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
reactFlowInstance.fitView();
}
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
//set extra sidebar
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
}, [setExtraComponent, setExtraNavigation]);
const [seconds, setSeconds] = useState(0); // Clear the previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Create a new timeout
timeoutRef.current = setTimeout(() => {
setLoading(false);
}, 300);
// Clear the timeout when the component is unmounted
return () => {
clearTimeout(timeoutRef.current);
};
}, [flow, reactFlowInstance]);
useEffect(() => { useEffect(() => {
const interval = setInterval(() => { const interval = setInterval(() => {
setSeconds((prevSeconds) => {
let updatedSeconds = prevSeconds + 1;
if (updatedSeconds % 30 === 0) {
saveFlow(flow, true); saveFlow(flow, true);
updatedSeconds = 0; }, 30000);
}
return updatedSeconds;
});
}, 1000);
return () => { return () => {
clearInterval(interval); clearInterval(interval);
}; };
}, []); }, [flow, flow.data]);
const onEdgesChangeMod = useCallback( const onEdgesChangeMod = useCallback(
(change: EdgeChange[]) => { (change: EdgeChange[]) => {
onEdgesChange(change); onEdgesChange(change);
setNodes((node) => {
let newX = _.cloneDeep(node);
return newX;
});
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { setTabsState((prev: FlowsState) => {
return { return {
...prev, ...prev,
[tabId]: { [tabId]: {
@ -210,15 +223,19 @@ export default function Page({
}, },
}; };
}); });
saveCurrentFlowTimeout();
}, },
[onEdgesChange, setNodes, setTabsState, tabId] [onEdgesChange, setNodes, setTabsState, saveCurrentFlowTimeout, tabId]
); );
const onNodesChangeMod = useCallback( const onNodesChangeMod = useCallback(
(change: NodeChange[]) => { (change: NodeChange[]) => {
const changeString = JSON.stringify(change);
if (changeString !== nodesOnFlow) {
onNodesChange(change); onNodesChange(change);
updateNodeFlow(changeString);
//@ts-ignore //@ts-ignore
setTabsState((prev: TabsState) => { setTabsState((prev: FlowsState) => {
return { return {
...prev, ...prev,
[tabId]: { [tabId]: {
@ -227,10 +244,16 @@ export default function Page({
}, },
}; };
}); });
saveCurrentFlowTimeout();
}
}, },
[onNodesChange, setTabsState, tabId] [onNodesChange, setTabsState, tabId, updateNodeFlow, saveCurrentFlowTimeout]
); );
function updateNodeFlow(changeString: string) {
setNodesOnFlow(changeString);
}
const onConnect = useCallback( const onConnect = useCallback(
(params: Connection) => { (params: Connection) => {
takeSnapshot(); takeSnapshot();
@ -238,12 +261,19 @@ export default function Page({
addEdge( addEdge(
{ {
...params, ...params,
data: {
targetHandle: scapeJSONParse(params.targetHandle!),
sourceHandle: scapeJSONParse(params.sourceHandle!),
},
style: { stroke: "#555" }, style: { stroke: "#555" },
className: className:
(params.targetHandle?.split("|")[0] === "Text" ((scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text"
? "stroke-foreground " ? "stroke-foreground "
: "stroke-foreground ") + " stroke-connection", : "stroke-foreground ") + " stroke-connection",
animated: params.targetHandle?.split("|")[0] === "Text", animated:
(scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text",
}, },
eds eds
) )
@ -252,8 +282,19 @@ export default function Page({
let newX = _.cloneDeep(node); let newX = _.cloneDeep(node);
return newX; return newX;
}); });
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
}, },
[setEdges, setNodes, takeSnapshot] };
});
saveCurrentFlowTimeout();
},
[setEdges, setNodes, takeSnapshot, addEdge]
); );
const onNodeDragStart: NodeDragHandler = useCallback(() => { const onNodeDragStart: NodeDragHandler = useCallback(() => {
@ -267,11 +308,6 @@ export default function Page({
takeSnapshot(); takeSnapshot();
}, [takeSnapshot]); }, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onDragOver = useCallback((event: React.DragEvent) => { const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault(); event.preventDefault();
if (event.dataTransfer.types.some((types) => types === "nodedata")) { if (event.dataTransfer.types.some((types) => types === "nodedata")) {
@ -296,11 +332,10 @@ export default function Page({
event.dataTransfer.getData("nodedata") event.dataTransfer.getData("nodedata")
); );
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
// Calculate the position where the node should be created // Calculate the position where the node should be created
const position = reactFlowInstance!.project({ const position = reactFlowInstance!.screenToFlowPosition({
x: event.clientX - reactflowBounds!.left, x: event.clientX,
y: event.clientY - reactflowBounds!.top, y: event.clientY,
}); });
// Generate a unique node ID // Generate a unique node ID
@ -337,7 +372,21 @@ export default function Page({
} else if (event.dataTransfer.types.some((types) => types === "Files")) { } else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot(); takeSnapshot();
if (event.dataTransfer.files.item(0)!.type === "application/json") { if (event.dataTransfer.files.item(0)!.type === "application/json") {
uploadFlow(false, event.dataTransfer.files.item(0)!); const position = {
x: event.clientX,
y: event.clientY,
};
uploadFlow({
newProject: false,
isComponent: false,
file: event.dataTransfer.files.item(0)!,
position: position,
}).catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else { } else {
setErrorData({ setErrorData({
title: "Invalid file type", title: "Invalid file type",
@ -351,6 +400,9 @@ export default function Page({
); );
useEffect(() => { useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
return () => { return () => {
if (tabsState && tabsState[flow.id]?.isPending) { if (tabsState && tabsState[flow.id]?.isPending) {
saveFlow(flow); saveFlow(flow);
@ -358,21 +410,6 @@ export default function Page({
}; };
}, []); }, []);
const onDelete = useCallback(
(mynodes: Node[]) => {
takeSnapshot();
setEdges(
edges.filter(
(edge) =>
!mynodes.some(
(node) => edge.source === node.id || edge.target === node.id
)
)
);
},
[takeSnapshot, edges, setEdges]
);
const onEdgeUpdateStart = useCallback(() => { const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false; edgeUpdateSuccessful.current = false;
}, []); }, []);
@ -394,7 +431,7 @@ export default function Page({
edgeUpdateSuccessful.current = true; edgeUpdateSuccessful.current = true;
}, []); }, []);
const [selectionEnded, setSelectionEnded] = useState(false); const [selectionEnded, setSelectionEnded] = useState(true);
const onSelectionEnd = useCallback(() => { const onSelectionEnd = useCallback(() => {
setSelectionEnded(true); setSelectionEnded(true);
@ -424,6 +461,20 @@ export default function Page({
setFilterEdge([]); setFilterEdge([]);
}, []); }, []);
const onMove = useCallback(() => {
saveCurrentFlowTimeout();
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
}, [setTabsState, saveCurrentFlowTimeout]);
return ( return (
<div className="flex h-full overflow-hidden"> <div className="flex h-full overflow-hidden">
{!view && <ExtraSidebar />} {!view && <ExtraSidebar />}
@ -434,16 +485,18 @@ export default function Page({
<div className="h-full w-full" ref={reactFlowWrapper}> <div className="h-full w-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 && {Object.keys(templates).length > 0 &&
Object.keys(types).length > 0 ? ( Object.keys(types).length > 0 ? (
<div className="h-full w-full"> <div id="react-flow-id" className="h-full w-full">
<div
className={cn(
"relative flex h-full w-full items-center justify-center bg-background",
!loading ? "hidden" : ""
)}
>
<Loading />
</div>
<ReactFlow <ReactFlow
nodes={nodes} nodes={nodes}
onMove={() => { onMove={onMove}
if (reactFlowInstance)
updateFlow({
...flow,
data: reactFlowInstance.toObject(),
});
}}
edges={edges} edges={edges}
onNodesChange={onNodesChangeMod} onNodesChange={onNodesChangeMod}
onEdgesChange={onEdgesChangeMod} onEdgesChange={onEdgesChangeMod}
@ -458,12 +511,10 @@ export default function Page({
onSelectionDragStart={onSelectionDragStart} onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd} onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart} onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent} connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver} onDragOver={onDragOver}
onDrop={onDrop} onDrop={onDrop}
onSelectionChange={onSelectionChange} onSelectionChange={onSelectionChange}
onNodesDelete={onDelete}
deleteKeyCode={[]} deleteKeyCode={[]}
className="theme-attribution" className="theme-attribution"
minZoom={0.01} minZoom={0.01}
@ -481,6 +532,51 @@ export default function Page({
[&>button]:border-b-border hover:[&>button]:bg-border" [&>button]:border-b-border hover:[&>button]:bg-border"
></Controls> ></Controls>
)} )}
<SelectionMenu
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
takeSnapshot();
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
lastSelection!,
reactFlowInstance!,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
!lastSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
} else {
setErrorData({
title: "Invalid selection",
list: validateSelection(lastSelection!, edges),
});
}
}}
/>
</ReactFlow> </ReactFlow>
{!view && ( {!view && (
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} /> <Chat flow={flow} reactFlowInstance={reactFlowInstance!} />

View file

@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
}, [isOpen]);
// transition starts after and ends before the toolbar closes
useEffect(() => {
if (isVisible) {
setIsOpen(true);
setTimeout(() => {
setIsTransitioning(true);
}, 50);
} else {
setIsTransitioning(false);
setTimeout(() => {
setIsOpen(false);
}, 500);
}
}, [isVisible]);
return (
<NodeToolbar
isVisible={isOpen}
offset={5}
nodeId={
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
}
>
<div className="h-10 w-28 overflow-hidden">
<div
className={
"duration-400 h-10 w-24 rounded-md border border-indigo-300 bg-background px-2.5 text-primary shadow-inner transition-all ease-in-out" +
(isTransitioning ? " opacity-100" : " opacity-0 ")
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
onClick={onClick}
>
<GradientGroup
strokeWidth={1.5}
size={22}
className="text-primary"
/>
Group
</button>
</div>
</div>
</NodeToolbar>
);
}

View file

@ -1,29 +1,37 @@
import { cloneDeep } from "lodash"; import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect, useMemo, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input"; import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator"; import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext"; import { alertContext } from "../../../../contexts/alertContext";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { typesContext } from "../../../../contexts/typesContext"; import { typesContext } from "../../../../contexts/typesContext";
import ApiModal from "../../../../modals/ApiModal"; import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal"; import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal";
import { APIClassType, APIObjectType } from "../../../../types/api"; import { APIClassType, APIObjectType } from "../../../../types/api";
import { import {
nodeColors, nodeColors,
nodeIconsLucide, nodeIconsLucide,
nodeNames, nodeNames,
} from "../../../../utils/styleUtils"; } from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils"; import {
classNames,
removeCountFromString,
sensitiveSort,
} from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent"; import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
export default function ExtraSidebar(): JSX.Element { export default function ExtraSidebar(): JSX.Element {
const { data, templates, getFilterEdge, setFilterEdge } = const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } =
useContext(typesContext); useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } = const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
useContext(TabsContext); useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext); const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
const { setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data); const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const isPending = tabsState[tabId]?.isPending; const isPending = tabsState[tabId]?.isPending;
@ -52,8 +60,10 @@ export default function ExtraSidebar(): JSX.Element {
let ret = {}; let ret = {};
Object.keys(data).forEach((d: keyof APIObjectType, i) => { Object.keys(data).forEach((d: keyof APIObjectType, i) => {
ret[d] = {}; ret[d] = {};
let keys = Object.keys(data[d]).filter((nd) => let keys = Object.keys(data[d]).filter(
nd.toLowerCase().includes(e.toLowerCase()) (nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
); );
keys.forEach((element) => { keys.forEach((element) => {
ret[d][element] = data[d][element]; ret[d][element] = data[d][element];
@ -76,7 +86,8 @@ export default function ExtraSidebar(): JSX.Element {
}, []); }, []);
function handleBlur() { function handleBlur() {
if (!search && search === "") { // check if search is search to reset fitler on click input
if ((!search && search === "") || search === "search") {
setFilterData(data); setFilterData(data);
setFilterEdge([]); setFilterEdge([]);
setSearch(""); setSearch("");
@ -84,11 +95,55 @@ export default function ExtraSidebar(): JSX.Element {
} }
useEffect(() => { useEffect(() => {
if (getFilterEdge.length === 0 && search === "") { if (getFilterEdge.length !== 0) {
setFilterData(data);
setFilterEdge([]);
setSearch(""); setSearch("");
} }
if (getFilterEdge.length === 0 && search === "") {
setSearch("");
setFilterData(data);
}
}, [getFilterEdge, data]);
useEffect(() => {
handleSearchInput(search);
}, [data]);
useEffect(() => {
if (getFilterEdge?.length > 0) {
setFilterData((_) => {
let dataClone = cloneDeep(data);
let ret = {};
Object.keys(dataClone).forEach((d: keyof APIObjectType, i) => {
ret[d] = {};
if (getFilterEdge.some((x) => x.family === d)) {
ret[d] = dataClone[d];
const filtered = getFilterEdge
.filter((x) => x.family === d)
.pop()
.type.split(",");
for (let i = 0; i < filtered.length; i++) {
filtered[i] = filtered[i].trimStart();
}
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
delete ret[d][element];
}
});
}
}
});
setSearch("");
return ret;
});
}
}, [getFilterEdge]); }, [getFilterEdge]);
useEffect(() => { useEffect(() => {
@ -122,39 +177,93 @@ export default function ExtraSidebar(): JSX.Element {
} }
} }
}); });
setSearch("search"); setSearch("");
return ret; return ret;
}); });
} }
}, [getFilterEdge]); }, [getFilterEdge, data]);
const ModalMemo = useMemo(
() => (
<ShareModal
is_component={false}
component={flow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: ""
)}
>
<IconComponent
name="Share3"
className={classNames(
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, flow, hasStore]
);
const ExportMemo = useMemo(
() => (
<ExportModal>
<ShadTooltip content="Export" side="top">
<button className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</button>
</ShadTooltip>
</ExportModal>
),
[]
);
return ( return (
<div className="side-bar-arrangement"> <div className="side-bar-arrangement">
<div className="side-bar-buttons-arrangement"> <div className="side-bar-buttons-arrangement">
{hasStore && (
<ShadTooltip
content={
!hasApiKey || !validApiKey
? "Please review your API key."
: "Share"
}
side="top"
styleClasses="cursor-default"
>
<div className="side-bar-button">{ModalMemo}</div>
</ShadTooltip>
)}
<div className="side-bar-button"> <div className="side-bar-button">
<ShadTooltip content="Import" side="top"> <ShadTooltip content="Import" side="top">
<button <button
className="extra-side-bar-buttons" className="extra-side-bar-buttons"
onClick={() => { onClick={() => {
uploadFlow(); uploadFlow({ newProject: false, isComponent: false }).catch(
(error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
}
);
}} }}
> >
<IconComponent name="FileUp" className="side-bar-button-size " /> <IconComponent name="FileUp" className="side-bar-button-size " />
</button> </button>
</ShadTooltip> </ShadTooltip>
</div> </div>
<div className="side-bar-button"> {!hasStore && ExportMemo}
<ExportModal>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="FileDown"
className="side-bar-button-size"
/>
</div>
</ShadTooltip>
</ExportModal>
</div>
<ShadTooltip content={"Code"} side="top"> <ShadTooltip content={"Code"} side="top">
<div className="side-bar-button"> <div className="side-bar-button">
{flow && flow.data && ( {flow && flow.data && (
@ -177,10 +286,15 @@ export default function ExtraSidebar(): JSX.Element {
</div> </div>
</ShadTooltip> </ShadTooltip>
<div className="side-bar-button"> <div className="side-bar-button">
{flow && flow.data && (
<ShadTooltip content="Save" side="top"> <ShadTooltip content="Save" side="top">
<button <button
disabled={flow?.data?.nodes.length === 0}
className={ className={
"extra-side-bar-buttons " + (isPending ? "" : "button-disable") "extra-side-bar-buttons " +
(isPending && flow!.data!.nodes?.length > 0
? ""
: "button-disable")
} }
onClick={(event) => { onClick={(event) => {
saveFlow(flow!); saveFlow(flow!);
@ -190,17 +304,21 @@ export default function ExtraSidebar(): JSX.Element {
name="Save" name="Save"
className={ className={
"side-bar-button-size" + "side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable") (isPending && flow!.data!.nodes?.length > 0
? " "
: " extra-side-bar-save-disable")
} }
/> />
</button> </button>
</ShadTooltip> </ShadTooltip>
)}
</div> </div>
</div> </div>
<Separator /> <Separator />
<div className="side-bar-search-div-placement"> <div className="side-bar-search-div-placement">
<Input <Input
onFocusCapture={() => handleBlur()} onFocusCapture={() => handleBlur()}
value={search}
type="text" type="text"
name="search" name="search"
id="search" id="search"
@ -223,12 +341,28 @@ export default function ExtraSidebar(): JSX.Element {
<div className="side-bar-components-div-arrangement"> <div className="side-bar-components-div-arrangement">
{Object.keys(dataFilter) {Object.keys(dataFilter)
.sort() .sort((a, b) => {
if (a.toLowerCase() === "saved_components") {
return -1;
} else if (b.toLowerCase() === "saved_components") {
return 1;
} else if (a.toLowerCase() === "custom_components") {
return -2;
} else if (b.toLowerCase() === "custom_components") {
return 2;
} else {
return a.localeCompare(b);
}
})
.map((SBSectionName: keyof APIObjectType, index) => .map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? ( Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<DisclosureComponent <DisclosureComponent
openDisc={search.length == 0 ? false : true} openDisc={
key={index} getFilterEdge.length !== 0 || search.length !== 0
? true
: false
}
key={index + search + JSON.stringify(getFilterEdge)}
button={{ button={{
title: nodeNames[SBSectionName] ?? nodeNames.unknown, title: nodeNames[SBSectionName] ?? nodeNames.unknown,
Icon: Icon:
@ -237,51 +371,45 @@ export default function ExtraSidebar(): JSX.Element {
> >
<div className="side-bar-components-gap"> <div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName]) {Object.keys(dataFilter[SBSectionName])
.sort() .sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
)
.map((SBItemName: string, index) => ( .map((SBItemName: string, index) => (
<ShadTooltip <ShadTooltip
content={data[SBSectionName][SBItemName].display_name} content={
dataFilter[SBSectionName][SBItemName].display_name
}
side="right" side="right"
key={index} key={index}
> >
<div key={index} data-tooltip-id={SBItemName}> <SidebarDraggableComponent
<div sectionName={SBSectionName as string}
draggable={!data[SBSectionName][SBItemName].error} apiClass={dataFilter[SBSectionName][SBItemName]}
className={ key={index}
"side-bar-components-border bg-background" +
(data[SBSectionName][SBItemName].error
? " cursor-not-allowed select-none"
: "")
}
style={{
borderLeftColor:
nodeColors[SBSectionName] ?? nodeColors.unknown,
}}
onDragStart={(event) => onDragStart={(event) =>
onDragStart(event, { onDragStart(event, {
type: SBItemName, //split type to remove type in nodes saved with same name removing it's
node: data[SBSectionName][SBItemName], type: removeCountFromString(SBItemName),
node: dataFilter[SBSectionName][SBItemName],
}) })
} }
onDragEnd={() => { color={nodeColors[SBSectionName]}
document.body.removeChild( itemName={SBItemName}
document.getElementsByClassName( //convert error to boolean
"cursor-grabbing" error={!!dataFilter[SBSectionName][SBItemName].error}
)[0] display_name={
); dataFilter[SBSectionName][SBItemName].display_name
}} }
> official={
<div className="side-bar-components-div-form"> dataFilter[SBSectionName][SBItemName].official ===
<span className="side-bar-components-text"> false
{data[SBSectionName][SBItemName].display_name} ? false
</span> : true
<IconComponent }
name="Menu"
className="side-bar-components-icon "
/> />
</div>
</div>
</div>
</ShadTooltip> </ShadTooltip>
))} ))}
</div> </div>

View file

@ -0,0 +1,152 @@
import { DragEventHandler, useContext, useRef, useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../../components/ui/select-custom";
import { AuthContext } from "../../../../../contexts/authContext";
import { FlowsContext } from "../../../../../contexts/flowsContext";
import { APIClassType } from "../../../../../types/api";
import {
createFlowComponent,
downloadNode,
} from "../../../../../utils/reactflowUtils";
import { removeCountFromString } from "../../../../../utils/utils";
export default function SidebarDraggableComponent({
sectionName,
display_name,
itemName,
error,
color,
onDragStart,
apiClass,
official,
}: {
sectionName: string;
apiClass: APIClassType;
display_name: string;
itemName: string;
error: boolean;
color: string;
onDragStart: DragEventHandler<HTMLDivElement>;
official: boolean;
}) {
const [open, setOpen] = useState(false);
const { getNodeId, deleteComponent, version } = useContext(FlowsContext);
const { autoLogin, userData } = useContext(AuthContext);
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const popoverRef = useRef<HTMLDivElement>(null);
const handlePointerDown = (e) => {
if (!open) {
const rect = popoverRef.current?.getBoundingClientRect() ?? {
left: 0,
top: 0,
};
setCursorPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}
};
function handleSelectChange(value: string) {
switch (value) {
case "share":
break;
case "download":
const type = removeCountFromString(itemName);
downloadNode(
createFlowComponent(
{ id: getNodeId(type), type, node: apiClass },
version
)
);
break;
case "delete":
deleteComponent(display_name);
break;
}
}
return (
<Select
onValueChange={handleSelectChange}
onOpenChange={(change) => setOpen(change)}
open={open}
key={itemName}
>
<div
onPointerDown={handlePointerDown}
onContextMenuCapture={(e) => {
e.preventDefault();
setOpen(true);
}}
key={itemName}
data-tooltip-id={itemName}
>
<div
draggable={!error}
className={
"side-bar-components-border bg-background" +
(error ? " cursor-not-allowed select-none" : "")
}
style={{
borderLeftColor: color,
}}
onDragStart={onDragStart}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0]
);
}}
>
<div
data-testid={sectionName + display_name}
id={sectionName + display_name}
className="side-bar-components-div-form"
>
<span className="side-bar-components-text">{display_name}</span>
<div ref={popoverRef}>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
<SelectTrigger></SelectTrigger>
<SelectContent
position="popper"
side="bottom"
sideOffset={-25}
style={{
position: "absolute",
left: cursorPos.x,
top: cursorPos.y,
}}
>
<SelectItem value={"download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
{!official && (
<SelectItem value={"delete"}>
<div className="flex">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Delete{" "}
</div>{" "}
</SelectItem>
)}
</SelectContent>
</div>
</div>
</div>
</div>
</Select>
);
}

View file

@ -1,4 +1,5 @@
import { useContext, useState } from "react"; import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useReactFlow, useUpdateNodeInternals } from "reactflow"; import { useReactFlow, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent"; import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent"; import IconComponent from "../../../../components/genericIconComponent";
@ -8,15 +9,26 @@ import {
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
} from "../../../../components/ui/select-custom"; } from "../../../../components/ui/select-custom";
import { TabsContext } from "../../../../contexts/tabsContext"; import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import ConfirmationModal from "../../../../modals/ConfirmationModal";
import EditNodeModal from "../../../../modals/EditNodeModal"; import EditNodeModal from "../../../../modals/EditNodeModal";
import ShareModal from "../../../../modals/shareModal";
import { nodeToolbarPropsType } from "../../../../types/components"; import { nodeToolbarPropsType } from "../../../../types/components";
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils"; import { FlowType } from "../../../../types/flow";
import {
createFlowComponent,
downloadNode,
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
export default function NodeToolbarComponent({ export default function NodeToolbarComponent({
data, data,
setData,
deleteNode, deleteNode,
position,
setShowNode, setShowNode,
numberOfHandles, numberOfHandles,
showNode, showNode,
@ -33,10 +45,14 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "prompt" || data.node.template[templateField].type === "prompt" ||
data.node.template[templateField].type === "file" || data.node.template[templateField].type === "file" ||
data.node.template[templateField].type === "Any" || data.node.template[templateField].type === "Any" ||
data.node.template[templateField].type === "int") data.node.template[templateField].type === "int" ||
data.node.template[templateField].type === "dict" ||
data.node.template[templateField].type === "NestedDict")
).length ).length
); );
const updateNodeInternals = useUpdateNodeInternals(); const updateNodeInternals = useUpdateNodeInternals();
const { getNodeId } = useContext(FlowsContext);
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
function canMinimize() { function canMinimize() {
let countHandles: number = 0; let countHandles: number = 0;
@ -47,26 +63,70 @@ export default function NodeToolbarComponent({
return true; return true;
} }
const isMinimal = canMinimize(); const isMinimal = canMinimize();
const { paste } = useContext(TabsContext); const isGroup = data.node?.flow ? true : false;
const { paste, saveComponent, version, flows } = useContext(FlowsContext);
const { takeSnapshot } = useContext(undoRedoContext);
const reactFlowInstance = useReactFlow(); const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false); const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState(""); const [selectedValue, setSelectedValue] = useState("");
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>();
const openInNewTab = (url) => {
window.open(url, "_blank", "noreferrer");
};
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [
data,
data.node,
data.node?.display_name,
data.node?.description,
data.node?.template,
showModalAdvanced,
showconfirmShare,
]);
const handleSelectChange = (event) => { const handleSelectChange = (event) => {
setSelectedValue(event); switch (event) {
if (event.includes("advanced")) { case "advanced":
return setShowModalAdvanced(true); setShowModalAdvanced(true);
} break;
setShowModalAdvanced(false); case "show":
if (event.includes("show")) { takeSnapshot();
setShowNode((prev) => !prev); setShowNode(data.showNode ?? true ? false : true);
updateNodeInternals(data.id); updateNodeInternals(data.id);
} break;
if (event.includes("disabled")) { case "Download":
return; downloadNode(createFlowComponent(cloneDeep(data), version));
break;
case "SaveAll":
saveComponent(cloneDeep(data), false);
break;
case "documentation":
if (data.node?.documentation) openInNewTab(data.node?.documentation);
break;
case "disabled":
break;
case "ungroup":
takeSnapshot();
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance, getNodeId);
break;
case "override":
setShowOverrideModal(true);
break;
} }
}; };
const isSaved = flows.some((flow) =>
Object.values(flow).includes(data.node?.display_name!)
);
return ( return (
<> <>
<div className="w-26 h-10"> <div className="w-26 h-10">
@ -106,45 +166,31 @@ export default function NodeToolbarComponent({
<IconComponent name="Copy" className="h-4 w-4" /> <IconComponent name="Copy" className="h-4 w-4" />
</button> </button>
</ShadTooltip> </ShadTooltip>
{hasStore && (
<ShadTooltip <ShadTooltip content="Share" side="top">
content={ <button
data.node?.documentation === "" ? "Coming Soon" : "Documentation"
}
side="top"
>
<a
className={classNames( className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" + "relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
(data.node?.documentation === "" !hasApiKey || !validApiKey ? " text-muted-foreground" : ""
? " text-muted-foreground"
: " text-foreground")
)} )}
target="_blank"
rel="noopener noreferrer"
href={data.node?.documentation}
// deactivate link if no documentation is provided
onClick={(event) => { onClick={(event) => {
if (data.node?.documentation === "") {
event.preventDefault(); event.preventDefault();
} if (hasApiKey || hasStore) setShowconfirmShare(true);
}} }}
> >
<IconComponent name="FileText" className="h-4 w-4 " /> <IconComponent name="Share3" className="-m-1 h-6 w-6" />
</a> </button>
</ShadTooltip> </ShadTooltip>
)}
{isMinimal ? (
<Select onValueChange={handleSelectChange} value={selectedValue}> <Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top"> <ShadTooltip content="More" side="top">
<SelectTrigger> <SelectTrigger>
<div> <div>
<div <div
data-testid="more-options-modal"
className={classNames( className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" + "relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)} )}
> >
<IconComponent <IconComponent
@ -156,20 +202,9 @@ export default function NodeToolbarComponent({
</SelectTrigger> </SelectTrigger>
</ShadTooltip> </ShadTooltip>
<SelectContent> <SelectContent>
<SelectItem {nodeLength > 0 && (
value={ <SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
getRandomKeyByssmm() + <div className="flex" data-testid="edit-button-modal">
(nodeLength == 0 ? "disabled" : "advanced")
}
>
<div
className={
"flex " +
(nodeLength == 0
? "text-muted-foreground"
: "text-primary")
}
>
<IconComponent <IconComponent
name="Settings2" name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4" className="relative top-0.5 mr-2 h-4 w-4"
@ -177,8 +212,56 @@ export default function NodeToolbarComponent({
Edit{" "} Edit{" "}
</div>{" "} </div>{" "}
</SelectItem> </SelectItem>
)}
{isSaved ? (
<SelectItem value={"override"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
) : (
<SelectItem value={"SaveAll"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
)}
{!hasStore && (
<SelectItem value={"Download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
)}
<SelectItem
value={"documentation"}
disabled={data.node?.documentation === ""}
>
<div className="flex">
<IconComponent
name="FileText"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
{data.node?.documentation === ""
? "Coming Soon"
: "Documentation"}
</div>{" "}
</SelectItem>
{isMinimal && ( {isMinimal && (
<SelectItem value={getRandomKeyByssmm() + "show"}> <SelectItem value={"show"}>
<div className="flex"> <div className="flex">
<IconComponent <IconComponent
name={showNode ? "Minimize2" : "Maximize2"} name={showNode ? "Minimize2" : "Maximize2"}
@ -188,63 +271,57 @@ export default function NodeToolbarComponent({
</div> </div>
</SelectItem> </SelectItem>
)} )}
{isGroup && (
<SelectItem value="ungroup">
<div className="flex">
<IconComponent
name="Combine"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent> </SelectContent>
</Select> </Select>
) : (
<ShadTooltip content="Edit" side="top">
<div>
<button
disabled={nodeLength === 0}
onClick={() => setShowModalAdvanced(true)}
className={classNames(
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</button>
</div>
</ShadTooltip>
)}
{showModalAdvanced && ( <ConfirmationModal
asChild
open={showOverrideModal}
title={`Replace`}
cancelText="Create New"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={(index, user) => {
saveComponent(cloneDeep(data), true);
}}
onClose={setShowOverrideModal}
onCancel={() => saveComponent(cloneDeep(data), false)}
>
<ConfirmationModal.Content>
<span>
It seems {data.node?.display_name} already exists. Do you want
to replace it with the current or create a new one?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</ConfirmationModal.Trigger>
</ConfirmationModal>
<EditNodeModal <EditNodeModal
data={data} data={data}
setData={setData}
nodeLength={nodeLength} nodeLength={nodeLength}
open={showModalAdvanced} open={showModalAdvanced}
onClose={(modal) => { setOpen={setShowModalAdvanced}
setShowModalAdvanced(modal); />
}} <ShareModal
> open={showconfirmShare}
<></> setOpen={setShowconfirmShare}
</EditNodeModal> is_component={true}
)} component={flowComponent!}
/>
{/*
<ShadTooltip content="Edit" side="top">
<div>
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
>
<div
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</div>
</EditNodeModal>
</div>
</ShadTooltip> */}
</span> </span>
</div> </div>
</> </>

View file

@ -1,12 +1,11 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Header from "../../components/headerComponent"; import Header from "../../components/headerComponent";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { getVersion } from "../../controllers/API";
import Page from "./components/PageComponent"; import Page from "./components/PageComponent";
export default function FlowPage(): JSX.Element { export default function FlowPage(): JSX.Element {
const { flows, tabId, setTabId } = useContext(TabsContext); const { flows, tabId, setTabId, version } = useContext(FlowsContext);
const { id } = useParams(); const { id } = useParams();
// Set flow tab id // Set flow tab id
@ -14,14 +13,6 @@ export default function FlowPage(): JSX.Element {
setTabId(id!); setTabId(id!);
}, [id]); }, [id]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return ( return (
<> <>
<Header /> <Header />

View file

@ -0,0 +1,202 @@
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import PaginatorComponent from "../../../../components/PaginatorComponent";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { FlowType } from "../../../../types/flow";
export default function ComponentsComponent({
is_component = true,
}: {
is_component?: boolean;
}) {
const { flows, removeFlow, uploadFlow, addFlow, isLoading } =
useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [pageSize, setPageSize] = useState(10);
const [pageIndex, setPageIndex] = useState(1);
const [loadingScreen, setLoadingScreen] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (isLoading) return;
const all = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
setData(all.slice(start, end));
}, [flows, pageIndex, pageSize]);
const [data, setData] = useState<FlowType[]>([]);
const name = is_component ? "Component" : "Flow";
const onFileDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow({
newProject: true,
file: e.dataTransfer.files.item(0)!,
isComponent: is_component,
})
.then(() => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
function resetFilter() {
setPageIndex(1);
setPageSize(10);
}
useEffect(() => {
setTimeout(() => {
setLoadingScreen(false);
}, 600);
}, []);
return (
<CardsWrapComponent
onFileDrop={onFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!loadingScreen && data.length === 0 ? (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1">
<span>New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{loadingScreen === false && data?.length > 0 ? (
data?.map((item, idx) => (
<CollectionCardComponent
onDelete={() => {
removeFlow(item.id);
setSuccessData({
title: `${
item.is_component ? "Component" : "Flow"
} deleted successfully!`,
});
resetFilter();
}}
key={idx}
data={item}
disabled={isLoading}
button={
!is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
/>
))
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!loadingScreen && data.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={
flows.filter((f) => (f.is_component ?? false) === is_component)
.length
}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
);
}

View file

@ -1,88 +1,73 @@
import { useContext, useEffect, useState } from "react"; import { Group, ToyBrick } from "lucide-react";
import { Link, useNavigate } from "react-router-dom"; import { useContext, useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import DropdownButton from "../../components/DropdownButtonComponent"; import DropdownButton from "../../components/DropdownButtonComponent";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent"; import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent"; import PageLayout from "../../components/pageLayout";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent"; import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button"; import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants"; import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
export default function HomePage(): JSX.Element { export default function HomePage(): JSX.Element {
const { const { setTabId, downloadFlows, uploadFlows, addFlow, uploadFlow } =
flows, useContext(FlowsContext);
setTabId, const { setErrorData, setSuccessData } = useContext(alertContext);
downloadFlows, const location = useLocation();
uploadFlows, const pathname = location.pathname;
addFlow, const is_component = pathname === "/components";
removeFlow,
uploadFlow,
isLoading,
} = useContext(TabsContext);
const { setErrorData } = useContext(alertContext);
const dropdownOptions = [ const dropdownOptions = [
{ {
name: "Import from JSON", name: "Import from JSON",
onBtnClick: () => onBtnClick: () => {
uploadFlow(true).then((id) => { uploadFlow({
navigate("/flow/" + id); newProject: true,
}), isComponent: is_component,
})
.then((id) => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
if (!is_component) navigate("/flow/" + id);
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
},
},
];
const sidebarNavItems = [
{
title: "Flows",
href: "/flows",
icon: <Group className="w-5 stroke-[1.5]" />,
},
{
title: "Components",
href: "/components",
icon: <ToyBrick className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]" />,
}, },
]; ];
// Set a null id // Set a null id
useEffect(() => { useEffect(() => {
setTabId(""); setTabId("");
}, []); }, [pathname]);
const navigate = useNavigate(); const navigate = useNavigate();
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = () => {
setIsDragging(false);
};
const fileDrop = (e) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow(true, e.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
// Personal flows display // Personal flows display
return ( return (
<> <PageLayout
<Header /> title={USER_PROJECTS_HEADER}
<div className="main-page-panel"> description="Manage your personal projects. Download or upload your collection."
<div className="main-page-nav-arrangement"> button={
<span className="main-page-nav-title"> <div className="flex gap-2">
<IconComponent name="Home" className="w-6" />
{USER_PROJECTS_HEADER}
</span>
<div className="button-div-style">
<Button <Button
variant="primary" variant="primary"
onClick={() => { onClick={() => {
@ -104,77 +89,23 @@ export default function HomePage(): JSX.Element {
<DropdownButton <DropdownButton
firstButtonName="New Project" firstButtonName="New Project"
onFirstBtnClick={() => { onFirstBtnClick={() => {
addFlow(null!, true).then((id) => { addFlow(true).then((id) => {
navigate("/flow/" + id); navigate("/flow/" + id);
}); });
}} }}
options={dropdownOptions} options={dropdownOptions}
/> />
</div> </div>
</div>
<span className="main-page-description-text">
Manage your personal projects. Download or upload your collection.
</span>
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={fileDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
} }
> >
{isDragging ? ( <div className="flex h-full w-full space-y-8 lg:flex-row lg:space-x-8 lg:space-y-0">
<> <aside className="flex h-full flex-col space-y-6 lg:w-1/5">
<IconComponent <SidebarNav items={sidebarNavItems} />
name="ArrowUpToLine" </aside>
className="h-12 w-12 stroke-1" <div className="h-full w-full flex-1">
/> <Outlet />
Drop your flow here
</>
) : (
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
flows.map((flow, idx) => (
<CardComponent
key={idx}
flow={flow}
id={flow.id}
button={
<Link to={"/flow/" + flow.id}>
<Button
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
)}
</div>
)}
</div> </div>
</div> </div>
</> </PageLayout>
); );
} }

View file

@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants"; import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext"; import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext"; import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext"; import { FlowsContext } from "../../contexts/flowsContext";
import { resetPassword, updateUser } from "../../controllers/API"; import { resetPassword, updateUser } from "../../controllers/API";
import { import {
inputHandlerEventType, inputHandlerEventType,
@ -17,7 +17,7 @@ import {
} from "../../types/components"; } from "../../types/components";
import { gradients } from "../../utils/styleUtils"; import { gradients } from "../../utils/styleUtils";
export default function ProfileSettingsPage(): JSX.Element { export default function ProfileSettingsPage(): JSX.Element {
const { setTabId } = useContext(TabsContext); const { setTabId } = useContext(FlowsContext);
const [inputState, setInputState] = useState<patchUserInputStateType>( const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE CONTROL_PATCH_USER_STATE

View file

@ -0,0 +1,412 @@
import { uniqueId } from "lodash";
import { useContext, useEffect, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CollectionCardComponent from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import PageLayout from "../../components/pageLayout";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { Link, useParams } from "react-router-dom";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Badge } from "../../components/ui/badge";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils";
export default function StorePage(): JSX.Element {
const { validApiKey, setValidApiKey, hasApiKey, loadingApiKey } =
useContext(StoreContext);
const { apiKey } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
const { setTabId } = useContext(FlowsContext);
const [loading, setLoading] = useState(true);
const [loadingTags, setLoadingTags] = useState(true);
const { id } = useParams();
const [filteredCategories, setFilterCategories] = useState<any[]>([]);
const [inputText, setInputText] = useState<string>("");
const [searchData, setSearchData] = useState<storeComponent[]>([]);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const [pageSize, setPageSize] = useState(12);
const [pageIndex, setPageIndex] = useState(1);
const [pageOrder, setPageOrder] = useState("Popular");
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [tabActive, setTabActive] = useState("All");
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
useEffect(() => {
handleGetTags();
}, []);
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
setErrorData({
title: "API Key Error",
list: [
"You don't have an API Key. Please add one to use the Langflow Store.",
],
});
setLoading(false);
} else if (!validApiKey) {
setErrorData({
title: "API Key Error",
list: [
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.",
],
});
}
}
}, [loadingApiKey, validApiKey, hasApiKey]);
useEffect(() => {
handleGetComponents();
}, [
tabActive,
pageOrder,
pageIndex,
pageSize,
filteredCategories,
selectFilter,
validApiKey,
hasApiKey,
apiKey,
searchNow,
loadingApiKey,
id,
]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags()
.then((res) => {
setTags(res);
setLoadingTags(false);
})
.catch((err) => {
console.log(err);
setLoadingTags(false);
});
}
function handleGetComponents() {
if (!hasApiKey || loadingApiKey) return;
setLoading(true);
getStoreComponents({
component_id: id,
page: pageIndex,
limit: pageSize,
is_component:
tabActive === "All" ? null : tabActive === "Flows" ? false : true,
sort: pageOrder === "Popular" ? "-count(downloads)" : "name",
tags: filteredCategories,
liked: selectFilter === "likedbyme" && validApiKey ? true : null,
isPrivate: null,
search: inputText === "" ? null : inputText,
filterByUser: selectFilter === "createdbyme" && validApiKey ? true : null,
})
.then((res) => {
if (!res?.authorized && validApiKey === true) {
setValidApiKey(false);
setSelectFilter("all");
} else {
if (res?.authorized) {
setValidApiKey(true);
}
setLoading(false);
setSearchData(res?.results ?? []);
setTotalRowsCount(
filteredCategories?.length === 0
? Number(res?.count ?? 0)
: res?.results?.length ?? 0
);
}
})
.catch((err) => {
if (err.response.status === 403 || err.response.status === 401) {
setValidApiKey(false);
} else {
setSearchData([]);
setTotalRowsCount(0);
setLoading(false);
setErrorData({
title: "Error getting components.",
list: [err["response"]["data"]["detail"]],
});
}
});
}
// Set a null id
useEffect(() => {
setTabId("");
}, []);
function resetPagination() {
setPageIndex(1);
setPageSize(12);
}
return (
<PageLayout
title="Langflow Store"
description="Search flows and components from the community."
button={
<>
{StoreApiKeyModal && (
<StoreApiKeyModal disabled={loading}>
<Button
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : ""
)}
variant="primary"
>
<IconComponent name="Key" className="mr-2 w-4" />
API Key
</Button>
</StoreApiKeyModal>
)}
</>
}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4 p-0">
<div className="flex items-end gap-4">
<div className="relative h-12 w-[40%]">
<Input
disabled={loading}
placeholder="Search Flows and Components"
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setInputText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchNow(uniqueId());
}
}}
value={inputText}
/>
<button
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
onClick={() => {
setSearchNow(uniqueId());
}}
>
<IconComponent
name={loading ? "Loader2" : "Search"}
className={loading ? " animate-spin cursor-not-allowed" : ""}
/>
</button>
</div>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
disabled={loading}
onClick={() => {
setTabActive("All");
}}
className={
(tabActive === "All"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
All
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Flows");
}}
className={
(tabActive === "Flows"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Flows
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Components");
}}
className={
(tabActive === "Components"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Components
</button>
<ShadTooltip content="Coming Soon">
<button className="cursor-not-allowed p-3 text-muted-foreground">
Bundles
</button>
</ShadTooltip>
</div>
</div>
<div className="flex items-center gap-2">
<Select
disabled={loading}
onValueChange={setSelectFilter}
value={selectFilter}
>
<SelectTrigger className="mr-4 w-[160px] flex-shrink-0">
<SelectValue placeholder="Filter Values" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="createdbyme"
>
Created By Me
</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="likedbyme"
>
Liked By Me
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{id === undefined ? (
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={loading}
selectedTags={filteredCategories}
setSelectedTags={setFilterCategories}
/>
) : (
<Badge
key="id"
variant="outline"
size="sq"
className="gap-2 bg-beta-foreground text-background hover:bg-beta-foreground"
>
<Link to={"/store"} className="cursor-pointer">
<IconComponent name="X" className="h-4 w-4" />
</Link>
{id}
</Badge>
)}
</div>
<div className="flex items-end justify-between">
<span className="px-0.5 text-sm text-muted-foreground">
{(!loading || searchData.length !== 0) && (
<>
{totalRowsCount} {totalRowsCount !== 1 ? "results" : "result"}
</>
)}
</span>
<Select
disabled={loading}
onValueChange={(e) => {
setPageOrder(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Popular" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Popular">Popular</SelectItem>
{/* <SelectItem value="Recent">Most Recent</SelectItem> */}
<SelectItem value="Alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{!loading || searchData.length !== 0 ? (
searchData.map((item) => {
return (
<>
<CollectionCardComponent
key={item.id}
data={item}
authorized={validApiKey}
disabled={loading}
/>
</>
);
})
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
{!loading && searchData?.length === 0 && (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex h-full w-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
{selectFilter != "all" ? (
<>
You haven't{" "}
{selectFilter === "createdbyme" ? "created" : "liked"}{" "}
anything with the selected filters yet.
</>
) : (
<>
There are no{" "}
{tabActive == "Flows" ? "Flows" : "Components"} with the
selected filters.
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
{!loading && searchData.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
totalRowsCount={totalRowsCount}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</PageLayout>
);
}

View file

@ -1,12 +1,8 @@
import { useContext, useEffect, useState } from "react"; import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API";
import Page from "../FlowPage/components/PageComponent"; import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() { export default function ViewPage() {
const { flows, tabId, setTabId } = useContext(TabsContext);
const { setDark } = useContext(darkContext); const { setDark } = useContext(darkContext);
const { id, theme } = useParams(); const { id, theme } = useParams();
@ -15,22 +11,6 @@ export default function ViewPage() {
setTabId(id!); setTabId(id!);
}, [id]); }, [id]);
useEffect(() => {
if (theme) {
setDark(theme === "dark");
} else {
setDark(false);
}
}, [theme]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return ( return (
<div className="flow-page-positioning"> <div className="flow-page-positioning">
{flows.length > 0 && {flows.length > 0 &&

Some files were not shown because too many files have changed in this diff Show more