Add new files and update existing files
This commit is contained in:
parent
b5c4e2dbc8
commit
2f8cb0d776
135 changed files with 13454 additions and 3508 deletions
2
src/frontend/.gitignore
vendored
2
src/frontend/.gitignore
vendored
|
|
@ -22,5 +22,5 @@ npm-debug.log*
|
|||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/playwright-report/*/
|
||||
/playwright/.cache/
|
||||
|
|
|
|||
599
src/frontend/harFiles/backend_12112023.har
Normal file
599
src/frontend/harFiles/backend_12112023.har
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2144
src/frontend/package-lock.json
generated
2144
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -111,13 +111,13 @@
|
|||
"@types/uuid": "^9.0.2",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"autoprefixer": "^10.4.15",
|
||||
"daisyui": "^3.6.3",
|
||||
"daisyui": "^4.0.4",
|
||||
"postcss": "^8.4.29",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"prettier-plugin-tailwindcss": "^0.3.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.4.9"
|
||||
"vite": "^4.5.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
src/frontend/playwright-report/index.html
Normal file
18
src/frontend/playwright-report/index.html
Normal 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>
|
||||
|
|
@ -20,11 +20,13 @@ export default defineConfig({
|
|||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* 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. */
|
||||
use: {
|
||||
/* 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 */
|
||||
trace: "on-first-retry",
|
||||
|
|
@ -69,9 +71,16 @@ export default defineConfig({
|
|||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
// webServer: [
|
||||
// {
|
||||
// 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,
|
||||
// },
|
||||
// ],
|
||||
});
|
||||
|
|
|
|||
78
src/frontend/run-tests.sh
Executable file
78
src/frontend/run-tests.sh
Executable 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
|
||||
|
|
@ -16,8 +16,8 @@ import {
|
|||
FETCH_ERROR_MESSAGE,
|
||||
} from "./constants/constants";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { FlowsContext } from "./contexts/flowsContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import { TabsContext } from "./contexts/tabsContext";
|
||||
import { typesContext } from "./contexts/typesContext";
|
||||
import Router from "./routes";
|
||||
|
||||
|
|
@ -30,7 +30,7 @@ export default function App() {
|
|||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
const { hardReset } = useContext(TabsContext);
|
||||
const { hardReset } = useContext(FlowsContext);
|
||||
|
||||
const {
|
||||
errorData,
|
||||
|
|
@ -42,9 +42,7 @@ export default function App() {
|
|||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
setErrorData,
|
||||
loading,
|
||||
setLoading,
|
||||
} = useContext(alertContext);
|
||||
const navigate = useNavigate();
|
||||
const { fetchError } = useContext(typesContext);
|
||||
|
|
|
|||
|
|
@ -22,16 +22,25 @@ import PromptAreaComponent from "../../../../components/promptComponent";
|
|||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
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 {
|
||||
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,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
isValidConnection,
|
||||
scapedJSONStringfy,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import {
|
||||
nodeColors,
|
||||
|
|
@ -44,7 +53,6 @@ export default function ParameterComponent({
|
|||
left,
|
||||
id,
|
||||
data,
|
||||
setData,
|
||||
tooltipTitle,
|
||||
title,
|
||||
color,
|
||||
|
|
@ -53,14 +61,20 @@ export default function ParameterComponent({
|
|||
required = false,
|
||||
optionalHandle = null,
|
||||
info = "",
|
||||
proxy,
|
||||
showNode,
|
||||
index = "",
|
||||
}: ParameterComponentType): JSX.Element {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
|
||||
const { setErrorData, modalContextOpen } = useContext(alertContext);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
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;
|
||||
|
||||
|
|
@ -78,41 +92,114 @@ export default function ParameterComponent({
|
|||
|
||||
const groupedEdge = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setDataRef(data);
|
||||
}, [modalContextOpen]);
|
||||
|
||||
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
|
||||
false;
|
||||
reactFlowInstance
|
||||
?.getEdges()
|
||||
.some(
|
||||
(edge) =>
|
||||
edge.targetHandle ===
|
||||
scapedJSONStringfy(proxy ? { ...id, proxy } : id)
|
||||
) ?? false;
|
||||
|
||||
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 = (
|
||||
newValue: string | string[] | boolean | Object[]
|
||||
): void => {
|
||||
let newData = cloneDeep(data);
|
||||
newData.node!.template[name].value = newValue;
|
||||
setData(newData);
|
||||
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;
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Set state to pending
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
if (!prev[tabId]) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
...prev[tabId],
|
||||
isPending: true,
|
||||
formKeysData: prev[tabId].formKeysData,
|
||||
},
|
||||
};
|
||||
});
|
||||
if (data.node!.template[name].value !== newValue) {
|
||||
const tabs = cloneDeep(tabsState);
|
||||
tabs[tabId].isPending = false;
|
||||
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
|
||||
setTabsState({
|
||||
...tabs,
|
||||
});
|
||||
}
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (name === "openai_api_base") console.log(info);
|
||||
// @ts-ignore
|
||||
infoHtml.current = (
|
||||
<div className="h-full w-full break-words">
|
||||
|
|
@ -166,21 +253,40 @@ export default function ParameterComponent({
|
|||
</div>
|
||||
<span className="ps-2 text-xs text-foreground">
|
||||
{nodeNames[item.family] ?? "Other"}{" "}
|
||||
<span className="text-xs">
|
||||
{" "}
|
||||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
{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">
|
||||
{" "}
|
||||
{item.type === "" ? "" : " - "}
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, index) => (
|
||||
<React.Fragment key={el + index}>
|
||||
<span>
|
||||
{index === item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
</React.Fragment>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -197,45 +303,43 @@ export default function ParameterComponent({
|
|||
}, [tooltipTitle, flow]);
|
||||
|
||||
return !showNode ? (
|
||||
left &&
|
||||
(type === "str" ||
|
||||
type === "bool" ||
|
||||
type === "float" ||
|
||||
type === "code" ||
|
||||
type === "prompt" ||
|
||||
type === "file" ||
|
||||
type === "int" ||
|
||||
type === "dict" ||
|
||||
type === "NestedDict") &&
|
||||
!optionalHandle ? (
|
||||
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||
<></>
|
||||
) : (
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||
<div className="flex">
|
||||
<ShadTooltip
|
||||
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
|
||||
"h-3 w-3 rounded-full border-2 bg-background"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
onClick={() => {
|
||||
setFilterEdge(groupedEdge.current);
|
||||
}}
|
||||
></Handle>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<div
|
||||
|
|
@ -250,7 +354,13 @@ export default function ParameterComponent({
|
|||
(info !== "" ? " flex items-center" : "")
|
||||
}
|
||||
>
|
||||
{title}
|
||||
{proxy ? (
|
||||
<ShadTooltip content={<span>{proxy.id}</span>}>
|
||||
<span>{title}</span>
|
||||
</ShadTooltip>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
<span className="text-status-red">{required ? " *" : ""}</span>
|
||||
<div className="">
|
||||
{info !== "" && (
|
||||
|
|
@ -266,17 +376,7 @@ export default function ParameterComponent({
|
|||
)}
|
||||
</div>
|
||||
</div>
|
||||
{left &&
|
||||
(type === "str" ||
|
||||
type === "bool" ||
|
||||
type === "float" ||
|
||||
type === "code" ||
|
||||
type === "prompt" ||
|
||||
type === "file" ||
|
||||
type === "int" ||
|
||||
type === "dict" ||
|
||||
type === "NestedDict") &&
|
||||
!optionalHandle ? (
|
||||
{left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
|
||||
<></>
|
||||
) : (
|
||||
<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
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
id={
|
||||
proxy
|
||||
? scapedJSONStringfy({ ...id, proxy })
|
||||
: scapedJSONStringfy(id)
|
||||
}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance!)
|
||||
}
|
||||
|
|
@ -331,9 +435,12 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + index}
|
||||
data-testid={"textarea-" + index}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
disabled={disabled}
|
||||
password={data.node?.template[name].password ?? false}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
|
|
@ -344,6 +451,7 @@ export default function ParameterComponent({
|
|||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<ToggleShadComponent
|
||||
id={"toggle-" + index}
|
||||
disabled={disabled}
|
||||
enabled={data.node?.template[name].value ?? false}
|
||||
setEnabled={(isEnabled) => {
|
||||
|
|
@ -357,30 +465,49 @@ export default function ParameterComponent({
|
|||
<FloatComponent
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
rangeSpec={data.node?.template[name].rangeSpec}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
data.node?.template[name].options ? (
|
||||
<div className="mt-2 w-full">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
// TODO: Improve CSS
|
||||
<div className="mt-2 flex w-full items-center">
|
||||
<div className="w-5/6 flex-grow">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
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>
|
||||
) : left === true && type === "code" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
data.node?.flow && data.node.template[name].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={data.node?.template[name].dynamic ?? false}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
setNodeClass={handleNodeClass}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"code-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "file" ? (
|
||||
|
|
@ -390,7 +517,6 @@ export default function ParameterComponent({
|
|||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node?.template[name].fileTypes}
|
||||
suffixes={data.node?.template[name].suffixes}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node!.template[name].file_path = filePath;
|
||||
}}
|
||||
|
|
@ -402,24 +528,21 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"int-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
<div className="mt-2 w-full">
|
||||
<PromptAreaComponent
|
||||
readonly={data.node?.flow ? true : false}
|
||||
field_name={name}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
const clone = cloneDeep(data);
|
||||
clone.node = nodeClass;
|
||||
setData(clone);
|
||||
}}
|
||||
setNodeClass={handleNodeClass}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
value={data.node?.template[name].value ?? ""}
|
||||
onChange={(e) => {
|
||||
handleOnNewValue(e);
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
id={"prompt-input-" + index}
|
||||
data-testid={"prompt-input-" + index}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "NestedDict" ? (
|
||||
|
|
@ -439,6 +562,7 @@ export default function ParameterComponent({
|
|||
data.node!.template[name].value = newValue;
|
||||
handleOnNewValue(newValue);
|
||||
}}
|
||||
id="div-dict-input"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "dict" ? (
|
||||
|
|
@ -447,10 +571,10 @@ export default function ParameterComponent({
|
|||
disabled={disabled}
|
||||
editNode={false}
|
||||
value={
|
||||
data.node!.template[name].value?.length === 0 ||
|
||||
!data.node!.template[name].value
|
||||
dataRef.node!.template[name].value?.length === 0 ||
|
||||
!dataRef.node!.template[name].value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(data.node!.template[name].value)
|
||||
: convertObjToArray(dataRef.node!.template[name].value)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
|
|
|
|||
|
|
@ -1,38 +1,54 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
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 { TabsContext } from "../../contexts/tabsContext";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { undoRedoContext } from "../../contexts/undoRedoContext";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
import { validationStatusType } from "../../types/components";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { cleanEdges } from "../../utils/reactflowUtils";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, toTitleCase } from "../../utils/utils";
|
||||
import { classNames, cn, getFieldTitle } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
data: olddata,
|
||||
data,
|
||||
xPos,
|
||||
yPos,
|
||||
selected,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
selected: boolean;
|
||||
xPos: number;
|
||||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const [data, setData] = useState(olddata);
|
||||
const { updateFlow, flows, tabId } = useContext(TabsContext);
|
||||
const { updateFlow, flows, tabId, saveCurrentFlow } =
|
||||
useContext(FlowsContext);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
|
||||
useContext(typesContext);
|
||||
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] =
|
||||
useState<validationStatusType | null>(null);
|
||||
const [showNode, setShowNode] = useState<boolean>(true);
|
||||
const [handles, setHandles] = useState<boolean[] | []>([]);
|
||||
let numberOfInputs: boolean[] = [];
|
||||
const { modalContextOpen } = useContext(alertContext);
|
||||
|
||||
const { takeSnapshot } = useContext(undoRedoContext);
|
||||
|
||||
function countHandles(): void {
|
||||
numberOfInputs = Object.keys(data.node!.template)
|
||||
|
|
@ -65,37 +81,18 @@ export default function GenericNode({
|
|||
|
||||
useEffect(() => {
|
||||
countHandles();
|
||||
}, []);
|
||||
}, [data, data.node]);
|
||||
|
||||
// State for outline color
|
||||
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(() => {
|
||||
setTimeout(() => {
|
||||
updateNodeInternals(data.id);
|
||||
}, 300);
|
||||
}, [showNode]);
|
||||
setNodeDescription(data.node!.description);
|
||||
}, [data.node!.description]);
|
||||
|
||||
useEffect(() => {
|
||||
setNodeName(data.node!.display_name);
|
||||
}, [data.node!.display_name]);
|
||||
|
||||
// New useEffect to watch for changes in sseData and update validation status
|
||||
useEffect(() => {
|
||||
|
|
@ -107,14 +104,25 @@ export default function GenericNode({
|
|||
setValidationStatus(null);
|
||||
}
|
||||
}, [sseData, data.id]);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
|
||||
const nameEditable = data.node?.flow || data.type === "CustomComponent";
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeToolbar>
|
||||
<NodeToolbarComponent
|
||||
position={{ x: xPos, y: yPos }}
|
||||
data={data}
|
||||
setData={setData}
|
||||
deleteNode={deleteNode}
|
||||
setShowNode={setShowNode}
|
||||
deleteNode={(id) => {
|
||||
takeSnapshot();
|
||||
deleteNode(id);
|
||||
saveCurrentFlow();
|
||||
}}
|
||||
setShowNode={(show: boolean) => {
|
||||
data.showNode = show;
|
||||
}}
|
||||
numberOfHandles={handles}
|
||||
showNode={showNode}
|
||||
></NodeToolbarComponent>
|
||||
|
|
@ -123,10 +131,7 @@ export default function GenericNode({
|
|||
<div
|
||||
className={classNames(
|
||||
selected ? "border border-ring" : "border",
|
||||
" transition-transform ",
|
||||
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 ",
|
||||
showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
|
||||
"generic-node-div"
|
||||
)}
|
||||
>
|
||||
|
|
@ -137,6 +142,7 @@ export default function GenericNode({
|
|||
)}
|
||||
<div>
|
||||
<div
|
||||
data-testid={"div-generic-node"}
|
||||
className={
|
||||
"generic-node-div-title " +
|
||||
(!showNode
|
||||
|
|
@ -151,20 +157,55 @@ export default function GenericNode({
|
|||
}
|
||||
>
|
||||
<IconComponent
|
||||
name={name}
|
||||
name={data.node?.flow ? "group_components" : name}
|
||||
className={
|
||||
"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]]}`}
|
||||
/>
|
||||
{showNode && (
|
||||
<div className="generic-node-tooltip-div">
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div className="generic-node-tooltip-div text-primary">
|
||||
{data.node?.display_name}
|
||||
{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>
|
||||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="flex"
|
||||
onDoubleClick={() => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
>
|
||||
<div className="generic-node-tooltip-div pr-2 text-primary">
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
{nameEditable && (
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 text-ring"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -178,18 +219,16 @@ export default function GenericNode({
|
|||
data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced && (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
index={idx.toString()}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
|
|
@ -199,15 +238,10 @@ export default function GenericNode({
|
|||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node?.template[templateField].display_name
|
||||
? data.node.template[templateField].display_name
|
||||
: data.node?.template[templateField].name
|
||||
? toTitleCase(
|
||||
data.node.template[templateField].name
|
||||
)
|
||||
: toTitleCase(templateField)
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
|
|
@ -217,33 +251,32 @@ export default function GenericNode({
|
|||
data.node?.template[templateField].type
|
||||
}
|
||||
required={
|
||||
data.node?.template[templateField].required
|
||||
}
|
||||
id={
|
||||
(data.node?.template[
|
||||
templateField
|
||||
].input_types?.join(";") ??
|
||||
data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
data.node!.template[templateField].required
|
||||
}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<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}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={
|
||||
data.node?.output_types &&
|
||||
|
|
@ -252,9 +285,11 @@ export default function GenericNode({
|
|||
: data.type
|
||||
}
|
||||
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("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
|
|
@ -282,9 +317,9 @@ export default function GenericNode({
|
|||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{typeof validationStatus.params === "string"
|
||||
? validationStatus.params
|
||||
? `Duration: ${validationStatus.duration}\n${validationStatus.params}`
|
||||
.split("\n")
|
||||
.map((line: string, index: number) => (
|
||||
.map((line, index) => (
|
||||
<div key={index}>{line}</div>
|
||||
))
|
||||
: ""}
|
||||
|
|
@ -329,54 +364,102 @@ export default function GenericNode({
|
|||
<div
|
||||
className={
|
||||
showNode
|
||||
? "generic-node-desc " +
|
||||
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
|
||||
? "overflow-hidden " +
|
||||
(data.node?.description === "" && !nameEditable
|
||||
? "pb-5"
|
||||
: "py-5")
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{data.node?.description !== "" && showNode && (
|
||||
<div className="generic-node-desc-text">
|
||||
{data.node?.description}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="generic-node-desc">
|
||||
{showNode && nameEditable && inputDescription ? (
|
||||
<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>
|
||||
<>
|
||||
{Object.keys(data.node!.template)
|
||||
.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) => (
|
||||
<div key={idx}>
|
||||
{data.node!.template[templateField].show &&
|
||||
!data.node!.template[templateField].advanced ? (
|
||||
<ParameterComponent
|
||||
key={
|
||||
(data.node!.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node!.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
index={idx.toString()}
|
||||
key={scapedJSONStringfy({
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node?.template[templateField].display_name
|
||||
? data.node.template[templateField].display_name
|
||||
: data.node?.template[templateField].name
|
||||
? toTitleCase(
|
||||
data.node.template[templateField].name
|
||||
)
|
||||
: toTitleCase(templateField)
|
||||
}
|
||||
title={getFieldTitle(
|
||||
data.node?.template!,
|
||||
templateField
|
||||
)}
|
||||
info={data.node?.template[templateField].info}
|
||||
name={templateField}
|
||||
tooltipTitle={
|
||||
|
|
@ -384,21 +467,20 @@ export default function GenericNode({
|
|||
"\n"
|
||||
) ?? data.node?.template[templateField].type
|
||||
}
|
||||
required={data.node?.template[templateField].required}
|
||||
id={
|
||||
(data.node?.template[templateField].input_types?.join(
|
||||
";"
|
||||
) ?? data.node?.template[templateField].type) +
|
||||
"|" +
|
||||
templateField +
|
||||
"|" +
|
||||
data.id
|
||||
}
|
||||
required={data.node!.template[templateField].required}
|
||||
id={{
|
||||
inputTypes:
|
||||
data.node!.template[templateField].input_types,
|
||||
type: data.node!.template[templateField].type,
|
||||
id: data.id,
|
||||
fieldName: templateField,
|
||||
}}
|
||||
left={true}
|
||||
type={data.node?.template[templateField].type}
|
||||
optionalHandle={
|
||||
data.node?.template[templateField].input_types
|
||||
}
|
||||
proxy={data.node?.template[templateField].proxy}
|
||||
showNode={showNode}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -414,22 +496,37 @@ export default function GenericNode({
|
|||
>
|
||||
{" "}
|
||||
</div>
|
||||
<ParameterComponent
|
||||
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={
|
||||
data.node?.output_types && data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
id={[data.type, data.id, ...data.node!.base_classes].join("|")}
|
||||
type={data.node?.base_classes.join("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
/>
|
||||
{data.node!.base_classes.length > 0 && (
|
||||
<ParameterComponent
|
||||
key={scapedJSONStringfy({
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
})}
|
||||
data={data}
|
||||
color={
|
||||
(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={
|
||||
data.node?.output_types && data.node.output_types.length > 0
|
||||
? data.node.output_types.join("|")
|
||||
: data.type
|
||||
}
|
||||
tooltipTitle={data.node?.base_classes.join("\n")}
|
||||
id={{
|
||||
baseClasses: data.node!.base_classes,
|
||||
id: data.id,
|
||||
dataType: data.type,
|
||||
}}
|
||||
type={data.node?.base_classes.join("|")}
|
||||
left={false}
|
||||
showNode={showNode}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default function AlertDropdown({
|
|||
}}
|
||||
>
|
||||
<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">
|
||||
Notifications
|
||||
<div className="flex gap-3 pr-3 ">
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export default function ErrorAlert({
|
|||
removeAlert(id);
|
||||
}, 500);
|
||||
}}
|
||||
className="error-build-message"
|
||||
className="error-build-message nocopy nopan nodelete nodrag noundo"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export default function NoticeAlert({
|
|||
setShow(false);
|
||||
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-shrink-0">
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function SuccessAlert({
|
|||
setShow(false);
|
||||
removeAlert(id);
|
||||
}}
|
||||
className="success-alert"
|
||||
className="success-alert nocopy nopan nodelete nodrag noundo"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export default function DropdownButton({
|
|||
<DropdownMenu open={showOptions}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
id="new-project-btn"
|
||||
variant="primary"
|
||||
className="relative pr-10"
|
||||
onClick={(event) => {
|
||||
|
|
|
|||
|
|
@ -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 { Label } from "../../components/ui/label";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { readFlowsFromDatabase } from "../../controllers/API";
|
||||
import { InputProps } from "../../types/components";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
export const EditFlowSettings: React.FC<InputProps> = ({
|
||||
name,
|
||||
invalidName,
|
||||
setInvalidName,
|
||||
invalidNameList,
|
||||
description,
|
||||
maxLength = 50,
|
||||
flows,
|
||||
tabId,
|
||||
setName,
|
||||
setDescription,
|
||||
}: InputProps): JSX.Element => {
|
||||
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 { value } = event.target;
|
||||
|
|
@ -34,63 +22,74 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
} else {
|
||||
setIsMaxLength(false);
|
||||
}
|
||||
if (invalidName !== undefined) {
|
||||
if (!nameLists.current.includes(value)) {
|
||||
setInvalidName!(false);
|
||||
} else {
|
||||
setInvalidName!(true);
|
||||
}
|
||||
|
||||
if (!nameLists.current.includes(value)) {
|
||||
setInvalidName!(false);
|
||||
} else {
|
||||
setInvalidName!(true);
|
||||
}
|
||||
}
|
||||
setName(value);
|
||||
setName!(value);
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Label>
|
||||
<div className="edit-flow-arrangement">
|
||||
<span className="font-medium">Name</span>{" "}
|
||||
<span className="font-medium">Name{setName ? "" : ":"}</span>{" "}
|
||||
{isMaxLength && (
|
||||
<span className="edit-flow-span">Character limit reached</span>
|
||||
)}
|
||||
{invalidName && (
|
||||
<span className="edit-flow-span">Name already in use</span>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
|
||||
onChange={handleNameChange}
|
||||
type="text"
|
||||
name="name"
|
||||
value={name ?? ""}
|
||||
placeholder="File name"
|
||||
id="name"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
{setName ? (
|
||||
<Input
|
||||
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
|
||||
onChange={handleNameChange}
|
||||
type="text"
|
||||
name="name"
|
||||
value={name ?? ""}
|
||||
placeholder="Flow name"
|
||||
id="name"
|
||||
maxLength={maxLength}
|
||||
onDoubleClickCapture={(event) => {
|
||||
handleFocus(event);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-normal text-muted-foreground word-break-break-word">
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
<Label>
|
||||
<div className="edit-flow-arrangement mt-3">
|
||||
<span className="font-medium ">Description (optional)</span>
|
||||
<span className="font-medium ">
|
||||
Description{setDescription ? " (optional)" : ":"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={handleDescriptionChange}
|
||||
value={description!}
|
||||
placeholder="Flow description"
|
||||
className="mt-2 max-h-[100px] font-normal"
|
||||
rows={3}
|
||||
/>
|
||||
{setDescription ? (
|
||||
<Textarea
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={handleDescriptionChange}
|
||||
value={description!}
|
||||
placeholder="Flow description"
|
||||
className="mt-2 max-h-[100px] font-normal"
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,11 +11,12 @@ import IconComponent from "../genericIconComponent";
|
|||
import { Button } from "../ui/button";
|
||||
|
||||
export default function PaginatorComponent({
|
||||
pageSize = 10,
|
||||
pageSize = 12,
|
||||
pageIndex = 1,
|
||||
rowsCount = [10, 20, 50, 100],
|
||||
rowsCount = [12, 24, 48, 96],
|
||||
totalRowsCount = 0,
|
||||
paginate,
|
||||
storeComponent = false,
|
||||
}: PaginatorComponentType) {
|
||||
const [size, setPageSize] = useState(pageSize);
|
||||
const [maxIndex, setMaxPageIndex] = useState(
|
||||
|
|
@ -30,7 +31,13 @@ export default function PaginatorComponent({
|
|||
<>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<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">
|
||||
<p className="text-sm font-medium">Rows per page</p>
|
||||
<Select
|
||||
|
|
@ -54,7 +61,8 @@ export default function PaginatorComponent({
|
|||
</Select>
|
||||
</div>
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {pageIndex} of {maxIndex}
|
||||
Page {pageIndex}
|
||||
{!storeComponent && <> of {maxIndex}</>}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const SanitizedHTMLWrapper = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
data-testid="edit-prompt-sanitized"
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
|
||||
suppressContentEditableWarning={suppressWarning}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { useContext } from "react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { cardComponentPropsType } from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
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 { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
|
|
@ -11,48 +19,326 @@ import {
|
|||
CardTitle,
|
||||
} from "../ui/card";
|
||||
|
||||
export const CardComponent = ({
|
||||
flow,
|
||||
id,
|
||||
onDelete,
|
||||
export default function CollectionCardComponent({
|
||||
data,
|
||||
authorized = true,
|
||||
disabled = false,
|
||||
button,
|
||||
}: cardComponentPropsType): JSX.Element => {
|
||||
const { removeFlow } = useContext(TabsContext);
|
||||
onDelete,
|
||||
}: {
|
||||
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 (
|
||||
<Card className="group">
|
||||
<CardHeader>
|
||||
<CardTitle className="card-component-title-display">
|
||||
<span
|
||||
className={
|
||||
"card-component-image " +
|
||||
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
|
||||
}
|
||||
></span>
|
||||
<span className="card-component-title-size">{flow.name}</span>
|
||||
{onDelete && (
|
||||
<button className="card-component-delete-button" onClick={onDelete}>
|
||||
<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>
|
||||
<div>
|
||||
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="card-component-delete-icon"
|
||||
className={cn(
|
||||
"flex-shrink-0",
|
||||
data.is_component
|
||||
? "mx-0.5 h-6 w-6 text-component-icon"
|
||||
: "h-7 w-7 flex-shrink-0 text-flow-icon"
|
||||
)}
|
||||
name={data.is_component ? "ToyBrick" : "Group"}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="card-component-desc">
|
||||
<div className="card-component-desc-text">
|
||||
{flow.description}
|
||||
{/* {flow.description} */}
|
||||
<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
|
||||
name="Trash2"
|
||||
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
|
||||
/>
|
||||
</DeleteConfirmationModal>
|
||||
)}
|
||||
</CardTitle>
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
{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>
|
||||
</CardHeader>
|
||||
</div>
|
||||
|
||||
<CardFooter>
|
||||
<div className="card-component-footer-arrangement">
|
||||
<div className="card-component-footer"></div>
|
||||
{button && button}
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
63
src/frontend/src/components/cardsWrapComponent/index.tsx
Normal file
63
src/frontend/src/components/cardsWrapComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,9 +7,9 @@ import { typesContext } from "../../../contexts/typesContext";
|
|||
import { postBuildInit } from "../../../controllers/API";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../contexts/flowsContext";
|
||||
import { parsedDataType } from "../../../types/components";
|
||||
import { TabsState } from "../../../types/tabs";
|
||||
import { FlowsState } from "../../../types/tabs";
|
||||
import { validateNodes } from "../../../utils/reactflowUtils";
|
||||
import RadialProgressComponent from "../../RadialProgress";
|
||||
import IconComponent from "../../genericIconComponent";
|
||||
|
|
@ -26,7 +26,7 @@ export default function BuildTrigger({
|
|||
}): JSX.Element {
|
||||
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setTabsState } = useContext(TabsContext);
|
||||
const { setTabsState, saveFlow } = useContext(FlowsContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const [isIconTouched, setIsIconTouched] = useState(false);
|
||||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
|
|
@ -76,65 +76,59 @@ export default function BuildTrigger({
|
|||
}
|
||||
async function streamNodeData(flow: FlowType) {
|
||||
// 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 { flowId } = response.data;
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults: boolean[] = [];
|
||||
let finished = false;
|
||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
// Close the connection and finish
|
||||
finished = true;
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
eventSource.close();
|
||||
resolve(validationResults.every((result) => result));
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
//@ts-ignore
|
||||
setTabsState((old: FlowsState) => {
|
||||
return {
|
||||
...old,
|
||||
[flowId]: {
|
||||
...old[flowId],
|
||||
formKeysData: parsedData,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
setProgress(parsedData.progress);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: any) => {
|
||||
console.error("EventSource failed:", error);
|
||||
|
||||
if (error.data) {
|
||||
const parsedData = JSON.parse(error.data);
|
||||
setErrorData({ title: parsedData.error });
|
||||
setIsBuilding(false);
|
||||
}
|
||||
eventSource.close();
|
||||
|
||||
return;
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
setSuccessData({ title: parsedData.log });
|
||||
} else if (parsedData.input_keys !== undefined) {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
return {
|
||||
...old,
|
||||
[flowId]: {
|
||||
...old[flowId],
|
||||
formKeysData: parsedData,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
setProgress(parsedData.progress);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error: any) => {
|
||||
console.error("EventSource failed:", error);
|
||||
eventSource.close();
|
||||
if (error.data) {
|
||||
const parsedData = JSON.parse(error.data);
|
||||
setErrorData({ title: parsedData.error });
|
||||
setIsBuilding(false);
|
||||
}
|
||||
};
|
||||
// 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);
|
||||
reject(new Error("Streaming failed"));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData: parsedDataType) {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
|
|||
import ChatTrigger from "./chatTrigger";
|
||||
|
||||
import * as _ from "lodash";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import FormModal from "../../modals/formModal";
|
||||
import { NodeType } from "../../types/flow";
|
||||
|
|
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
|
|||
export default function Chat({ flow }: ChatType): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [canOpen, setCanOpen] = useState(false);
|
||||
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
|
||||
const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -36,7 +36,7 @@ export default function Chat({ flow }: ChatType): JSX.Element {
|
|||
// Define an async function within the useEffect hook
|
||||
const fetchBuildStatus = async () => {
|
||||
const response = await getBuildStatus(flow.id);
|
||||
setIsBuilt(response.built);
|
||||
setIsBuilt(response.data.built);
|
||||
};
|
||||
|
||||
// Call the async function
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export default function CodeAreaComponent({
|
|||
nodeClass,
|
||||
dynamic,
|
||||
setNodeClass,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: CodeAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(
|
||||
typeof value == "string" ? value : JSON.stringify(value)
|
||||
|
|
@ -30,6 +32,7 @@ export default function CodeAreaComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<CodeAreaModal
|
||||
readonly={readonly}
|
||||
dynamic={dynamic}
|
||||
value={myValue}
|
||||
nodeClass={nodeClass}
|
||||
|
|
@ -41,6 +44,8 @@ export default function CodeAreaComponent({
|
|||
>
|
||||
<div className="flex w-full items-center">
|
||||
<span
|
||||
id={id}
|
||||
data-testid={id}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node input-dialog"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import {
|
|||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { codeTabsPropsType } from "../../types/components";
|
||||
|
|
@ -63,7 +64,7 @@ export default function CodeTabsComponent({
|
|||
}, [flow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tweaks) {
|
||||
if (tweaks && data) {
|
||||
unselectAllNodes({
|
||||
data,
|
||||
updateNodes: (nodes) => {
|
||||
|
|
@ -242,24 +243,10 @@ export default function CodeTabsComponent({
|
|||
templateField.charAt(0) !== "_" &&
|
||||
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 === "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")
|
||||
.type
|
||||
)
|
||||
)
|
||||
.map((templateField, indx) => {
|
||||
return (
|
||||
|
|
@ -457,11 +444,6 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].fileTypes
|
||||
}
|
||||
suffixes={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].suffixes
|
||||
}
|
||||
onFileChange={(
|
||||
value: any
|
||||
) => {
|
||||
|
|
@ -490,6 +472,11 @@ export default function CodeTabsComponent({
|
|||
templateField
|
||||
].value
|
||||
}
|
||||
rangeSpec={
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].rangeSpec
|
||||
}
|
||||
onChange={(target) => {
|
||||
setData((old) => {
|
||||
let newInputList =
|
||||
|
|
@ -604,6 +591,14 @@ export default function CodeTabsComponent({
|
|||
].type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
|
|
@ -646,6 +641,14 @@ export default function CodeTabsComponent({
|
|||
<CodeAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
readonly={
|
||||
node.data.node?.flow &&
|
||||
node.data.node.template[
|
||||
templateField
|
||||
].dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
value={
|
||||
!node.data.node.template[
|
||||
templateField
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default function DictComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
}: DictComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -22,7 +23,6 @@ export default function DictComponent({
|
|||
}, [value]);
|
||||
|
||||
const ref = useRef(value);
|
||||
debugger;
|
||||
return (
|
||||
<div
|
||||
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
|
||||
value={ref.current}
|
||||
onChange={(obj) => {
|
||||
|
|
@ -46,6 +46,7 @@ export default function DictComponent({
|
|||
: "input-disable pointer-events-none cursor-pointer"
|
||||
}
|
||||
placeholder="Click to edit your dictionary..."
|
||||
data-testid="dict-input"
|
||||
/>
|
||||
</DictAreaModal>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export default function Dropdown({
|
|||
editNode = false,
|
||||
numberOfOptions = 0,
|
||||
apiModal = false,
|
||||
id = "",
|
||||
}: DropDownComponentType): JSX.Element {
|
||||
let [internalValue, setInternalValue] = useState(
|
||||
value === "" || !value ? "Choose an option" : value
|
||||
|
|
@ -31,15 +32,19 @@ export default function Dropdown({
|
|||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className={editNode ? "mt-1" : "relative mt-1"}>
|
||||
<div className={"relative mt-1"}>
|
||||
<Listbox.Button
|
||||
data-test={`${id ?? ""}`}
|
||||
className={
|
||||
editNode
|
||||
? "dropdown-component-outline"
|
||||
: "dropdown-component-false-outline"
|
||||
}
|
||||
>
|
||||
<span className="dropdown-component-display">
|
||||
<span
|
||||
className="dropdown-component-display"
|
||||
data-testid={`${id ?? ""}-display`}
|
||||
>
|
||||
{internalValue}
|
||||
</span>
|
||||
<span className={"dropdown-component-arrow"}>
|
||||
|
|
@ -63,7 +68,7 @@ export default function Dropdown({
|
|||
editNode
|
||||
? "dropdown-component-true-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) => (
|
||||
|
|
@ -86,6 +91,7 @@ export default function Dropdown({
|
|||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate "
|
||||
)}
|
||||
data-testid={`${option}-${id ?? ""}-option`}
|
||||
>
|
||||
{option}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ export default function FloatComponent({
|
|||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
rangeSpec,
|
||||
editNode = false,
|
||||
}: FloatComponentType): JSX.Element {
|
||||
const step = 0.1;
|
||||
const min = -2;
|
||||
const max = 2;
|
||||
|
||||
const step = rangeSpec?.step ?? 0.1;
|
||||
const min = rangeSpec?.min ?? -2;
|
||||
const max = rangeSpec?.max ?? 2;
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -23,6 +23,7 @@ export default function FloatComponent({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id="float-input"
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
|
|
@ -39,7 +40,9 @@ export default function FloatComponent({
|
|||
disabled={disabled}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
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.target.value);
|
||||
|
|
|
|||
|
|
@ -3,14 +3,31 @@ import { IconComponentProps } from "../../types/components";
|
|||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
||||
const ForwardedIconComponent = forwardRef(
|
||||
({ name, className, iconColor }: IconComponentProps, ref) => {
|
||||
(
|
||||
{
|
||||
name,
|
||||
className,
|
||||
iconColor,
|
||||
stroke,
|
||||
strokeWidth,
|
||||
id = "",
|
||||
}: IconComponentProps,
|
||||
ref
|
||||
) => {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
|
||||
const style = {
|
||||
strokeWidth: strokeWidth ?? 1.5,
|
||||
...(stroke && { stroke: stroke }),
|
||||
...(iconColor && { color: iconColor, stroke: stroke }),
|
||||
};
|
||||
|
||||
return (
|
||||
<TargetIcon
|
||||
strokeWidth={1.5}
|
||||
className={className}
|
||||
style={iconColor ? { color: iconColor } : {}}
|
||||
style={style}
|
||||
ref={ref}
|
||||
data-testid={id ? `${id}-${name}` : `icon-${name}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -8,7 +8,7 @@ import {
|
|||
DropdownMenuTrigger,
|
||||
} from "../../../ui/dropdown-menu";
|
||||
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
|
|
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
|
|||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
||||
const { addFlow } = useContext(TabsContext);
|
||||
const { addFlow } = useContext(FlowsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { undo, redo } = useContext(undoRedoContext);
|
||||
const [openSettings, setOpenSettings] = useState(false);
|
||||
|
|
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
|||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(undefined, true).then((id) => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
|
|
@ -38,9 +38,13 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
|
|||
|
||||
return (
|
||||
<div className="round-button-div">
|
||||
<Link to="/">
|
||||
<button
|
||||
onClick={() => {
|
||||
navigate(-1);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="ChevronLeft" className="w-4" />
|
||||
</Link>
|
||||
</button>
|
||||
<div className="header-menu-bar">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
|||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
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 IconComponent from "../genericIconComponent";
|
||||
import { Button } from "../ui/button";
|
||||
|
|
@ -22,17 +24,18 @@ import { Separator } from "../ui/separator";
|
|||
import MenuBar from "./components/menuBar";
|
||||
|
||||
export default function Header(): JSX.Element {
|
||||
const { flows, tabId } = useContext(TabsContext);
|
||||
const { flows, tabId } = useContext(FlowsContext);
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter } = useContext(alertContext);
|
||||
const location = useLocation();
|
||||
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
|
||||
const { hasStore } = useContext(StoreContext);
|
||||
const { stars, gradientIndex } = useContext(darkContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="header-arrangement">
|
||||
<div className="header-start-display">
|
||||
<div className="header-start-display lg:w-[30%]">
|
||||
<Link to="/">
|
||||
<span className="ml-4 text-2xl">⛓️</span>
|
||||
</Link>
|
||||
|
|
@ -45,14 +48,19 @@ export default function Header(): JSX.Element {
|
|||
<Link to="/">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={location.pathname === "/" ? "primary" : "secondary"}
|
||||
variant={
|
||||
location.pathname === "/flows" ||
|
||||
location.pathname === "/components"
|
||||
? "primary"
|
||||
: "secondary"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<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>
|
||||
</Link>
|
||||
<Link to="/community">
|
||||
{/* <Link to="/community">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={
|
||||
|
|
@ -63,18 +71,30 @@ export default function Header(): JSX.Element {
|
|||
<IconComponent name="Users2" className="h-4 w-4" />
|
||||
<div className="flex-1">Community Examples</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
<div className="header-end-division">
|
||||
<div className="header-end-division lg:w-[30%]">
|
||||
<div className="header-end-display">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="header-github-link"
|
||||
className="header-github-link gap-2"
|
||||
>
|
||||
<FaGithub className="mr-2 h-5 w-5" />
|
||||
Star
|
||||
<FaGithub className="h-5 w-5" />
|
||||
<div className="hidden lg:block">Star</div>
|
||||
<div className="header-github-display">{stars}</div>
|
||||
</a>
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
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 { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
export default function InputComponent({
|
||||
autoFocus = false,
|
||||
onBlur,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
|
|
@ -15,9 +17,11 @@ export default function InputComponent({
|
|||
editNode = false,
|
||||
placeholder = "Type something...",
|
||||
className,
|
||||
id = "",
|
||||
blurOnEnter = false,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -30,6 +34,10 @@ export default function InputComponent({
|
|||
{isForm ? (
|
||||
<Form.Control asChild>
|
||||
<Input
|
||||
id={"form-" + id}
|
||||
ref={refInput}
|
||||
onBlur={onBlur}
|
||||
autoFocus={autoFocus}
|
||||
type={password && !pwdVisible ? "password" : "text"}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
|
|
@ -47,15 +55,26 @@ export default function InputComponent({
|
|||
onChange={(e) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
onCopy={(e) => {
|
||||
e.preventDefault();
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.ctrlKey && e.key === "c") {
|
||||
// Perform any actions you need when Ctrl+C is detected
|
||||
}
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
) : (
|
||||
<Input
|
||||
id={id}
|
||||
ref={refInput}
|
||||
type="text"
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
className={classNames(
|
||||
|
|
@ -73,6 +92,7 @@ export default function InputComponent({
|
|||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { uploadFile } from "../../controllers/API";
|
||||
import { FileComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -9,7 +9,6 @@ export default function InputFileComponent({
|
|||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
suffixes,
|
||||
fileTypes,
|
||||
onFileChange,
|
||||
editNode = false,
|
||||
|
|
@ -17,7 +16,7 @@ export default function InputFileComponent({
|
|||
const [myValue, setMyValue] = useState(value);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { tabId } = useContext(TabsContext);
|
||||
const { tabId } = useContext(FlowsContext);
|
||||
|
||||
// Clear component state
|
||||
useEffect(() => {
|
||||
|
|
@ -29,8 +28,9 @@ export default function InputFileComponent({
|
|||
}, [disabled, onChange]);
|
||||
|
||||
function checkFileType(fileName: string): boolean {
|
||||
for (let index = 0; index < suffixes.length; index++) {
|
||||
if (fileName.endsWith(suffixes[index])) {
|
||||
if (fileTypes === undefined) return true;
|
||||
for (let index = 0; index < fileTypes.length; index++) {
|
||||
if (fileName.endsWith(fileTypes[index])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ export default function InputFileComponent({
|
|||
// Create a file input element
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = suffixes.join(",");
|
||||
input.accept = fileTypes?.join(",");
|
||||
input.style.display = "none"; // Hidden from view
|
||||
input.multiple = false; // Allow only one file selection
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect } from "react";
|
||||
import { FloatComponentType } from "../../types/components";
|
||||
import { IntComponentType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
|
|
@ -8,7 +8,8 @@ export default function IntComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: FloatComponentType): JSX.Element {
|
||||
id = "",
|
||||
}: IntComponentType): JSX.Element {
|
||||
const min = 0;
|
||||
|
||||
// Clear component state
|
||||
|
|
@ -21,6 +22,7 @@ export default function IntComponent({
|
|||
return (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
id={id}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key !== "Backspace" &&
|
||||
|
|
@ -34,6 +36,8 @@ export default function IntComponent({
|
|||
event.key !== "c" &&
|
||||
event.key !== "v" &&
|
||||
event.key !== "a" &&
|
||||
event.key !== "ArrowUp" &&
|
||||
event.key !== "ArrowDown" &&
|
||||
!/^[-]?\d*$/.test(event.key)
|
||||
) {
|
||||
event.preventDefault();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ export default function KeypairListComponent({
|
|||
disabled,
|
||||
editNode = false,
|
||||
duplicateKey,
|
||||
advanced = false,
|
||||
dataValue,
|
||||
}: KeyPairListComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -55,6 +53,10 @@ export default function KeypairListComponent({
|
|||
return (
|
||||
<div key={idx} className="flex w-full gap-2">
|
||||
<Input
|
||||
data-testid={
|
||||
editNode ? "editNodekeypair" + index : "keypair" + index
|
||||
}
|
||||
id={editNode ? "editNodekeypair" + index : "keypair" + index}
|
||||
type="text"
|
||||
value={key.trim()}
|
||||
className={classNames(
|
||||
|
|
@ -72,6 +74,16 @@ export default function KeypairListComponent({
|
|||
/>
|
||||
|
||||
<Input
|
||||
data-testid={
|
||||
editNode
|
||||
? "editNodekeypair" + (index + 100).toString()
|
||||
: "keypair" + (index + 100).toString()
|
||||
}
|
||||
id={
|
||||
editNode
|
||||
? "editNodekeypair" + (index + 100).toString()
|
||||
: "keypair" + (index + 100).toString()
|
||||
}
|
||||
type="text"
|
||||
value={obj[key]}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
|
|
@ -88,6 +100,16 @@ export default function KeypairListComponent({
|
|||
newInputList.push({ "": "" });
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={
|
||||
editNode
|
||||
? "editNodeplusbtn" + index.toString()
|
||||
: "plusbtn" + index.toString()
|
||||
}
|
||||
data-testid={
|
||||
editNode
|
||||
? "editNodeplusbtn" + index.toString()
|
||||
: "plusbtn" + index.toString()
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
|
|
@ -101,6 +123,16 @@ export default function KeypairListComponent({
|
|||
newInputList.splice(index, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
data-testid={
|
||||
editNode
|
||||
? "editNodeminusbtn" + index.toString()
|
||||
: "minusbtn" + index.toString()
|
||||
}
|
||||
id={
|
||||
editNode
|
||||
? "editNodeminusbtn" + index.toString()
|
||||
: "minusbtn" + index.toString()
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
|
|
|
|||
31
src/frontend/src/components/pageLayout/index.tsx
Normal file
31
src/frontend/src/components/pageLayout/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useEffect } from "react";
|
||||
|
||||
import { TypeModal } from "../../constants/enums";
|
||||
import { postValidatePrompt } from "../../controllers/API";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { PromptAreaComponentType } from "../../types/components";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -14,39 +13,32 @@ export default function PromptAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: PromptAreaComponentType) {
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: PromptAreaComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
onChange("");
|
||||
}
|
||||
}, [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 (
|
||||
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
|
||||
<GenericModal
|
||||
id={id}
|
||||
readonly={readonly}
|
||||
type={TypeModal.PROMPT}
|
||||
value={value}
|
||||
buttonText="Check & Save"
|
||||
modalTitle="Edit Prompt"
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
setValue={onChange}
|
||||
nodeClass={nodeClass}
|
||||
setNodeClass={setNodeClass}
|
||||
>
|
||||
<div className="flex w-full items-center">
|
||||
<span
|
||||
id={id}
|
||||
data-testid={id}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node input-dialog"
|
||||
|
|
@ -58,6 +50,7 @@ export default function PromptAreaComponent({
|
|||
</span>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
|
|
|
|||
47
src/frontend/src/components/sidebarComponent/index.tsx
Normal file
47
src/frontend/src/components/sidebarComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
src/frontend/src/components/stackedComponents/index.tsx
Normal file
30
src/frontend/src/components/stackedComponents/index.tsx
Normal 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;
|
||||
12
src/frontend/src/components/storeGuard/index.tsx
Normal file
12
src/frontend/src/components/storeGuard/index.tsx
Normal 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;
|
||||
};
|
||||
111
src/frontend/src/components/tagsSelectorComponent/index.tsx
Normal file
111
src/frontend/src/components/tagsSelectorComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ export default function TextAreaComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
}: TextAreaComponentType): JSX.Element {
|
||||
// Clear text area
|
||||
useEffect(() => {
|
||||
|
|
@ -19,28 +20,39 @@ export default function TextAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center">
|
||||
<Input
|
||||
<div
|
||||
className={
|
||||
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
|
||||
}
|
||||
>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finishing Editing"
|
||||
modalTitle="Edit Text"
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
className={editNode ? "input-edit-node" : ""}
|
||||
placeholder={"Type something..."}
|
||||
onChange={(event) => {
|
||||
onChange(event.target.value);
|
||||
setValue={(value: string) => {
|
||||
onChange(value);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<GenericModal
|
||||
type={TypeModal.TEXT}
|
||||
buttonText="Finishing Editing"
|
||||
modalTitle="Edit Text"
|
||||
value={value}
|
||||
setValue={(value: string) => {
|
||||
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 && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
|
|
@ -48,8 +60,8 @@ export default function TextAreaComponent({
|
|||
}
|
||||
/>
|
||||
)}
|
||||
</GenericModal>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export default function ToggleShadComponent({
|
|||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
id = "",
|
||||
}: ToggleComponentType): JSX.Element {
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
|
|
@ -29,6 +30,7 @@ export default function ToggleShadComponent({
|
|||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
|
||||
<Switch
|
||||
id={id}
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -14,12 +14,14 @@ const badgeVariants = cva(
|
|||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
"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: {
|
||||
sm: "h-4 text-xs",
|
||||
md: "h-5 text-sm",
|
||||
lg: "h-6 text-base",
|
||||
sq: "h-6 text-sm font-normal rounded-md",
|
||||
xq: "h-5 text-xs font-normal rounded-md",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,16 @@ const buttonVariants = cva(
|
|||
outline:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
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:
|
||||
"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",
|
||||
link: "underline-offset-4 hover:underline text-primary",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 py-2 px-4",
|
||||
sm: "h-9 px-3 rounded-md",
|
||||
xs: "py-1 px-1 rounded-md",
|
||||
lg: "h-11 px-8 rounded-md",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const Card = React.forwardRef<
|
|||
<div
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
79
src/frontend/src/components/ui/combobox.tsx
Normal file
79
src/frontend/src/components/ui/combobox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
155
src/frontend/src/components/ui/command.tsx
Normal file
155
src/frontend/src/components/ui/command.tsx
Normal 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,
|
||||
};
|
||||
|
|
@ -18,14 +18,14 @@ const SelectTrigger = React.forwardRef<
|
|||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
|
|
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
|
|||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -517,16 +517,19 @@ export const ADMIN_HEADER_TITLE = "Admin Page";
|
|||
export const ADMIN_HEADER_DESCRIPTION =
|
||||
"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.
|
||||
* @constant
|
||||
*
|
||||
*/
|
||||
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
|
||||
"/api/v1/validate/code",
|
||||
"/api/v1/custom_component",
|
||||
"/api/v1/validate/prompt",
|
||||
"http://localhost:7860/login",
|
||||
`${BASE_URL_API}validate/code`,
|
||||
`${BASE_URL_API}custom_component`,
|
||||
`${BASE_URL_API}validate/prompt`,
|
||||
`http://localhost:7860/login`,
|
||||
`${BASE_URL_API}api_key/store`,
|
||||
];
|
||||
|
||||
export const skipNodeUpdate = [
|
||||
|
|
@ -561,10 +564,6 @@ export const CONTROL_NEW_USER = {
|
|||
is_superuser: false,
|
||||
};
|
||||
|
||||
export const CONTROL_NEW_API_KEY = {
|
||||
apikeyname: "",
|
||||
};
|
||||
|
||||
export const tabsCode = [];
|
||||
|
||||
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 =
|
||||
"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 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 =
|
||||
"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"]);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ const initialValue: alertContextType = {
|
|||
pushNotificationList: () => {},
|
||||
clearNotificationList: () => {},
|
||||
removeFromNotificationList: () => {},
|
||||
modalContextOpen: false,
|
||||
setModalContextOpen: (open: boolean) => {},
|
||||
};
|
||||
|
||||
export const alertContext = createContext<alertContextType>(initialValue);
|
||||
|
|
@ -49,6 +51,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
const [notificationCenter, setNotificationCenter] = useState(false);
|
||||
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
|
||||
const [isTweakPage, setIsTweakPage] = useState<boolean>(false);
|
||||
const [modalContextOpen, setModalContextOpen] = useState<boolean>(false);
|
||||
const pushNotificationList = (notification: AlertItemType) => {
|
||||
setNotificationList((old) => {
|
||||
let newNotificationList = _.cloneDeep(old);
|
||||
|
|
@ -141,6 +144,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
setSuccessData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
modalContextOpen,
|
||||
setModalContextOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ const initialValue: AuthContextType = {
|
|||
authenticationErrorCount: 0,
|
||||
autoLogin: false,
|
||||
setAutoLogin: () => {},
|
||||
setApiKey: () => {},
|
||||
apiKey: null,
|
||||
storeApiKey: () => {},
|
||||
};
|
||||
|
||||
export const AuthContext = createContext<AuthContextType>(initialValue);
|
||||
|
|
@ -36,6 +39,10 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
const [userData, setUserData] = useState<Users | null>(null);
|
||||
const [autoLogin, setAutoLogin] = useState<boolean>(false);
|
||||
const { setLoading } = useContext(alertContext);
|
||||
const [apiKey, setApiKey] = useState<string | null>(
|
||||
cookies.get("apikey_tkn_lflw")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const storedAccessToken = cookies.get("access_tkn_lflw");
|
||||
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(() => {
|
||||
const isLoginPage = location.pathname.includes("login");
|
||||
|
||||
|
|
@ -74,7 +88,7 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
setLoading(false);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
|
||||
|
||||
function getAuthentication() {
|
||||
const storedRefreshToken = cookies.get("refresh_tkn_lflw");
|
||||
|
|
@ -94,6 +108,7 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
function logout() {
|
||||
cookies.remove("access_tkn_lflw", { path: "/" });
|
||||
cookies.remove("refresh_tkn_lflw", { path: "/" });
|
||||
cookies.remove("apikey_tkn_lflw", { path: "/" });
|
||||
setIsAdmin(false);
|
||||
setUserData(null);
|
||||
setAccessToken(null);
|
||||
|
|
@ -101,6 +116,11 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
setIsAuthenticated(false);
|
||||
}
|
||||
|
||||
function storeApiKey(apikey: string) {
|
||||
cookies.set("apikey_tkn_lflw", apikey, { path: "/" });
|
||||
setApiKey(apikey);
|
||||
}
|
||||
|
||||
return (
|
||||
// !! to convert string to boolean
|
||||
<AuthContext.Provider
|
||||
|
|
@ -118,6 +138,9 @@ export function AuthProvider({ children }): React.ReactElement {
|
|||
authenticationErrorCount: 0,
|
||||
setAutoLogin,
|
||||
autoLogin,
|
||||
setApiKey,
|
||||
apiKey,
|
||||
storeApiKey,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AxiosError } from "axios";
|
||||
import _ from "lodash";
|
||||
import _, { cloneDeep } from "lodash";
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
|
|
@ -8,12 +8,18 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
|
||||
import {
|
||||
Edge,
|
||||
Node,
|
||||
ReactFlowJsonObject,
|
||||
XYPosition,
|
||||
addEdge,
|
||||
} from "reactflow";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { skipNodeUpdate } from "../constants/constants";
|
||||
import {
|
||||
deleteFlowFromDatabase,
|
||||
downloadFlowsFromDatabase,
|
||||
getVersion,
|
||||
readFlowsFromDatabase,
|
||||
saveFlowToDatabase,
|
||||
updateFlowInDatabase,
|
||||
|
|
@ -21,27 +27,47 @@ import {
|
|||
} from "../controllers/API";
|
||||
import { APIClassType, APITemplateType } from "../types/api";
|
||||
import { tweakType } from "../types/components";
|
||||
import { FlowType, NodeDataType, NodeType } from "../types/flow";
|
||||
import { TabsContextType, TabsState } from "../types/tabs";
|
||||
import {
|
||||
FlowType,
|
||||
NodeDataType,
|
||||
NodeType,
|
||||
sourceHandleType,
|
||||
targetHandleType,
|
||||
} from "../types/flow";
|
||||
import { FlowsContextType, FlowsState } from "../types/tabs";
|
||||
import {
|
||||
addVersionToDuplicates,
|
||||
checkOldEdgesHandles,
|
||||
createFlowComponent,
|
||||
removeFileNameFromComponents,
|
||||
scapeJSONParse,
|
||||
scapedJSONStringfy,
|
||||
updateEdgesHandleIds,
|
||||
updateIds,
|
||||
updateTemplate,
|
||||
} from "../utils/reactflowUtils";
|
||||
import { getRandomDescription, getRandomName } from "../utils/utils";
|
||||
import {
|
||||
createRandomKey,
|
||||
getRandomDescription,
|
||||
getRandomName,
|
||||
} from "../utils/utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
import { AuthContext } from "./authContext";
|
||||
import { typesContext } from "./typesContext";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
const FlowsContextInitialValue: FlowsContextType = {
|
||||
tabId: "",
|
||||
setTabId: (index: string) => {},
|
||||
isLoading: true,
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: async (flowData?: any) => "",
|
||||
addFlow: async (
|
||||
newProject: boolean,
|
||||
flowData?: FlowType,
|
||||
override?: boolean
|
||||
) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
|
|
@ -55,7 +81,8 @@ const TabsContextInitialValue: TabsContextType = {
|
|||
lastCopiedSelection: null,
|
||||
setLastCopiedSelection: (selection: any) => {},
|
||||
tabsState: {},
|
||||
setTabsState: (state: TabsState) => {},
|
||||
setTabsState: (state: FlowsState) => {},
|
||||
saveCurrentFlow: () => {},
|
||||
getNodeId: (nodeType: string) => "",
|
||||
setTweak: (tweak: any) => {},
|
||||
getTweak: [],
|
||||
|
|
@ -63,29 +90,35 @@ const TabsContextInitialValue: TabsContextType = {
|
|||
selection: { nodes: any; edges: any },
|
||||
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>(
|
||||
TabsContextInitialValue
|
||||
export const FlowsContext = createContext<FlowsContextType>(
|
||||
FlowsContextInitialValue
|
||||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
export function FlowsProvider({ children }: { children: ReactNode }) {
|
||||
const { setErrorData, setNoticeData, setSuccessData } =
|
||||
useContext(alertContext);
|
||||
const { getAuthentication, isAuthenticated } = useContext(AuthContext);
|
||||
|
||||
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 [id, setId] = useState(uid());
|
||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||
const { reactFlowInstance, setData, data } = useContext(typesContext);
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState<{
|
||||
nodes: any;
|
||||
edges: any;
|
||||
} | null>(null);
|
||||
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||
const [tabsState, setTabsState] = useState<FlowsState>({});
|
||||
const [getTweak, setTweak] = useState<tweakType>([]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -103,9 +136,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
function refreshFlows() {
|
||||
setIsLoading(true);
|
||||
getTabsDataFromDB().then((DbData) => {
|
||||
if (DbData && Object.keys(templates).length > 0) {
|
||||
if (DbData) {
|
||||
try {
|
||||
processDBData(DbData);
|
||||
processFlows(DbData, false);
|
||||
updateStateWithDbData(DbData);
|
||||
setIsLoading(false);
|
||||
} 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 (getAuthentication() === true) {
|
||||
// get data from db
|
||||
//get tabs locally saved
|
||||
// let tabsData = getLocalStorageTabsData();
|
||||
refreshFlows();
|
||||
}
|
||||
}, [templates, getAuthentication()]);
|
||||
}, [getAuthentication(), tabId]);
|
||||
|
||||
function getTabsDataFromDB() {
|
||||
//get tabs from db
|
||||
return readFlowsFromDatabase();
|
||||
}
|
||||
|
||||
function processDBData(DbData: FlowType[]) {
|
||||
function processFlows(DbData: FlowType[], skipUpdate = true) {
|
||||
let savedComponents: { [key: string]: APIClassType } = {};
|
||||
DbData.forEach((flow: FlowType) => {
|
||||
try {
|
||||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
} catch (e) {}
|
||||
if (flow.data && flow.is_component) {
|
||||
(flow.data.nodes[0].data as NodeDataType).node!.display_name =
|
||||
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) {
|
||||
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) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555" };
|
||||
|
|
@ -156,26 +210,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
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) {
|
||||
node.data.node!.base_classes = template["base_classes"];
|
||||
}
|
||||
|
|
@ -187,11 +221,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
) {
|
||||
flow.data!.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
?.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
sourceHandleObject.baseClasses = template["base_classes"];
|
||||
edge.data.sourceHandle = sourceHandleObject;
|
||||
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -227,9 +262,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
flowName: string,
|
||||
flowDescription?: string
|
||||
) {
|
||||
let clonedFlow = cloneDeep(flow);
|
||||
removeFileNameFromComponents(clonedFlow);
|
||||
// create a data URI with the current flow data
|
||||
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
|
||||
|
|
@ -243,9 +284,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
setNoticeData({
|
||||
title: "Warning: Critical data, JSON file may include API keys.",
|
||||
});
|
||||
}
|
||||
|
||||
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.
|
||||
* The resulting JSON object is passed to the addFlow function.
|
||||
*/
|
||||
async function uploadFlow(
|
||||
newProject?: boolean,
|
||||
file?: File
|
||||
): Promise<String | undefined> {
|
||||
let id;
|
||||
if (file) {
|
||||
let text = await file.text();
|
||||
let fileData = JSON.parse(text);
|
||||
if (fileData.flows) {
|
||||
fileData.flows.forEach((flow: FlowType) => {
|
||||
id = addFlow(flow, newProject);
|
||||
});
|
||||
}
|
||||
// parse the text into a JSON object
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
|
||||
id = await addFlow(flow, newProject);
|
||||
} else {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
// add a change event listener to the file input
|
||||
id = await new Promise((resolve) => {
|
||||
async function uploadFlow({
|
||||
newProject,
|
||||
file,
|
||||
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;
|
||||
if (file) {
|
||||
let text = await file.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) {
|
||||
fileData.flows.forEach((flow: FlowType) => {
|
||||
id = addFlow(newProject, flow, undefined, position);
|
||||
});
|
||||
resolve("");
|
||||
} else {
|
||||
id = await addFlow(newProject, fileData, undefined, position);
|
||||
resolve(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
// add a change event listener to the file input
|
||||
input.onchange = async (e: Event) => {
|
||||
if (
|
||||
(e.target as HTMLInputElement).files![0].type === "application/json"
|
||||
) {
|
||||
const currentfile = (e.target as HTMLInputElement).files![0];
|
||||
let text = await currentfile.text();
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
const flowId = await addFlow(flow, newProject);
|
||||
resolve(flowId);
|
||||
let fileData: FlowType = await JSON.parse(text);
|
||||
|
||||
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
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
return id;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFlows() {
|
||||
|
|
@ -343,19 +405,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
|
||||
* @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);
|
||||
if (index >= 0) {
|
||||
deleteFlowFromDatabase(id).then(() => {
|
||||
setFlows(flows.filter((flow) => flow.id !== id));
|
||||
});
|
||||
await deleteFlowFromDatabase(id);
|
||||
//removes component from data if there is any
|
||||
setFlows(flows.filter((flow) => flow.id !== id));
|
||||
processFlows(flows.filter((flow) => flow.id !== id));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a new flow to the list of flows.
|
||||
* @param flow Optional flow to add.
|
||||
*/
|
||||
|
||||
function paste(
|
||||
selectionInstance: { nodes: Node[]; edges: Edge[] },
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
|
|
@ -376,7 +438,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const insidePosition = position.paneX
|
||||
? { 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) => {
|
||||
// Generate a unique node ID
|
||||
|
|
@ -404,19 +469,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
reactFlowInstance!.setNodes(nodes);
|
||||
|
||||
selectionInstance.edges.forEach((edge) => {
|
||||
selectionInstance.edges.forEach((edge: Edge) => {
|
||||
let source = idsMap[edge.source];
|
||||
let target = idsMap[edge.target];
|
||||
let sourceHandleSplitted = edge.sourceHandle!.split("|");
|
||||
let sourceHandle =
|
||||
sourceHandleSplitted[0] +
|
||||
"|" +
|
||||
source +
|
||||
"|" +
|
||||
sourceHandleSplitted.slice(2).join("|");
|
||||
let targetHandleSplitted = edge.targetHandle!.split("|");
|
||||
let targetHandle =
|
||||
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
let sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: source,
|
||||
});
|
||||
sourceHandleObject.id = source;
|
||||
|
||||
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 =
|
||||
"reactflow__edge-" +
|
||||
source +
|
||||
|
|
@ -431,12 +505,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
sourceHandle,
|
||||
targetHandle,
|
||||
id,
|
||||
data: cloneDeep(edge.data),
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
targetHandle.split("|")[0] === "Text"
|
||||
targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ",
|
||||
animated: targetHandle.split("|")[0] === "Text",
|
||||
animated: targetHandleObject.type === "Text",
|
||||
selected: false,
|
||||
},
|
||||
edges.map((edge) => ({ ...edge, selected: false }))
|
||||
|
|
@ -446,24 +521,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
const addFlow = async (
|
||||
newProject: Boolean,
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
override?: boolean,
|
||||
position?: XYPosition
|
||||
): Promise<String | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = extractDataFromFlow(flow!);
|
||||
if (flowData.description == "") {
|
||||
flowData.description = getRandomDescription();
|
||||
}
|
||||
let flowData = flow
|
||||
? processDataFromFlow(flow)
|
||||
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
|
||||
if (override) {
|
||||
deleteComponent(flow!.name);
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
const { id } = await saveFlowToDatabase(newFlow);
|
||||
newFlow.id = id;
|
||||
//setTimeout to prevent update state with wrong state
|
||||
setTimeout(() => {
|
||||
addFlowToLocalState(newFlow);
|
||||
}, 200);
|
||||
// addFlowToLocalState(newFlow);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
processFlowEdges(newFlow);
|
||||
processFlowNodes(newFlow);
|
||||
|
||||
const flowName = addVersionToDuplicates(newFlow, flows);
|
||||
|
||||
newFlow.name = flowName;
|
||||
const newName = addVersionToDuplicates(newFlow, flows);
|
||||
|
||||
newFlow.name = newName;
|
||||
try {
|
||||
const { id } = await saveFlowToDatabase(newFlow);
|
||||
// Change the id to the new id.
|
||||
|
|
@ -481,76 +568,59 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
} else {
|
||||
paste(
|
||||
{ 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;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
if (data) {
|
||||
processFlowEdges(flow);
|
||||
//prevent node update for now
|
||||
// processFlowNodes(flow);
|
||||
//add animation to text type edges
|
||||
updateEdges(data.edges);
|
||||
updateNodes(data.nodes, data.edges);
|
||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
// updateNodes(data.nodes, data.edges);
|
||||
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
}
|
||||
|
||||
return { data, description };
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateEdges = (edges: Edge[]) => {
|
||||
edges.forEach((edge) => {
|
||||
edge.className =
|
||||
(edge.targetHandle!.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ") + " stroke-connection";
|
||||
edge.animated = edge.targetHandle!.split("|")[0] === "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
|
||||
if (edges)
|
||||
edges.forEach((edge) => {
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
}
|
||||
});
|
||||
edge.className =
|
||||
(targetHandleObject.type === "Text"
|
||||
? "stroke-gray-800 "
|
||||
: "stroke-gray-900 ") + " stroke-connection";
|
||||
edge.animated = targetHandleObject.type === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const createNewFlow = (
|
||||
flowData: { data: ReactFlowJsonObject | null; description: string },
|
||||
flowData: ReactFlowJsonObject | null,
|
||||
flow: FlowType
|
||||
) => ({
|
||||
description: flowData.description,
|
||||
description: flow?.description ?? getRandomDescription(),
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
data: flowData,
|
||||
id: "",
|
||||
is_component: flow?.is_component ?? false,
|
||||
});
|
||||
|
||||
const addFlowToLocalState = (newFlow: FlowType) => {
|
||||
let newFlows: FlowType[] = [];
|
||||
setFlows((prevState) => {
|
||||
newFlows = newFlows.concat(prevState);
|
||||
newFlows.push(newFlow);
|
||||
return [...prevState, newFlow];
|
||||
});
|
||||
processFlows(newFlows);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -566,10 +636,20 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
newFlows[index].data = newFlow.data;
|
||||
newFlows[index].name = newFlow.name;
|
||||
}
|
||||
newFlow = {
|
||||
...newFlow,
|
||||
};
|
||||
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) {
|
||||
try {
|
||||
// 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);
|
||||
// Initialize state variable for the version
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
getVersion().then((data) => {
|
||||
setVersion(data.version);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
<FlowsContext.Provider
|
||||
value={{
|
||||
version,
|
||||
saveFlow,
|
||||
isBuilt,
|
||||
setIsBuilt,
|
||||
lastCopiedSelection,
|
||||
setLastCopiedSelection,
|
||||
saveCurrentFlow,
|
||||
hardReset,
|
||||
tabId,
|
||||
setTabId,
|
||||
|
|
@ -637,9 +742,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
getTweak,
|
||||
setTweak,
|
||||
isLoading,
|
||||
saveComponent,
|
||||
deleteComponent,
|
||||
nodesOnFlow,
|
||||
setNodesOnFlow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
</FlowsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
|
|||
import { AlertProvider } from "./alertContext";
|
||||
import { AuthProvider } from "./authContext";
|
||||
import { DarkProvider } from "./darkContext";
|
||||
import { FlowsProvider } from "./flowsContext";
|
||||
import { LocationProvider } from "./locationContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
import { StoreProvider } from "./storeContext";
|
||||
|
||||
import { TypesProvider } from "./typesContext";
|
||||
import { UndoRedoProvider } from "./undoRedoContext";
|
||||
|
||||
|
|
@ -26,9 +28,11 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
|
|||
<LocationProvider>
|
||||
<ApiInterceptor />
|
||||
<SSEProvider>
|
||||
<TabsProvider>
|
||||
<UndoRedoProvider>{children}</UndoRedoProvider>
|
||||
</TabsProvider>
|
||||
<FlowsProvider>
|
||||
<UndoRedoProvider>
|
||||
<StoreProvider>{children}</StoreProvider>
|
||||
</UndoRedoProvider>
|
||||
</FlowsProvider>
|
||||
</SSEProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
|
|
|
|||
74
src/frontend/src/contexts/storeContext.tsx
Normal file
74
src/frontend/src/contexts/storeContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import _ from "lodash";
|
||||
import {
|
||||
createContext,
|
||||
ReactNode,
|
||||
|
|
@ -5,7 +6,7 @@ import {
|
|||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Edge, Node, ReactFlowInstance } from "reactflow";
|
||||
import { ReactFlowInstance } from "reactflow";
|
||||
import { getAll, getHealth } from "../controllers/API";
|
||||
import { APIKindType } from "../types/api";
|
||||
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.
|
||||
if (isMounted && result?.status === 200) {
|
||||
setLoading(false);
|
||||
setData(result.data);
|
||||
let { data } = _.cloneDeep(result);
|
||||
setData((old) => ({ ...old, ...data }));
|
||||
setTemplates(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = result.data[curr][c];
|
||||
Object.keys(data).reduce((acc, curr) => {
|
||||
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
|
||||
//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;
|
||||
}, {})
|
||||
|
|
@ -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.
|
||||
setTypes(
|
||||
// Reverse the keys so the tool world does not overlap
|
||||
Object.keys(result.data)
|
||||
Object.keys(data)
|
||||
.reverse()
|
||||
.reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
|
|
@ -87,6 +90,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error("An error has occurred while fetching types.");
|
||||
console.log(error);
|
||||
await getHealth().catch((e) => {
|
||||
setFetchError(true);
|
||||
});
|
||||
|
|
@ -94,31 +98,25 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
|
||||
function deleteNode(idx: string | Array<string>) {
|
||||
reactFlowInstance!.setNodes(
|
||||
reactFlowInstance!
|
||||
.getNodes()
|
||||
.filter((node: Node) =>
|
||||
typeof idx === "string" ? node.id !== idx : !idx.includes(node.id)
|
||||
)
|
||||
);
|
||||
reactFlowInstance!.setEdges(
|
||||
reactFlowInstance!
|
||||
.getEdges()
|
||||
.filter((edge) =>
|
||||
typeof idx === "string"
|
||||
? edge.source !== idx && edge.target !== idx
|
||||
: !idx.includes(edge.source) && !idx.includes(edge.target)
|
||||
)
|
||||
);
|
||||
if (reactFlowInstance === null) return;
|
||||
const edges = reactFlowInstance!
|
||||
.getEdges()
|
||||
.filter((edge) =>
|
||||
typeof idx === "string"
|
||||
? edge.source == idx || edge.target == idx
|
||||
: 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>) {
|
||||
reactFlowInstance!.setEdges(
|
||||
reactFlowInstance!
|
||||
.getEdges()
|
||||
.filter((edge: Edge) =>
|
||||
typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id)
|
||||
)
|
||||
);
|
||||
reactFlowInstance!.deleteElements({
|
||||
edges:
|
||||
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
undoRedoContextType,
|
||||
} from "../types/typesContext";
|
||||
import { isWrappedWithClass } from "../utils/utils";
|
||||
import { TabsContext } from "./tabsContext";
|
||||
import { FlowsContext } from "./flowsContext";
|
||||
|
||||
const initialValue = {
|
||||
undo: () => {},
|
||||
|
|
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
|
|||
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||
|
||||
export function UndoRedoProvider({ children }) {
|
||||
const { tabId, flows } = useContext(TabsContext);
|
||||
const { tabId, flows } = useContext(FlowsContext);
|
||||
|
||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
|
|
@ -52,15 +52,23 @@ export function UndoRedoProvider({ children }) {
|
|||
|
||||
const takeSnapshot = useCallback(() => {
|
||||
// push the current graph to the past state
|
||||
setPast((old) => {
|
||||
let newPast = cloneDeep(old);
|
||||
newPast[tabIndex] = old[tabIndex].slice(
|
||||
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
|
||||
old[tabIndex].length
|
||||
let newPast = cloneDeep(past);
|
||||
let newState = {
|
||||
nodes: cloneDeep(getNodes()),
|
||||
edges: cloneDeep(getEdges()),
|
||||
};
|
||||
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() });
|
||||
return newPast;
|
||||
});
|
||||
newPast[tabIndex].push(newState);
|
||||
}
|
||||
setPast(newPast);
|
||||
|
||||
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
|
||||
setFuture((old) => {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
|
|||
import { api } from "../../controllers/API/api";
|
||||
import {
|
||||
APIObjectType,
|
||||
Component,
|
||||
LoginType,
|
||||
Users,
|
||||
changeUser,
|
||||
|
|
@ -12,6 +13,7 @@ import {
|
|||
} from "../../types/api/index";
|
||||
import { UserInputType } from "../../types/components";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { StoreComponentResponse } from "../../types/store";
|
||||
import {
|
||||
APIClassType,
|
||||
BuildStatusTypeAPI,
|
||||
|
|
@ -70,10 +72,10 @@ export async function postValidatePrompt(
|
|||
template: string,
|
||||
frontend_node: APIClassType
|
||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||
return await api.post(`${BASE_URL_API}validate/prompt`, {
|
||||
name: name,
|
||||
template: template,
|
||||
frontend_node: frontend_node,
|
||||
return api.post(`${BASE_URL_API}validate/prompt`, {
|
||||
name,
|
||||
template,
|
||||
frontend_node,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -112,12 +114,14 @@ export async function saveFlowToDatabase(newFlow: {
|
|||
data: ReactFlowJsonObject | null;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
is_component?: boolean;
|
||||
}): Promise<FlowType> {
|
||||
try {
|
||||
const response = await api.post(`${BASE_URL_API}flows/`, {
|
||||
name: newFlow.name,
|
||||
data: newFlow.data,
|
||||
description: newFlow.description,
|
||||
is_component: newFlow.is_component,
|
||||
});
|
||||
|
||||
if (response.status !== 201) {
|
||||
|
|
@ -353,7 +357,21 @@ export async function postCustomComponent(
|
|||
code: string,
|
||||
apiClass: 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) {
|
||||
|
|
@ -522,3 +540,323 @@ export async function deleteApiKey(api_key: string) {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/frontend/src/icons/AWS/AWS.jsx
Normal file
31
src/frontend/src/icons/AWS/AWS.jsx
Normal 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;
|
||||
38
src/frontend/src/icons/AWS/AWS.svg
Normal file
38
src/frontend/src/icons/AWS/AWS.svg
Normal 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 |
8
src/frontend/src/icons/AWS/index.tsx
Normal file
8
src/frontend/src/icons/AWS/index.tsx
Normal 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} />;
|
||||
}
|
||||
);
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { InfinityIcon } from "lucide-react";
|
||||
import { Code } from "lucide-react";
|
||||
import { forwardRef } from "react";
|
||||
import ForwardedIconComponent from "../../components/genericIconComponent";
|
||||
|
||||
export const GradientSparkles = forwardRef<
|
||||
export const GradientInfinity = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
|
|
@ -15,7 +16,63 @@ export const GradientSparkles = forwardRef<
|
|||
</linearGradient>
|
||||
</defs>
|
||||
</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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
9
src/frontend/src/icons/Share/Share.jsx
Normal file
9
src/frontend/src/icons/Share/Share.jsx
Normal 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;
|
||||
8
src/frontend/src/icons/Share/index.tsx
Normal file
8
src/frontend/src/icons/Share/index.tsx
Normal 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} />;
|
||||
}
|
||||
);
|
||||
3
src/frontend/src/icons/Share/share.svg
Normal file
3
src/frontend/src/icons/Share/share.svg
Normal 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 |
17
src/frontend/src/icons/Share2/Share2.jsx
Normal file
17
src/frontend/src/icons/Share2/Share2.jsx
Normal 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;
|
||||
9
src/frontend/src/icons/Share2/index.tsx
Normal file
9
src/frontend/src/icons/Share2/index.tsx
Normal 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} />;
|
||||
});
|
||||
3
src/frontend/src/icons/Share2/share2.svg
Normal file
3
src/frontend/src/icons/Share2/share2.svg
Normal 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 |
|
|
@ -13,9 +13,12 @@ import {
|
|||
// import "ace-builds/webpack-resolver";
|
||||
import CodeTabsComponent from "../../components/codeTabsComponent";
|
||||
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 { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
import { tweakType, uniqueTweakType } from "../../types/components";
|
||||
import { FlowType, NodeType } from "../../types/flow/index";
|
||||
|
|
@ -45,7 +48,7 @@ const ApiModal = forwardRef(
|
|||
const [activeTab, setActiveTab] = useState("0");
|
||||
const tweak = useRef<tweakType>([]);
|
||||
const tweaksList = useRef<string[]>([]);
|
||||
const { setTweak, getTweak, tabsState } = useContext(TabsContext);
|
||||
const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
|
||||
const pythonApiCode = getPythonApiCode(
|
||||
flow,
|
||||
autoLogin,
|
||||
|
|
@ -99,15 +102,9 @@ const ApiModal = forwardRef(
|
|||
(templateField) =>
|
||||
templateField.charAt(0) !== "_" &&
|
||||
node.data.node.template[templateField].show &&
|
||||
(node.data.node.template[templateField].type === "str" ||
|
||||
node.data.node.template[templateField].type === "bool" ||
|
||||
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")
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
node.data.node.template[templateField].type
|
||||
)
|
||||
)
|
||||
.map((n, i) => {
|
||||
arrNodesWithValues.push(node["id"]);
|
||||
|
|
@ -146,9 +143,9 @@ const ApiModal = forwardRef(
|
|||
);
|
||||
|
||||
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) => {
|
||||
if (element[tw] && Object.keys(element[tw])?.length === 0) {
|
||||
tweak.current = tweak.current.filter((obj) => {
|
||||
|
|
@ -161,7 +158,7 @@ const ApiModal = forwardRef(
|
|||
} else {
|
||||
const newTweak = {
|
||||
[tw]: {
|
||||
[template["name"]]: changes,
|
||||
[template["name"]!]: changes,
|
||||
},
|
||||
} as uniqueTweakType;
|
||||
tweak.current.push(newTweak);
|
||||
|
|
|
|||
|
|
@ -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 { ConfirmationModalType } from "../../types/components";
|
||||
import { ConfirmationModalType, ContentProps } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
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,
|
||||
asChild,
|
||||
titleHeader,
|
||||
modalContent,
|
||||
modalContentTitle,
|
||||
cancelText,
|
||||
confirmationText,
|
||||
children,
|
||||
destructive = false,
|
||||
icon,
|
||||
data,
|
||||
index,
|
||||
onConfirm,
|
||||
size,
|
||||
open,
|
||||
onClose,
|
||||
onCancel,
|
||||
}: ConfirmationModalType) {
|
||||
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 (
|
||||
<BaseModal size="x-small" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={titleHeader}>
|
||||
<BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
|
||||
<BaseModal.Trigger asChild={asChild}>{triggerChild}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={titleHeader ?? null}>
|
||||
<span className="pr-2">{title}</span>
|
||||
<Icon
|
||||
name="icon"
|
||||
|
|
@ -33,20 +68,21 @@ export default function ConfirmationModal({
|
|||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
{modalContentTitle != "" && (
|
||||
{modalContentTitle && modalContentTitle != "" && (
|
||||
<>
|
||||
<strong>{modalContentTitle}</strong>
|
||||
<br></br>
|
||||
</>
|
||||
)}
|
||||
<span>{modalContent}</span>
|
||||
{ContentChild}
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
className="ml-3"
|
||||
variant={destructive ? "destructive" : "default"}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
setModalOpen(false);
|
||||
onConfirm(index, data);
|
||||
}}
|
||||
>
|
||||
|
|
@ -54,9 +90,11 @@ export default function ConfirmationModal({
|
|||
</Button>
|
||||
|
||||
<Button
|
||||
className=""
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
if (onCancel) onCancel();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
{cancelText}
|
||||
|
|
@ -65,3 +103,7 @@ export default function ConfirmationModal({
|
|||
</BaseModal>
|
||||
);
|
||||
}
|
||||
ConfirmationModal.Content = Content;
|
||||
ConfirmationModal.Trigger = Trigger;
|
||||
|
||||
export default ConfirmationModal;
|
||||
|
|
|
|||
62
src/frontend/src/modals/DeleteConfirmationModal/index.tsx
Normal file
62
src/frontend/src/modals/DeleteConfirmationModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,7 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import {
|
||||
ReactNode,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { forwardRef, useContext, useEffect, useState } from "react";
|
||||
import { useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import DictComponent from "../../components/dictComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
|
|
@ -30,15 +25,20 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { limitScrollFieldsModal } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import {
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
limitScrollFieldsModal,
|
||||
} from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
import { FlowsState } from "../../types/tabs";
|
||||
import {
|
||||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
|
@ -47,66 +47,68 @@ const EditNodeModal = forwardRef(
|
|||
(
|
||||
{
|
||||
data,
|
||||
setData,
|
||||
nodeLength,
|
||||
children,
|
||||
open,
|
||||
onClose,
|
||||
setOpen,
|
||||
}: {
|
||||
data: NodeDataType;
|
||||
setData: (data: NodeDataType) => void;
|
||||
nodeLength: number;
|
||||
children: ReactNode;
|
||||
open?: boolean;
|
||||
onClose?: (close: boolean) => void;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
},
|
||||
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);
|
||||
let disabled =
|
||||
reactFlowInstance
|
||||
?.getEdges()
|
||||
.some((edge) => edge.targetHandle === data.id) ?? false;
|
||||
const { setModalContextOpen } = useContext(alertContext);
|
||||
|
||||
function changeAdvanced(n) {
|
||||
myData.current.node!.template[n].advanced =
|
||||
!myData.current.node!.template[n].advanced;
|
||||
setAdv(!adv);
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node!.template[n].advanced =
|
||||
!newData.node!.template[n].advanced;
|
||||
return newData;
|
||||
});
|
||||
}
|
||||
|
||||
const handleOnNewValue = (newValue: any, name) => {
|
||||
myData.current.node!.template[name].value = newValue;
|
||||
setDataValue(newValue);
|
||||
setMyData((old) => {
|
||||
let newData = cloneDeep(old);
|
||||
newData.node!.template[name].value = newValue;
|
||||
return newData;
|
||||
});
|
||||
updateNodeInternals(data.id);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
myData.current = data; // reset data to what it is on node when opening modal
|
||||
onClose!(modalOpen);
|
||||
}, [modalOpen]);
|
||||
if (open) {
|
||||
setMyData(data); // reset data to what it is on node when opening modal
|
||||
}
|
||||
setModalContextOpen(open);
|
||||
}, [open]);
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
const [adv, setAdv] = useState<boolean | null>(null);
|
||||
const [dataValue, setDataValue] = useState(data);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
key={data.id}
|
||||
size="large-h-full"
|
||||
open={modalOpen}
|
||||
setOpen={setModalOpen}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
onChangeOpenModal={(open) => {
|
||||
myData.current = data;
|
||||
setMyData(data);
|
||||
}}
|
||||
>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={myData.current.node?.description!}>
|
||||
<span className="pr-2">{myData.current.type}</span>
|
||||
<Badge variant="secondary">ID: {myData.current.id}</Badge>
|
||||
<BaseModal.Trigger>
|
||||
<></>
|
||||
</BaseModal.Trigger>
|
||||
<BaseModal.Header description={myData.node?.description!}>
|
||||
<span className="pr-2">{myData.type}</span>
|
||||
<Badge variant="secondary">ID: {myData.id}</Badge>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex pb-2">
|
||||
|
|
@ -123,7 +125,7 @@ const EditNodeModal = forwardRef(
|
|||
"edit-node-modal-box",
|
||||
nodeLength > limitScrollFieldsModal
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "overflow-hidden"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{nodeLength > 0 && (
|
||||
|
|
@ -139,349 +141,387 @@ const EditNodeModal = forwardRef(
|
|||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="p-0">
|
||||
{Object.keys(myData.current.node!.template)
|
||||
{Object.keys(myData.node!.template)
|
||||
.filter(
|
||||
(templateParam) =>
|
||||
templateParam.charAt(0) !== "_" &&
|
||||
myData.current.node?.template[templateParam].show &&
|
||||
(myData.current.node.template[templateParam]
|
||||
.type === "str" ||
|
||||
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")
|
||||
myData.node?.template[templateParam].show &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(
|
||||
myData.node.template[templateParam].type
|
||||
)
|
||||
)
|
||||
.map((templateParam, index) => (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
{myData.current.node?.template[templateParam].name
|
||||
? myData.current.node.template[templateParam]
|
||||
.name
|
||||
: myData.current.node?.template[templateParam]
|
||||
.display_name}
|
||||
</TableCell>
|
||||
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
{myData.current.node?.template[templateParam]
|
||||
.type === "str" &&
|
||||
!myData.current.node.template[templateParam]
|
||||
.options ? (
|
||||
<div className="mx-auto">
|
||||
{myData.current.node.template[templateParam]
|
||||
.list ? (
|
||||
<InputListComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!myData.current.node.template[
|
||||
templateParam
|
||||
].value ||
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value === ""
|
||||
? [""]
|
||||
: myData.current.node.template[
|
||||
templateParam
|
||||
].value
|
||||
.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,
|
||||
}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
: id
|
||||
)
|
||||
) ?? false;
|
||||
return (
|
||||
<TableRow key={index} className="h-10">
|
||||
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
|
||||
<ShadTooltip
|
||||
content={
|
||||
myData.node?.template[templateParam].proxy
|
||||
? myData.node?.template[templateParam]
|
||||
.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 className="w-[300px] p-0 text-center text-xs text-foreground ">
|
||||
{myData.node?.template[templateParam].type ===
|
||||
"str" &&
|
||||
!myData.node.template[templateParam].options ? (
|
||||
<div className="mx-auto">
|
||||
{myData.node.template[templateParam]
|
||||
.list ? (
|
||||
<InputListComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
!myData.node.template[templateParam]
|
||||
.value ||
|
||||
myData.node.template[templateParam]
|
||||
.value === ""
|
||||
? [""]
|
||||
: myData.node.template[
|
||||
templateParam
|
||||
].value
|
||||
}
|
||||
onChange={(value: string[]) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : myData.node.template[templateParam]
|
||||
.multiline ? (
|
||||
<TextAreaComponent
|
||||
id={"textarea-edit-" + index}
|
||||
data-testid={"textarea-edit-" + index}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(
|
||||
value: string | string[]
|
||||
) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
id={"input-" + index}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
password={
|
||||
myData.node.template[templateParam]
|
||||
.password ?? false
|
||||
}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(
|
||||
value,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "NestedDict" ? (
|
||||
<div className=" w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
]?.value?.toString() === "{}"
|
||||
? {
|
||||
yourkey: "value",
|
||||
}
|
||||
: myData.node!.template[templateParam]
|
||||
.value
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value = newValue;
|
||||
handleOnNewValue(
|
||||
newValue,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
id="editnode-div-dict-input"
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "dict" ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"max-h-48 w-full overflow-auto custom-scroll",
|
||||
myData.node!.template[templateParam].value
|
||||
?.length > 1
|
||||
? "my-3"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<KeypairListComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node!.template[templateParam]
|
||||
.value?.length === 0 ||
|
||||
!myData.node!.template[templateParam]
|
||||
.value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value
|
||||
)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers =
|
||||
convertValuesToNumbers(newValue);
|
||||
myData.node!.template[
|
||||
templateParam
|
||||
].value = valueToNumbers;
|
||||
setErrorDuplicateKey(
|
||||
hasDuplicateKeys(valueToNumbers)
|
||||
);
|
||||
handleOnNewValue(
|
||||
valueToNumbers,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
/>
|
||||
) : myData.current.node.template[
|
||||
templateParam
|
||||
].multiline ? (
|
||||
<TextAreaComponent
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "bool" ? (
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
id={"toggle-edit-" + index}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
enabled={
|
||||
myData.node.template[templateParam]
|
||||
.value
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
setEnabled={(isEnabled) => {
|
||||
handleOnNewValue(
|
||||
isEnabled,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
editNode={true}
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "float" ? (
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
password={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].password ?? false
|
||||
editNode={true}
|
||||
rangeSpec={
|
||||
myData.node!.template[templateParam]
|
||||
.rangeSpec
|
||||
}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "NestedDict" ? (
|
||||
<div className=" w-full">
|
||||
<DictComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node!.template[
|
||||
templateParam
|
||||
].value.toString() === "{}"
|
||||
? {
|
||||
yourkey: "value",
|
||||
}
|
||||
: myData.current.node!.template[
|
||||
templateParam
|
||||
].value
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
myData.current.node!.template[
|
||||
templateParam
|
||||
].value = newValue;
|
||||
handleOnNewValue(newValue, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "dict" ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"max-h-48 w-full overflow-auto custom-scroll",
|
||||
myData.current.node!.template[templateParam]
|
||||
.value?.length > 1
|
||||
? "my-3"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<KeypairListComponent
|
||||
dataValue={dataValue}
|
||||
advanced={adv}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node!.template[
|
||||
templateParam
|
||||
].value?.length === 0 ||
|
||||
!myData.current.node!.template[
|
||||
templateParam
|
||||
].value
|
||||
? [{ "": "" }]
|
||||
: convertObjToArray(
|
||||
myData.current.node!.template[
|
||||
templateParam
|
||||
].value
|
||||
)
|
||||
}
|
||||
duplicateKey={errorDuplicateKey}
|
||||
onChange={(newValue) => {
|
||||
const valueToNumbers =
|
||||
convertValuesToNumbers(newValue);
|
||||
myData.current.node!.template[
|
||||
templateParam
|
||||
].value = valueToNumbers;
|
||||
setErrorDuplicateKey(
|
||||
hasDuplicateKeys(valueToNumbers)
|
||||
);
|
||||
handleOnNewValue(
|
||||
valueToNumbers,
|
||||
templateParam
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "bool" ? (
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "str" &&
|
||||
myData.node.template[templateParam]
|
||||
.options ? (
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
numberOfOptions={nodeLength}
|
||||
editNode={true}
|
||||
options={
|
||||
myData.node.template[templateParam]
|
||||
.options
|
||||
}
|
||||
onSelect={(value) =>
|
||||
handleOnNewValue(value, templateParam)
|
||||
}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? "Choose an option"
|
||||
}
|
||||
id={"dropdown-edit-" + index}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
id={"edit-int-input-" + index}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "file" ? (
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
fileTypes={
|
||||
myData.node.template[templateParam]
|
||||
.fileTypes
|
||||
}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node!.template[
|
||||
templateParam
|
||||
].file_path = filePath;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
readonly={
|
||||
myData.node?.flow ? true : false
|
||||
}
|
||||
field_name={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
nodeClass={myData.node}
|
||||
setNodeClass={(nodeClass) => {
|
||||
myData.node = nodeClass;
|
||||
}}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"prompt-area-edit" + index}
|
||||
data-testid={
|
||||
"modal-prompt-input-" + index
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
myData.node?.flow &&
|
||||
myData.node.template[templateParam]
|
||||
.dynamic
|
||||
? true
|
||||
: false
|
||||
}
|
||||
dynamic={
|
||||
data.node!.template[templateParam]
|
||||
.dynamic ?? false
|
||||
}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.node.template[templateParam]
|
||||
.value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"code-area-edit" + index}
|
||||
/>
|
||||
</div>
|
||||
) : myData.node?.template[templateParam]
|
||||
.type === "Any" ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="hidden"></div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
disabled={disabled}
|
||||
enabled={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value
|
||||
id={
|
||||
"show" +
|
||||
myData.node?.template[templateParam].name
|
||||
}
|
||||
setEnabled={(isEnabled) => {
|
||||
handleOnNewValue(
|
||||
isEnabled,
|
||||
templateParam
|
||||
);
|
||||
enabled={
|
||||
!myData.node?.template[templateParam]
|
||||
.advanced
|
||||
}
|
||||
setEnabled={(e) => {
|
||||
changeAdvanced(templateParam);
|
||||
}}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "float" ? (
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "str" &&
|
||||
myData.current.node.template[templateParam]
|
||||
.options ? (
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
numberOfOptions={nodeLength}
|
||||
editNode={true}
|
||||
options={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].options
|
||||
}
|
||||
onSelect={(value) =>
|
||||
handleOnNewValue(value, templateParam)
|
||||
}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? "Choose an option"
|
||||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "file" ? (
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
fileTypes={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].fileTypes
|
||||
}
|
||||
suffixes={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].suffixes
|
||||
}
|
||||
onFileChange={(filePath: string) => {
|
||||
data.node!.template[
|
||||
templateParam
|
||||
].file_path = filePath;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
field_name={templateParam}
|
||||
editNode={true}
|
||||
disabled={disabled}
|
||||
nodeClass={myData.current.node}
|
||||
setNodeClass={(nodeClass) => {
|
||||
myData.current.node = nodeClass;
|
||||
}}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
dynamic={
|
||||
data.node!.template[templateParam]
|
||||
.dynamic ?? false
|
||||
}
|
||||
setNodeClass={(nodeClass) => {
|
||||
data.node = nodeClass;
|
||||
}}
|
||||
nodeClass={data.node}
|
||||
disabled={disabled}
|
||||
editNode={true}
|
||||
value={
|
||||
myData.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "Any" ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="hidden"></div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
enabled={
|
||||
!myData.current.node?.template[
|
||||
templateParam
|
||||
].advanced
|
||||
}
|
||||
setEnabled={(e) => {
|
||||
changeAdvanced(templateParam);
|
||||
}}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
|
@ -492,12 +532,12 @@ const EditNodeModal = forwardRef(
|
|||
|
||||
<BaseModal.Footer>
|
||||
<Button
|
||||
id={"saveChangesBtn"}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
const newData = cloneDeep(myData.current);
|
||||
myData.current = newData;
|
||||
data.node = myData.node;
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
|
|
@ -506,7 +546,7 @@ const EditNodeModal = forwardRef(
|
|||
},
|
||||
};
|
||||
});
|
||||
setModalOpen(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,9 @@ import { useContext, useEffect, useRef, useState } from "react";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CONTROL_NEW_API_KEY } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { createApiKey } from "../../controllers/API";
|
||||
import {
|
||||
ApiKeyInputType,
|
||||
ApiKeyType,
|
||||
inputHandlerEventType,
|
||||
} from "../../types/components";
|
||||
import { ApiKeyType } from "../../types/components";
|
||||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
|
|
@ -27,19 +22,11 @@ export default function SecretKeyModal({
|
|||
const [open, setOpen] = useState(false);
|
||||
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
|
||||
const [apiKeyValue, setApiKeyValue] = useState("");
|
||||
const [inputState, setInputState] =
|
||||
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
|
||||
const [renderKey, setRenderKey] = useState(false);
|
||||
const [textCopied, setTextCopied] = useState(true);
|
||||
const { setSuccessData } = useContext(alertContext);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
function handleInput({
|
||||
target: { name, value },
|
||||
}: inputHandlerEventType): void {
|
||||
setInputState((prev) => ({ ...prev, [name]: value }));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setRenderKey(false);
|
||||
|
|
@ -101,14 +88,7 @@ export default function SecretKeyModal({
|
|||
</span>
|
||||
<div className="flex pt-3">
|
||||
<div className="w-full">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onChange={(event) => {
|
||||
setApiKeyValue(event.target.value);
|
||||
}}
|
||||
readOnly={true}
|
||||
value={apiKeyValue}
|
||||
/>
|
||||
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
@ -153,7 +133,6 @@ export default function SecretKeyModal({
|
|||
<Form.Control asChild>
|
||||
<input
|
||||
onChange={({ target: { value } }) => {
|
||||
handleInput({ target: { name: "apikeyname", value } });
|
||||
setApiKeyName(value);
|
||||
}}
|
||||
value={apiKeyName}
|
||||
|
|
|
|||
124
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal file
124
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -67,7 +67,8 @@ interface BaseModalProps {
|
|||
| "large"
|
||||
| "large-h-full"
|
||||
| "small-h-full"
|
||||
| "medium-h-full";
|
||||
| "medium-h-full"
|
||||
| "smaller-h-full";
|
||||
|
||||
disable?: boolean;
|
||||
onChangeOpenModal?: (open?: boolean) => void;
|
||||
|
|
@ -98,11 +99,15 @@ function BaseModal({
|
|||
switch (size) {
|
||||
case "x-small":
|
||||
minWidth = "min-w-[20vw]";
|
||||
height = "h-[10vh]";
|
||||
height = " ";
|
||||
break;
|
||||
case "smaller":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-[27vh]";
|
||||
height = "h-[11rem]";
|
||||
break;
|
||||
case "smaller-h-full":
|
||||
minWidth = "min-w-[40vw]";
|
||||
height = "h-full";
|
||||
break;
|
||||
case "small":
|
||||
minWidth = "min-w-[40vw]";
|
||||
|
|
@ -145,9 +150,7 @@ function BaseModal({
|
|||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div className={`mt-2 flex flex-col ${height!} w-full `}>
|
||||
{ContentChild}
|
||||
</div>
|
||||
<div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
|
|||
import AceEditor from "react-ace";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Input } from "../../components/ui/input";
|
||||
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
|
|
@ -23,6 +24,7 @@ export default function CodeAreaModal({
|
|||
setNodeClass,
|
||||
children,
|
||||
dynamic,
|
||||
readonly = false,
|
||||
}: codeAreaModalPropsType): JSX.Element {
|
||||
const [code, setCode] = useState(value);
|
||||
const { dark } = useContext(darkContext);
|
||||
|
|
@ -86,8 +88,7 @@ export default function CodeAreaModal({
|
|||
.then((apiReturn) => {
|
||||
const { data } = apiReturn;
|
||||
if (data) {
|
||||
setNodeClass(data);
|
||||
setValue(code);
|
||||
setNodeClass(data, code);
|
||||
setError({ detail: { error: undefined, traceback: undefined } });
|
||||
setOpen(false);
|
||||
}
|
||||
|
|
@ -143,9 +144,15 @@ export default function CodeAreaModal({
|
|||
/>
|
||||
</BaseModal.Header>
|
||||
<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="h-full w-full">
|
||||
<AceEditor
|
||||
readOnly={readonly}
|
||||
value={code}
|
||||
mode="python"
|
||||
height={height ?? "100%"}
|
||||
|
|
@ -180,7 +187,13 @@ export default function CodeAreaModal({
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ export default function DictAreaModal({
|
|||
/>
|
||||
<div className="flex h-fit w-full justify-end">
|
||||
<Button
|
||||
data-testid="save-dict-button"
|
||||
className="mt-3"
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
|
|||
import { Button } from "../../components/ui/button";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
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 BaseModal from "../baseModal";
|
||||
|
||||
const ExportModal = forwardRef(
|
||||
(props: { children: ReactNode }, ref): JSX.Element => {
|
||||
const { flows, tabId, downloadFlow } = useContext(TabsContext);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setNoticeData } = useContext(alertContext);
|
||||
const [checked, setChecked] = useState(true);
|
||||
const flow = flows.find((f) => f.id === tabId);
|
||||
useEffect(() => {
|
||||
setName(flow!.name);
|
||||
|
|
@ -22,7 +26,7 @@ const ExportModal = forwardRef(
|
|||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<BaseModal size="smaller" open={open} setOpen={setOpen}>
|
||||
<BaseModal size="smaller-h-full" open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{props.children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2">Export</span>
|
||||
|
|
@ -36,14 +40,13 @@ const ExportModal = forwardRef(
|
|||
<EditFlowSettings
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
/>
|
||||
<div className="mt-3 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={checked}
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setChecked(event);
|
||||
}}
|
||||
|
|
@ -52,20 +55,42 @@ const ExportModal = forwardRef(
|
|||
Save with my API keys
|
||||
</label>
|
||||
</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.Footer>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (checked)
|
||||
if (checked) {
|
||||
downloadFlow(
|
||||
flows.find((flow) => flow.id === tabId)!,
|
||||
{
|
||||
id: tabId,
|
||||
data: flow!.data!,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: false,
|
||||
},
|
||||
name!,
|
||||
description
|
||||
);
|
||||
else
|
||||
setNoticeData({
|
||||
title:
|
||||
"Warning: Critical data, JSON file may include API keys.",
|
||||
});
|
||||
} else
|
||||
downloadFlow(
|
||||
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
|
||||
removeApiKeys({
|
||||
id: tabId,
|
||||
data: flow!.data!,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: false,
|
||||
}),
|
||||
name!,
|
||||
description
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,15 +3,16 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
|
|||
import IconComponent from "../../components/genericIconComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { FlowSettingsPropsType } from "../../types/components";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import BaseModal from "../baseModal";
|
||||
|
||||
export default function FlowSettingsModal({
|
||||
open,
|
||||
setOpen,
|
||||
}: FlowSettingsPropsType): JSX.Element {
|
||||
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext);
|
||||
const { flows, tabId, saveFlow } = useContext(FlowsContext);
|
||||
const flow = flows.find((f) => f.id === tabId);
|
||||
useEffect(() => {
|
||||
setName(flow!.name);
|
||||
|
|
@ -19,7 +20,6 @@ export default function FlowSettingsModal({
|
|||
}, [flow!.name, flow!.description]);
|
||||
const [name, setName] = useState(flow!.name);
|
||||
const [description, setDescription] = useState(flow!.description);
|
||||
const [invalidName, setInvalidName] = useState(false);
|
||||
|
||||
function handleClick(): void {
|
||||
let savedFlow = flows.find((flow) => flow.id === tabId);
|
||||
|
|
@ -28,6 +28,17 @@ export default function FlowSettingsModal({
|
|||
saveFlow(savedFlow!);
|
||||
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 (
|
||||
<BaseModal open={open} setOpen={setOpen} size="smaller">
|
||||
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
|
||||
|
|
@ -36,19 +47,20 @@ export default function FlowSettingsModal({
|
|||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<EditFlowSettings
|
||||
invalidName={invalidName}
|
||||
setInvalidName={setInvalidName}
|
||||
invalidNameList={nameLists}
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
/>
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<Button disabled={invalidName} onClick={handleClick} type="submit">
|
||||
<Button
|
||||
disabled={nameLists.includes(name) && name !== flow!.name}
|
||||
onClick={handleClick}
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</BaseModal.Footer>
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ import {
|
|||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import { TabsState } from "../../types/tabs";
|
||||
import { FlowsState } from "../../types/tabs";
|
||||
import { validateNodes } from "../../utils/reactflowUtils";
|
||||
|
||||
export default function FormModal({
|
||||
|
|
@ -38,7 +38,7 @@ export default function FormModal({
|
|||
setOpen: (open: boolean) => void;
|
||||
flow: FlowType;
|
||||
}): JSX.Element {
|
||||
const { tabsState, setTabsState } = useContext(TabsContext);
|
||||
const { tabsState, setTabsState } = useContext(FlowsContext);
|
||||
const [chatValue, setChatValue] = useState(() => {
|
||||
try {
|
||||
const { formKeysData } = tabsState[flow.id];
|
||||
|
|
@ -393,7 +393,7 @@ export default function FormModal({
|
|||
const message = inputs;
|
||||
addChatHistory(message!, true, chatKey!, template.current);
|
||||
sendAll({
|
||||
...reactFlowInstance?.toObject()!,
|
||||
...flow.data!,
|
||||
inputs: inputs!,
|
||||
chatHistory,
|
||||
name: flow.name,
|
||||
|
|
@ -401,7 +401,7 @@ export default function FormModal({
|
|||
chatKey: chatKey!,
|
||||
});
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
setTabsState((old: FlowsState) => {
|
||||
if (!chatKey) return old;
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[id.current].formKeysData.input_keys![chatKey] = "";
|
||||
|
|
@ -522,7 +522,7 @@ export default function FormModal({
|
|||
}
|
||||
onChange={(e) => {
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
setTabsState((old: FlowsState) => {
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[
|
||||
id.current
|
||||
|
|
@ -634,7 +634,7 @@ export default function FormModal({
|
|||
setChatValue={(value) => {
|
||||
setChatValue(value);
|
||||
//@ts-ignore
|
||||
setTabsState((old: TabsState) => {
|
||||
setTabsState((old: FlowsState) => {
|
||||
let newTabsState = _.cloneDeep(old);
|
||||
newTabsState[id.current].formKeysData.input_keys![
|
||||
chatKey!
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export default function GenericModal({
|
|||
nodeClass,
|
||||
setNodeClass,
|
||||
children,
|
||||
id = "",
|
||||
readonly = false,
|
||||
}: genericModalPropsType): JSX.Element {
|
||||
const [myButtonText] = useState(buttonText);
|
||||
const [myModalTitle] = useState(modalTitle);
|
||||
|
|
@ -38,7 +40,7 @@ export default function GenericModal({
|
|||
const [inputValue, setInputValue] = useState(value);
|
||||
const [isEdit, setIsEdit] = useState(true);
|
||||
const [wordsHighlight, setWordsHighlight] = useState<string[]>([]);
|
||||
const { setErrorData, setSuccessData, setNoticeData } =
|
||||
const { setErrorData, setSuccessData, setNoticeData, setModalContextOpen } =
|
||||
useContext(alertContext);
|
||||
const ref = useRef();
|
||||
const divRef = useRef(null);
|
||||
|
|
@ -104,15 +106,14 @@ export default function GenericModal({
|
|||
: "code-nohighlight";
|
||||
}
|
||||
|
||||
// Function need some review, working for now
|
||||
function validatePrompt(closeModal: boolean): void {
|
||||
//nodeClass is always null on tweaks
|
||||
|
||||
postValidatePrompt(field_name, inputValue, nodeClass!)
|
||||
.then((apiReturn) => {
|
||||
// if field_name is an empty string, then we need to set it
|
||||
// to the first key of the custom_fields object
|
||||
if (field_name === "") {
|
||||
console.log(apiReturn.data?.frontend_node?.custom_fields);
|
||||
field_name = Array.isArray(
|
||||
apiReturn.data?.frontend_node?.custom_fields?.[""]
|
||||
)
|
||||
|
|
@ -121,38 +122,23 @@ export default function GenericModal({
|
|||
}
|
||||
if (apiReturn.data) {
|
||||
let inputVariables = apiReturn.data.input_variables ?? [];
|
||||
if (inputVariables && inputVariables.length === 0) {
|
||||
if (!inputVariables || inputVariables.length === 0) {
|
||||
setIsEdit(true);
|
||||
setNoticeData({
|
||||
title: "Your template does not have any variables.",
|
||||
});
|
||||
setModalOpen(false);
|
||||
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 {
|
||||
setIsEdit(false);
|
||||
setSuccessData({
|
||||
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;
|
||||
) {
|
||||
setNodeClass!(apiReturn.data?.frontend_node, inputValue);
|
||||
setModalOpen(closeModal);
|
||||
setIsEdit(false);
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -172,6 +158,10 @@ export default function GenericModal({
|
|||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setModalContextOpen(modalOpen);
|
||||
}, [modalOpen]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
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
|
||||
name="FileText"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
|
|
@ -208,8 +200,10 @@ export default function GenericModal({
|
|||
"flex h-full w-full"
|
||||
)}
|
||||
>
|
||||
{type === TypeModal.PROMPT && isEdit ? (
|
||||
{type === TypeModal.PROMPT && isEdit && !readonly ? (
|
||||
<Textarea
|
||||
id={"modal-" + id}
|
||||
data-testid={"modal-" + id}
|
||||
ref={divRefPrompt}
|
||||
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
|
||||
value={inputValue}
|
||||
|
|
@ -226,7 +220,7 @@ export default function GenericModal({
|
|||
handleKeyDown(e, inputValue, "");
|
||||
}}
|
||||
/>
|
||||
) : type === TypeModal.PROMPT && !isEdit ? (
|
||||
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
|
||||
<SanitizedHTMLWrapper
|
||||
className={getClassByNumberLength()}
|
||||
content={coloredContent}
|
||||
|
|
@ -248,6 +242,9 @@ export default function GenericModal({
|
|||
onKeyDown={(e) => {
|
||||
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"
|
||||
>
|
||||
<div className="relative bottom-[1px]">
|
||||
<span>
|
||||
<span id={"badge" + index.toString()}>
|
||||
{word.replace(/[{}]/g, "").length > 59
|
||||
? word.replace(/[{}]/g, "").slice(0, 56) +
|
||||
"..."
|
||||
|
|
@ -304,6 +301,9 @@ export default function GenericModal({
|
|||
)}
|
||||
</div>
|
||||
<Button
|
||||
data-testid="genericModalBtnSave"
|
||||
id="genericModalBtnSave"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
switch (myModalType) {
|
||||
case TypeModal.TEXT:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
298
src/frontend/src/modals/shareModal/index.tsx
Normal file
298
src/frontend/src/modals/shareModal/index.tsx
Normal 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}</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import {
|
||||
addUser,
|
||||
deleteUser,
|
||||
|
|
@ -44,7 +44,7 @@ export default function AdminPage() {
|
|||
const { userData } = useContext(AuthContext);
|
||||
const [totalRowsCount, setTotalRowsCount] = useState(0);
|
||||
|
||||
const { setTabId } = useContext(TabsContext);
|
||||
const { setTabId } = useContext(FlowsContext);
|
||||
|
||||
// set null id
|
||||
useEffect(() => {
|
||||
|
|
@ -312,7 +312,6 @@ export default function AdminPage() {
|
|||
title="Edit"
|
||||
titleHeader={`${user.username}`}
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Confirm"
|
||||
icon={"UserCog2"}
|
||||
|
|
@ -326,12 +325,20 @@ export default function AdminPage() {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-fit">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={user.is_active}
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<Checkbox
|
||||
id="is_active"
|
||||
checked={user.is_active}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
</TableCell>
|
||||
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
|
||||
|
|
@ -340,7 +347,6 @@ export default function AdminPage() {
|
|||
title="Edit"
|
||||
titleHeader={`${user.username}`}
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you completely confident about the changes you are making to this user?"
|
||||
cancelText="Cancel"
|
||||
confirmationText="Confirm"
|
||||
icon={"UserCog2"}
|
||||
|
|
@ -354,12 +360,20 @@ export default function AdminPage() {
|
|||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-fit">
|
||||
<Checkbox
|
||||
id="is_superuser"
|
||||
checked={user.is_superuser}
|
||||
/>
|
||||
</div>
|
||||
<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">
|
||||
<Checkbox
|
||||
id="is_superuser"
|
||||
checked={user.is_superuser}
|
||||
/>
|
||||
</div>
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
</TableCell>
|
||||
<TableCell className="truncate py-2 ">
|
||||
|
|
@ -402,7 +416,6 @@ export default function AdminPage() {
|
|||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this user? This action cannot be undone."
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
|
|
@ -412,12 +425,20 @@ export default function AdminPage() {
|
|||
handleDeleteUser(user);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip content="Delete" side="top">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
<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">
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -224,7 +224,6 @@ export default function ApiKeysPage() {
|
|||
title="Delete"
|
||||
titleHeader="Delete User"
|
||||
modalContentTitle="Attention!"
|
||||
modalContent="Are you sure you want to delete this key? This action cannot be undone."
|
||||
cancelText="Cancel"
|
||||
confirmationText="Delete"
|
||||
icon={"UserMinus2"}
|
||||
|
|
@ -234,15 +233,19 @@ export default function ApiKeysPage() {
|
|||
handleDeleteKey(keys);
|
||||
}}
|
||||
>
|
||||
<ShadTooltip
|
||||
content="Delete"
|
||||
side="top"
|
||||
>
|
||||
<ConfirmationModal.Content>
|
||||
<span>
|
||||
Are you sure you want to delete
|
||||
this key? This action cannot be
|
||||
undone.
|
||||
</span>
|
||||
</ConfirmationModal.Content>
|
||||
<ConfirmationModal.Trigger>
|
||||
<IconComponent
|
||||
name="Trash2"
|
||||
className="ml-2 h-4 w-4 cursor-pointer"
|
||||
/>
|
||||
</ShadTooltip>
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,12 +8,13 @@ export default function DisclosureComponent({
|
|||
openDisc,
|
||||
}: DisclosureComponentType): JSX.Element {
|
||||
return (
|
||||
<Disclosure as="div" key={title}>
|
||||
<Disclosure as="div" defaultOpen={openDisc} key={title}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Disclosure.Button className="components-disclosure-arrangement">
|
||||
<div className="flex gap-4">
|
||||
{/* BUG ON THIS ICON */}
|
||||
<Icon strokeWidth={1.5} size={22} className="text-primary" />
|
||||
<span className="components-disclosure-title">{title}</span>
|
||||
</div>
|
||||
|
|
@ -34,9 +35,7 @@ export default function DisclosureComponent({
|
|||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Disclosure.Panel as="div" static={openDisc}>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
<Disclosure.Panel as="div">{children}</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
|
|
|
|||
|
|
@ -13,10 +13,8 @@ import ReactFlow, {
|
|||
Controls,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeDragHandler,
|
||||
OnEdgesDelete,
|
||||
OnSelectionChangeParams,
|
||||
SelectionDragHandler,
|
||||
addEdge,
|
||||
|
|
@ -27,17 +25,25 @@ import ReactFlow, {
|
|||
} from "reactflow";
|
||||
import GenericNode from "../../../../CustomNodes/GenericNode";
|
||||
import Chat from "../../../../components/chatComponent";
|
||||
import Loading from "../../../../components/ui/loading";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../../../contexts/flowsContext";
|
||||
import { locationContext } from "../../../../contexts/locationContext";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { FlowType, NodeType } from "../../../../types/flow";
|
||||
import { TabsState } from "../../../../types/tabs";
|
||||
import { isValidConnection } from "../../../../utils/reactflowUtils";
|
||||
import { isWrappedWithClass } from "../../../../utils/utils";
|
||||
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
|
||||
import { FlowsState } from "../../../../types/tabs";
|
||||
import {
|
||||
generateFlow,
|
||||
generateNodeFromFlow,
|
||||
isValidConnection,
|
||||
scapeJSONParse,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import SelectionMenu from "../SelectionMenuComponent";
|
||||
import ExtraSidebar from "../extraSidebarComponent";
|
||||
|
||||
const nodeTypes = {
|
||||
|
|
@ -54,7 +60,6 @@ export default function Page({
|
|||
let {
|
||||
updateFlow,
|
||||
uploadFlow,
|
||||
addFlow,
|
||||
getNodeId,
|
||||
paste,
|
||||
lastCopiedSelection,
|
||||
|
|
@ -63,7 +68,8 @@ export default function Page({
|
|||
saveFlow,
|
||||
setTabsState,
|
||||
tabId,
|
||||
} = useContext(TabsContext);
|
||||
saveCurrentFlow,
|
||||
} = useContext(FlowsContext);
|
||||
const {
|
||||
types,
|
||||
reactFlowInstance,
|
||||
|
|
@ -76,16 +82,24 @@ export default function Page({
|
|||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
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] =
|
||||
useState<OnSelectionChangeParams | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
const saveCurrentFlowTimeout = () => {
|
||||
setTimeout(() => {
|
||||
saveCurrentFlow();
|
||||
}, 500); // need to do this because ReactFlow is not asynchronous.
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (!isWrappedWithClass(event, "nocopy")) {
|
||||
if (
|
||||
!isWrappedWithClass(event, "nocopy") &&
|
||||
window.getSelection()?.toString().length === 0
|
||||
) {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "c" &&
|
||||
|
|
@ -100,10 +114,10 @@ export default function Page({
|
|||
lastCopiedSelection
|
||||
) {
|
||||
event.preventDefault();
|
||||
let bounds = reactFlowWrapper.current?.getBoundingClientRect();
|
||||
takeSnapshot();
|
||||
paste(lastCopiedSelection, {
|
||||
x: position.x - bounds!.left,
|
||||
y: position.y - bounds!.top,
|
||||
x: position.current.x,
|
||||
y: position.current.y,
|
||||
});
|
||||
}
|
||||
if (
|
||||
|
|
@ -120,13 +134,16 @@ export default function Page({
|
|||
lastSelection
|
||||
) {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
deleteNode(lastSelection.nodes.map((node) => node.id));
|
||||
deleteEdge(lastSelection.edges.map((edge) => edge.id));
|
||||
saveCurrentFlowTimeout();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseMove = (event) => {
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
position.current = { x: event.clientX, y: event.clientY };
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
|
@ -136,7 +153,12 @@ export default function Page({
|
|||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, [position, lastCopiedSelection, lastSelection]);
|
||||
}, [
|
||||
lastCopiedSelection,
|
||||
lastSelection,
|
||||
takeSnapshot,
|
||||
saveCurrentFlowTimeout,
|
||||
]);
|
||||
|
||||
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
|
||||
|
||||
|
|
@ -145,63 +167,54 @@ export default function Page({
|
|||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
flow.data?.nodes ?? []
|
||||
);
|
||||
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
flow.data?.edges ?? []
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
const edgeUpdateSuccessful = useRef(true);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance && flow) {
|
||||
flow.data = reactFlowInstance.toObject();
|
||||
updateFlow(flow);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [edges]);
|
||||
//update flow when tabs change
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setNodes(flow?.data?.nodes ?? []);
|
||||
setEdges(flow?.data?.edges ?? []);
|
||||
if (reactFlowInstance) {
|
||||
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]);
|
||||
setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 });
|
||||
|
||||
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(() => {
|
||||
const interval = setInterval(() => {
|
||||
setSeconds((prevSeconds) => {
|
||||
let updatedSeconds = prevSeconds + 1;
|
||||
|
||||
if (updatedSeconds % 30 === 0) {
|
||||
saveFlow(flow, true);
|
||||
updatedSeconds = 0;
|
||||
}
|
||||
|
||||
return updatedSeconds;
|
||||
});
|
||||
}, 1000);
|
||||
saveFlow(flow, true);
|
||||
}, 30000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, []);
|
||||
}, [flow, flow.data]);
|
||||
|
||||
const onEdgesChangeMod = useCallback(
|
||||
(change: EdgeChange[]) => {
|
||||
onEdgesChange(change);
|
||||
setNodes((node) => {
|
||||
let newX = _.cloneDeep(node);
|
||||
return newX;
|
||||
});
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
|
|
@ -210,27 +223,37 @@ export default function Page({
|
|||
},
|
||||
};
|
||||
});
|
||||
saveCurrentFlowTimeout();
|
||||
},
|
||||
[onEdgesChange, setNodes, setTabsState, tabId]
|
||||
[onEdgesChange, setNodes, setTabsState, saveCurrentFlowTimeout, tabId]
|
||||
);
|
||||
|
||||
const onNodesChangeMod = useCallback(
|
||||
(change: NodeChange[]) => {
|
||||
onNodesChange(change);
|
||||
//@ts-ignore
|
||||
setTabsState((prev: TabsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
...prev[tabId],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
const changeString = JSON.stringify(change);
|
||||
if (changeString !== nodesOnFlow) {
|
||||
onNodesChange(change);
|
||||
updateNodeFlow(changeString);
|
||||
//@ts-ignore
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
...prev[tabId],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
saveCurrentFlowTimeout();
|
||||
}
|
||||
},
|
||||
[onNodesChange, setTabsState, tabId]
|
||||
[onNodesChange, setTabsState, tabId, updateNodeFlow, saveCurrentFlowTimeout]
|
||||
);
|
||||
|
||||
function updateNodeFlow(changeString: string) {
|
||||
setNodesOnFlow(changeString);
|
||||
}
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
takeSnapshot();
|
||||
|
|
@ -238,12 +261,19 @@ export default function Page({
|
|||
addEdge(
|
||||
{
|
||||
...params,
|
||||
data: {
|
||||
targetHandle: scapeJSONParse(params.targetHandle!),
|
||||
sourceHandle: scapeJSONParse(params.sourceHandle!),
|
||||
},
|
||||
style: { stroke: "#555" },
|
||||
className:
|
||||
(params.targetHandle?.split("|")[0] === "Text"
|
||||
((scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text"
|
||||
? "stroke-foreground "
|
||||
: "stroke-foreground ") + " stroke-connection",
|
||||
animated: params.targetHandle?.split("|")[0] === "Text",
|
||||
animated:
|
||||
(scapeJSONParse(params.targetHandle!) as targetHandleType)
|
||||
.type === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
|
|
@ -252,8 +282,19 @@ export default function Page({
|
|||
let newX = _.cloneDeep(node);
|
||||
return newX;
|
||||
});
|
||||
//@ts-ignore
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
...prev[tabId],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
saveCurrentFlowTimeout();
|
||||
},
|
||||
[setEdges, setNodes, takeSnapshot]
|
||||
[setEdges, setNodes, takeSnapshot, addEdge]
|
||||
);
|
||||
|
||||
const onNodeDragStart: NodeDragHandler = useCallback(() => {
|
||||
|
|
@ -267,11 +308,6 @@ export default function Page({
|
|||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
|
||||
// 👇 make deleting edges undoable
|
||||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer.types.some((types) => types === "nodedata")) {
|
||||
|
|
@ -296,11 +332,10 @@ export default function Page({
|
|||
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
|
||||
const position = reactFlowInstance!.project({
|
||||
x: event.clientX - reactflowBounds!.left,
|
||||
y: event.clientY - reactflowBounds!.top,
|
||||
const position = reactFlowInstance!.screenToFlowPosition({
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
});
|
||||
|
||||
// Generate a unique node ID
|
||||
|
|
@ -337,7 +372,21 @@ export default function Page({
|
|||
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
|
||||
takeSnapshot();
|
||||
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 {
|
||||
setErrorData({
|
||||
title: "Invalid file type",
|
||||
|
|
@ -351,6 +400,9 @@ export default function Page({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExtraComponent(<ExtraSidebar />);
|
||||
setExtraNavigation({ title: "Components" });
|
||||
|
||||
return () => {
|
||||
if (tabsState && tabsState[flow.id]?.isPending) {
|
||||
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(() => {
|
||||
edgeUpdateSuccessful.current = false;
|
||||
}, []);
|
||||
|
|
@ -394,7 +431,7 @@ export default function Page({
|
|||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
||||
const [selectionEnded, setSelectionEnded] = useState(true);
|
||||
|
||||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
|
|
@ -424,6 +461,20 @@ export default function Page({
|
|||
setFilterEdge([]);
|
||||
}, []);
|
||||
|
||||
const onMove = useCallback(() => {
|
||||
saveCurrentFlowTimeout();
|
||||
//@ts-ignore
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
...prev[tabId],
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [setTabsState, saveCurrentFlowTimeout]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
{!view && <ExtraSidebar />}
|
||||
|
|
@ -434,16 +485,18 @@ export default function Page({
|
|||
<div className="h-full w-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).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
|
||||
nodes={nodes}
|
||||
onMove={() => {
|
||||
if (reactFlowInstance)
|
||||
updateFlow({
|
||||
...flow,
|
||||
data: reactFlowInstance.toObject(),
|
||||
});
|
||||
}}
|
||||
onMove={onMove}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChangeMod}
|
||||
onEdgesChange={onEdgesChangeMod}
|
||||
|
|
@ -458,12 +511,10 @@ export default function Page({
|
|||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
onSelectionStart={onSelectionStart}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
connectionLineComponent={ConnectionLineComponent}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onSelectionChange={onSelectionChange}
|
||||
onNodesDelete={onDelete}
|
||||
deleteKeyCode={[]}
|
||||
className="theme-attribution"
|
||||
minZoom={0.01}
|
||||
|
|
@ -481,6 +532,51 @@ export default function Page({
|
|||
[&>button]:border-b-border hover:[&>button]:bg-border"
|
||||
></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>
|
||||
{!view && (
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,29 +1,37 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useMemo, useState } from "react";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { Input } from "../../../../components/ui/input";
|
||||
import { Separator } from "../../../../components/ui/separator";
|
||||
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 ApiModal from "../../../../modals/ApiModal";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import ShareModal from "../../../../modals/shareModal";
|
||||
import { APIClassType, APIObjectType } from "../../../../types/api";
|
||||
import {
|
||||
nodeColors,
|
||||
nodeIconsLucide,
|
||||
nodeNames,
|
||||
} from "../../../../utils/styleUtils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
import {
|
||||
classNames,
|
||||
removeCountFromString,
|
||||
sensitiveSort,
|
||||
} from "../../../../utils/utils";
|
||||
import DisclosureComponent from "../DisclosureComponent";
|
||||
import SidebarDraggableComponent from "./sideBarDraggableComponent";
|
||||
|
||||
export default function ExtraSidebar(): JSX.Element {
|
||||
const { data, templates, getFilterEdge, setFilterEdge } =
|
||||
const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } =
|
||||
useContext(typesContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
|
||||
useContext(TabsContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
|
||||
useContext(FlowsContext);
|
||||
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const [dataFilter, setFilterData] = useState(data);
|
||||
const [search, setSearch] = useState("");
|
||||
const isPending = tabsState[tabId]?.isPending;
|
||||
|
|
@ -52,8 +60,10 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
let ret = {};
|
||||
Object.keys(data).forEach((d: keyof APIObjectType, i) => {
|
||||
ret[d] = {};
|
||||
let keys = Object.keys(data[d]).filter((nd) =>
|
||||
nd.toLowerCase().includes(e.toLowerCase())
|
||||
let keys = Object.keys(data[d]).filter(
|
||||
(nd) =>
|
||||
nd.toLowerCase().includes(e.toLowerCase()) ||
|
||||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
|
||||
);
|
||||
keys.forEach((element) => {
|
||||
ret[d][element] = data[d][element];
|
||||
|
|
@ -76,7 +86,8 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}, []);
|
||||
|
||||
function handleBlur() {
|
||||
if (!search && search === "") {
|
||||
// check if search is search to reset fitler on click input
|
||||
if ((!search && search === "") || search === "search") {
|
||||
setFilterData(data);
|
||||
setFilterEdge([]);
|
||||
setSearch("");
|
||||
|
|
@ -84,11 +95,55 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getFilterEdge.length === 0 && search === "") {
|
||||
setFilterData(data);
|
||||
setFilterEdge([]);
|
||||
if (getFilterEdge.length !== 0) {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -122,39 +177,93 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}
|
||||
}
|
||||
});
|
||||
setSearch("search");
|
||||
setSearch("");
|
||||
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 (
|
||||
<div className="side-bar-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">
|
||||
<ShadTooltip content="Import" side="top">
|
||||
<button
|
||||
className="extra-side-bar-buttons"
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
uploadFlow({ newProject: false, isComponent: false }).catch(
|
||||
(error) => {
|
||||
setErrorData({
|
||||
title: "Error uploading file",
|
||||
list: [error],
|
||||
});
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileUp" className="side-bar-button-size " />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<div className="side-bar-button">
|
||||
<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>
|
||||
{!hasStore && ExportMemo}
|
||||
<ShadTooltip content={"Code"} side="top">
|
||||
<div className="side-bar-button">
|
||||
{flow && flow.data && (
|
||||
|
|
@ -177,30 +286,39 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
</div>
|
||||
</ShadTooltip>
|
||||
<div className="side-bar-button">
|
||||
<ShadTooltip content="Save" side="top">
|
||||
<button
|
||||
className={
|
||||
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
|
||||
}
|
||||
onClick={(event) => {
|
||||
saveFlow(flow!);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Save"
|
||||
{flow && flow.data && (
|
||||
<ShadTooltip content="Save" side="top">
|
||||
<button
|
||||
disabled={flow?.data?.nodes.length === 0}
|
||||
className={
|
||||
"side-bar-button-size" +
|
||||
(isPending ? " " : " extra-side-bar-save-disable")
|
||||
"extra-side-bar-buttons " +
|
||||
(isPending && flow!.data!.nodes?.length > 0
|
||||
? ""
|
||||
: "button-disable")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
onClick={(event) => {
|
||||
saveFlow(flow!);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Save"
|
||||
className={
|
||||
"side-bar-button-size" +
|
||||
(isPending && flow!.data!.nodes?.length > 0
|
||||
? " "
|
||||
: " extra-side-bar-save-disable")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="side-bar-search-div-placement">
|
||||
<Input
|
||||
onFocusCapture={() => handleBlur()}
|
||||
value={search}
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
|
|
@ -223,12 +341,28 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
|
||||
<div className="side-bar-components-div-arrangement">
|
||||
{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) =>
|
||||
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
|
||||
<DisclosureComponent
|
||||
openDisc={search.length == 0 ? false : true}
|
||||
key={index}
|
||||
openDisc={
|
||||
getFilterEdge.length !== 0 || search.length !== 0
|
||||
? true
|
||||
: false
|
||||
}
|
||||
key={index + search + JSON.stringify(getFilterEdge)}
|
||||
button={{
|
||||
title: nodeNames[SBSectionName] ?? nodeNames.unknown,
|
||||
Icon:
|
||||
|
|
@ -237,51 +371,45 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
>
|
||||
<div className="side-bar-components-gap">
|
||||
{Object.keys(dataFilter[SBSectionName])
|
||||
.sort()
|
||||
.sort((a, b) =>
|
||||
sensitiveSort(
|
||||
dataFilter[SBSectionName][a].display_name,
|
||||
dataFilter[SBSectionName][b].display_name
|
||||
)
|
||||
)
|
||||
.map((SBItemName: string, index) => (
|
||||
<ShadTooltip
|
||||
content={data[SBSectionName][SBItemName].display_name}
|
||||
content={
|
||||
dataFilter[SBSectionName][SBItemName].display_name
|
||||
}
|
||||
side="right"
|
||||
key={index}
|
||||
>
|
||||
<div key={index} data-tooltip-id={SBItemName}>
|
||||
<div
|
||||
draggable={!data[SBSectionName][SBItemName].error}
|
||||
className={
|
||||
"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, {
|
||||
type: SBItemName,
|
||||
node: data[SBSectionName][SBItemName],
|
||||
})
|
||||
}
|
||||
onDragEnd={() => {
|
||||
document.body.removeChild(
|
||||
document.getElementsByClassName(
|
||||
"cursor-grabbing"
|
||||
)[0]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="side-bar-components-div-form">
|
||||
<span className="side-bar-components-text">
|
||||
{data[SBSectionName][SBItemName].display_name}
|
||||
</span>
|
||||
<IconComponent
|
||||
name="Menu"
|
||||
className="side-bar-components-icon "
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<SidebarDraggableComponent
|
||||
sectionName={SBSectionName as string}
|
||||
apiClass={dataFilter[SBSectionName][SBItemName]}
|
||||
key={index}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
//split type to remove type in nodes saved with same name removing it's
|
||||
type: removeCountFromString(SBItemName),
|
||||
node: dataFilter[SBSectionName][SBItemName],
|
||||
})
|
||||
}
|
||||
color={nodeColors[SBSectionName]}
|
||||
itemName={SBItemName}
|
||||
//convert error to boolean
|
||||
error={!!dataFilter[SBSectionName][SBItemName].error}
|
||||
display_name={
|
||||
dataFilter[SBSectionName][SBItemName].display_name
|
||||
}
|
||||
official={
|
||||
dataFilter[SBSectionName][SBItemName].official ===
|
||||
false
|
||||
? false
|
||||
: true
|
||||
}
|
||||
/>
|
||||
</ShadTooltip>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
|
|
@ -8,15 +9,26 @@ import {
|
|||
SelectItem,
|
||||
SelectTrigger,
|
||||
} 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 ShareModal from "../../../../modals/shareModal";
|
||||
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({
|
||||
data,
|
||||
setData,
|
||||
deleteNode,
|
||||
position,
|
||||
setShowNode,
|
||||
numberOfHandles,
|
||||
showNode,
|
||||
|
|
@ -33,10 +45,14 @@ export default function NodeToolbarComponent({
|
|||
data.node.template[templateField].type === "prompt" ||
|
||||
data.node.template[templateField].type === "file" ||
|
||||
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
|
||||
);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const { getNodeId } = useContext(FlowsContext);
|
||||
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
|
||||
|
||||
function canMinimize() {
|
||||
let countHandles: number = 0;
|
||||
|
|
@ -47,26 +63,70 @@ export default function NodeToolbarComponent({
|
|||
return true;
|
||||
}
|
||||
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 [showModalAdvanced, setShowModalAdvanced] = useState(false);
|
||||
const [showconfirmShare, setShowconfirmShare] = useState(false);
|
||||
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) => {
|
||||
setSelectedValue(event);
|
||||
if (event.includes("advanced")) {
|
||||
return setShowModalAdvanced(true);
|
||||
}
|
||||
setShowModalAdvanced(false);
|
||||
if (event.includes("show")) {
|
||||
setShowNode((prev) => !prev);
|
||||
updateNodeInternals(data.id);
|
||||
}
|
||||
if (event.includes("disabled")) {
|
||||
return;
|
||||
switch (event) {
|
||||
case "advanced":
|
||||
setShowModalAdvanced(true);
|
||||
break;
|
||||
case "show":
|
||||
takeSnapshot();
|
||||
setShowNode(data.showNode ?? true ? false : true);
|
||||
updateNodeInternals(data.id);
|
||||
break;
|
||||
case "Download":
|
||||
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 (
|
||||
<>
|
||||
<div className="w-26 h-10">
|
||||
|
|
@ -106,70 +166,45 @@ export default function NodeToolbarComponent({
|
|||
<IconComponent name="Copy" className="h-4 w-4" />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
<ShadTooltip
|
||||
content={
|
||||
data.node?.documentation === "" ? "Coming Soon" : "Documentation"
|
||||
}
|
||||
side="top"
|
||||
>
|
||||
<a
|
||||
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" +
|
||||
(data.node?.documentation === ""
|
||||
? " text-muted-foreground"
|
||||
: " text-foreground")
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={data.node?.documentation}
|
||||
// deactivate link if no documentation is provided
|
||||
onClick={(event) => {
|
||||
if (data.node?.documentation === "") {
|
||||
{hasStore && (
|
||||
<ShadTooltip content="Share" side="top">
|
||||
<button
|
||||
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",
|
||||
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<IconComponent name="FileText" className="h-4 w-4 " />
|
||||
</a>
|
||||
</ShadTooltip>
|
||||
if (hasApiKey || hasStore) setShowconfirmShare(true);
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Share3" className="-m-1 h-6 w-6" />
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
||||
{isMinimal ? (
|
||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||
<ShadTooltip content="More" side="top">
|
||||
<SelectTrigger>
|
||||
<div>
|
||||
<div
|
||||
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" +
|
||||
(nodeLength == 0
|
||||
? " text-muted-foreground"
|
||||
: " text-foreground")
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="MoreHorizontal"
|
||||
className="relative left-2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
</ShadTooltip>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
value={
|
||||
getRandomKeyByssmm() +
|
||||
(nodeLength == 0 ? "disabled" : "advanced")
|
||||
}
|
||||
>
|
||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||
<ShadTooltip content="More" side="top">
|
||||
<SelectTrigger>
|
||||
<div>
|
||||
<div
|
||||
className={
|
||||
"flex " +
|
||||
(nodeLength == 0
|
||||
? "text-muted-foreground"
|
||||
: "text-primary")
|
||||
}
|
||||
data-testid="more-options-modal"
|
||||
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"
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="MoreHorizontal"
|
||||
className="relative left-2 h-4 w-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SelectTrigger>
|
||||
</ShadTooltip>
|
||||
<SelectContent>
|
||||
{nodeLength > 0 && (
|
||||
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
|
||||
<div className="flex" data-testid="edit-button-modal">
|
||||
<IconComponent
|
||||
name="Settings2"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
|
|
@ -177,74 +212,116 @@ export default function NodeToolbarComponent({
|
|||
Edit{" "}
|
||||
</div>{" "}
|
||||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "show"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={showNode ? "Minimize2" : "Maximize2"}
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>
|
||||
{showNode ? "Minimize" : "Expand"}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</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 && (
|
||||
<EditNodeModal
|
||||
data={data}
|
||||
setData={setData}
|
||||
nodeLength={nodeLength}
|
||||
open={showModalAdvanced}
|
||||
onClose={(modal) => {
|
||||
setShowModalAdvanced(modal);
|
||||
}}
|
||||
>
|
||||
<></>
|
||||
</EditNodeModal>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<div>
|
||||
<EditNodeModal
|
||||
data={data}
|
||||
setData={setData}
|
||||
nodeLength={nodeLength}
|
||||
{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={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> */}
|
||||
<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 && (
|
||||
<SelectItem value={"show"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name={showNode ? "Minimize2" : "Maximize2"}
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>
|
||||
{showNode ? "Minimize" : "Expand"}
|
||||
</div>
|
||||
</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>
|
||||
</Select>
|
||||
|
||||
<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
|
||||
data={data}
|
||||
nodeLength={nodeLength}
|
||||
open={showModalAdvanced}
|
||||
setOpen={setShowModalAdvanced}
|
||||
/>
|
||||
<ShareModal
|
||||
open={showconfirmShare}
|
||||
setOpen={setShowconfirmShare}
|
||||
is_component={true}
|
||||
component={flowComponent!}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import Header from "../../components/headerComponent";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import Page from "./components/PageComponent";
|
||||
|
||||
export default function FlowPage(): JSX.Element {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { flows, tabId, setTabId, version } = useContext(FlowsContext);
|
||||
const { id } = useParams();
|
||||
|
||||
// Set flow tab id
|
||||
|
|
@ -14,14 +13,6 @@ export default function FlowPage(): JSX.Element {
|
|||
setTabId(id!);
|
||||
}, [id]);
|
||||
|
||||
// Initialize state variable for the version
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
getVersion().then((data) => {
|
||||
setVersion(data.version);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
|
|
|
|||
202
src/frontend/src/pages/MainPage/components/components/index.tsx
Normal file
202
src/frontend/src/pages/MainPage/components/components/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,180 +1,111 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Group, ToyBrick } from "lucide-react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||
import DropdownButton from "../../components/DropdownButtonComponent";
|
||||
import { CardComponent } from "../../components/cardComponent";
|
||||
import IconComponent from "../../components/genericIconComponent";
|
||||
import Header from "../../components/headerComponent";
|
||||
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
|
||||
import PageLayout from "../../components/pageLayout";
|
||||
import SidebarNav from "../../components/sidebarComponent";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
export default function HomePage(): JSX.Element {
|
||||
const {
|
||||
flows,
|
||||
setTabId,
|
||||
downloadFlows,
|
||||
uploadFlows,
|
||||
addFlow,
|
||||
removeFlow,
|
||||
uploadFlow,
|
||||
isLoading,
|
||||
} = useContext(TabsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { setTabId, downloadFlows, uploadFlows, addFlow, uploadFlow } =
|
||||
useContext(FlowsContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const is_component = pathname === "/components";
|
||||
const dropdownOptions = [
|
||||
{
|
||||
name: "Import from JSON",
|
||||
onBtnClick: () =>
|
||||
uploadFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
}),
|
||||
onBtnClick: () => {
|
||||
uploadFlow({
|
||||
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
|
||||
useEffect(() => {
|
||||
setTabId("");
|
||||
}, []);
|
||||
}, [pathname]);
|
||||
|
||||
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
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className="main-page-panel">
|
||||
<div className="main-page-nav-arrangement">
|
||||
<span className="main-page-nav-title">
|
||||
<IconComponent name="Home" className="w-6" />
|
||||
{USER_PROJECTS_HEADER}
|
||||
</span>
|
||||
<div className="button-div-style">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
downloadFlows();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Download" className="main-page-nav-button" />
|
||||
Download Collection
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
uploadFlows();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Upload" className="main-page-nav-button" />
|
||||
Upload Collection
|
||||
</Button>
|
||||
<DropdownButton
|
||||
firstButtonName="New Project"
|
||||
onFirstBtnClick={() => {
|
||||
addFlow(null!, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
options={dropdownOptions}
|
||||
/>
|
||||
</div>
|
||||
<PageLayout
|
||||
title={USER_PROJECTS_HEADER}
|
||||
description="Manage your personal projects. Download or upload your collection."
|
||||
button={
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
downloadFlows();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Download" className="main-page-nav-button" />
|
||||
Download Collection
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
uploadFlows();
|
||||
}}
|
||||
>
|
||||
<IconComponent name="Upload" className="main-page-nav-button" />
|
||||
Upload Collection
|
||||
</Button>
|
||||
<DropdownButton
|
||||
firstButtonName="New Project"
|
||||
onFirstBtnClick={() => {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
options={dropdownOptions}
|
||||
/>
|
||||
</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 ? (
|
||||
<>
|
||||
<IconComponent
|
||||
name="ArrowUpToLine"
|
||||
className="h-12 w-12 stroke-1"
|
||||
/>
|
||||
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 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">
|
||||
<SidebarNav items={sidebarNavItems} />
|
||||
</aside>
|
||||
<div className="h-full w-full flex-1">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
|
|||
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { resetPassword, updateUser } from "../../controllers/API";
|
||||
import {
|
||||
inputHandlerEventType,
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
export default function ProfileSettingsPage(): JSX.Element {
|
||||
const { setTabId } = useContext(TabsContext);
|
||||
const { setTabId } = useContext(FlowsContext);
|
||||
|
||||
const [inputState, setInputState] = useState<patchUserInputStateType>(
|
||||
CONTROL_PATCH_USER_STATE
|
||||
|
|
|
|||
412
src/frontend/src/pages/StorePage/index.tsx
Normal file
412
src/frontend/src/pages/StorePage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,8 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
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";
|
||||
|
||||
export default function ViewPage() {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { setDark } = useContext(darkContext);
|
||||
const { id, theme } = useParams();
|
||||
|
||||
|
|
@ -15,22 +11,6 @@ export default function ViewPage() {
|
|||
setTabId(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 (
|
||||
<div className="flow-page-positioning">
|
||||
{flows.length > 0 &&
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue