Merge remote-tracking branch 'origin/zustand/io/migration' into globalVariables
This commit is contained in:
commit
99561d0d4c
26 changed files with 1381 additions and 887 deletions
|
|
@ -1,4 +1,3 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import useFlowStore from "../../stores/flowStore";
|
||||
import { IOOutputProps } from "../../types/components";
|
||||
import { Textarea } from "../ui/textarea";
|
||||
|
|
|
|||
|
|
@ -105,7 +105,13 @@ export default function IOView({
|
|||
|
||||
return (
|
||||
<BaseModal
|
||||
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"}
|
||||
size={
|
||||
haveChat || selectedViewField
|
||||
? selectedTab === 0
|
||||
? "large-thin"
|
||||
: "large"
|
||||
: "small"
|
||||
}
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
disable={disable}
|
||||
|
|
@ -123,13 +129,13 @@ export default function IOView({
|
|||
</div>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<div className="flex-max-width mt-2 h-full">
|
||||
{selectedTab !== 0 && (
|
||||
<div
|
||||
className={cn(
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start",
|
||||
haveChat ? "w-2/6" : "w-full"
|
||||
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start transition-all duration-300",
|
||||
haveChat || selectedViewField ? "w-2/6" : "w-full"
|
||||
)}
|
||||
>
|
||||
<Tabs
|
||||
|
|
@ -179,20 +185,18 @@ export default function IOView({
|
|||
<Badge variant="gray" size="md">
|
||||
{node.data.node.display_name}
|
||||
</Badge>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(input);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
|
|
@ -248,20 +252,18 @@ export default function IOView({
|
|||
</Badge>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
{haveChat && (
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="-mb-1 pr-4"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setSelectedViewField(output);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
className="h-4 w-4"
|
||||
name="ExternalLink"
|
||||
></IconComponent>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
key={index}
|
||||
|
|
@ -287,12 +289,12 @@ export default function IOView({
|
|||
</div>
|
||||
)}
|
||||
|
||||
{haveChat ? (
|
||||
{haveChat || selectedViewField ? (
|
||||
<div className="flex h-full min-w-96 flex-grow">
|
||||
{selectedViewField && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col items-start gap-4 p-4",
|
||||
"flex h-full w-full flex-col items-start gap-4 pt-4",
|
||||
!selectedViewField ? "hidden" : ""
|
||||
)}
|
||||
>
|
||||
|
|
@ -347,7 +349,7 @@ export default function IOView({
|
|||
</BaseModal.Content>
|
||||
<BaseModal.Footer>
|
||||
{!haveChat && (
|
||||
<div className="flex w-full justify-end pt-6">
|
||||
<div className="flex w-full justify-end pt-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="flex gap-2 px-3"
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const MenuBar = ({
|
|||
const addFlow = useFlowsManagerStore((state) => state.addFlow);
|
||||
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
|
||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
||||
const setSuccessData = useAlertStore((state) => state.setSuccessData);
|
||||
const undo = useFlowsManagerStore((state) => state.undo);
|
||||
const redo = useFlowsManagerStore((state) => state.redo);
|
||||
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
|
||||
|
|
@ -38,11 +39,21 @@ export const MenuBar = ({
|
|||
const navigate = useNavigate();
|
||||
const isBuilding = useFlowStore((state) => state.isBuilding);
|
||||
|
||||
function handleAddFlow() {
|
||||
function handleAddFlow(duplicate?: boolean) {
|
||||
try {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
if (duplicate) {
|
||||
if (!currentFlow) {
|
||||
throw new Error("No flow to duplicate");
|
||||
}
|
||||
addFlow(true, currentFlow).then((id) => {
|
||||
setSuccessData({ title: "Flow duplicated successfully" });
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
} else {
|
||||
addFlow(true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorData(err as { title: string; list?: Array<string> });
|
||||
}
|
||||
|
|
@ -62,7 +73,7 @@ export const MenuBar = ({
|
|||
<button
|
||||
onClick={() => {
|
||||
removeFunction(nodes);
|
||||
navigate(-1);
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<IconComponent name="ChevronLeft" className="w-4" />
|
||||
|
|
@ -88,6 +99,15 @@ export const MenuBar = ({
|
|||
<IconComponent name="Plus" className="header-menu-options" />
|
||||
New
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleAddFlow(true);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<IconComponent name="Copy" className="header-menu-options" />
|
||||
Duplicate
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as Form from "@radix-ui/react-form";
|
||||
import { PopoverAnchor } from "@radix-ui/react-popover";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import useAlertStore from "../../stores/alertStore";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { handleKeyDown } from "../../utils/reactflowUtils";
|
||||
import { classNames, cn } from "../../utils/utils";
|
||||
|
|
@ -37,6 +38,7 @@ export default function InputComponent({
|
|||
optionsButton,
|
||||
optionButton,
|
||||
}: InputComponentType): JSX.Element {
|
||||
const setErrorData = useAlertStore.getState().setErrorData;
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const refInput = useRef<HTMLInputElement>(null);
|
||||
const [showOptions, setShowOptions] = useState<boolean>(false);
|
||||
|
|
@ -119,8 +121,23 @@ export default function InputComponent({
|
|||
)}
|
||||
placeholder={password && editNode ? "Key" : placeholder}
|
||||
onChange={(e) => {
|
||||
onChange && onChange(e.target.value);
|
||||
}}
|
||||
// if the user copies a password from another input
|
||||
// it might come as ••••••••••• it causes errors
|
||||
// in ascii encoding, so we need to handle it
|
||||
if (password) {
|
||||
// check if all chars are •
|
||||
if (e.target.value.split("").every((char) => char === "•")) {
|
||||
setErrorData({
|
||||
title: `Invalid characters: ${e.target.value}`,
|
||||
list: [
|
||||
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
onChange && onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
handleKeyDown(e, value, "");
|
||||
if (blurOnEnter && e.key === "Enter")
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default function RenameLabel(props) {
|
|||
}
|
||||
resizeInput();
|
||||
return () => {
|
||||
if(isRename) document.removeEventListener("keydown", () => {});
|
||||
if (isRename) document.removeEventListener("keydown", () => {});
|
||||
};
|
||||
}, [isRename]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// src/constants/constants.ts
|
||||
|
||||
import { languageMap } from "../types/components";
|
||||
import { FlowType } from "../types/flow";
|
||||
|
||||
/**
|
||||
* invalid characters for flow name
|
||||
|
|
@ -750,75 +749,5 @@ export const NATIVE_CATEGORIES = [
|
|||
"helpers",
|
||||
"experimental",
|
||||
];
|
||||
/*
|
||||
Data ingestion
|
||||
Basic Prompting
|
||||
Chat com memória
|
||||
Working with data (file/website)
|
||||
API requests
|
||||
Vector Store
|
||||
Assistant
|
||||
*/
|
||||
|
||||
export const EXAMPLES_MOCK: FlowType[] = [
|
||||
{
|
||||
name: "Working with data",
|
||||
id: "Working with data Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Basic Prompting",
|
||||
id: "Basic Prompting Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Chat with memory",
|
||||
id: "Chat with memory Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "API requests",
|
||||
id: "API requests Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
{
|
||||
name: "Assistant",
|
||||
id: "Assistant Description",
|
||||
data: {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
viewport: { zoom: 1, x: 1, y: 1 },
|
||||
},
|
||||
description: "This flow represents the first process in our application.",
|
||||
folder: STARTER_FOLDER_NAME,
|
||||
user_id: undefined,
|
||||
},
|
||||
];
|
||||
export const SAVE_DEBOUNCE_TIME = 500;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { modalHeaderType } from "../../types/components";
|
||||
import { cn } from "../../utils/utils";
|
||||
|
||||
type ContentProps = { children: ReactNode };
|
||||
type HeaderProps = { children: ReactNode; description: string };
|
||||
|
|
@ -157,11 +158,15 @@ function BaseModal({
|
|||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
{triggerChild}
|
||||
<DialogContent className={minWidth}>
|
||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
||||
<div className="truncate-doubleline word-break-break-word">
|
||||
{headerChild}
|
||||
</div>
|
||||
<div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div>
|
||||
<div
|
||||
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
||||
>
|
||||
{ContentChild}
|
||||
</div>
|
||||
{ContentFooter && (
|
||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -146,75 +146,83 @@ export default function CodeAreaModal({
|
|||
setCode(value);
|
||||
}, [value, open]);
|
||||
|
||||
const handlePreventEsc = (e: React.KeyboardEvent) => {
|
||||
if (e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseModal open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2"> {EDIT_CODE_TITLE} </span>
|
||||
<IconComponent
|
||||
name="prompts"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<Input
|
||||
value={code}
|
||||
readOnly
|
||||
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"
|
||||
setOptions={{ fontFamily: "monospace" }}
|
||||
height={height ?? "100%"}
|
||||
highlightActiveLine={true}
|
||||
showPrintMargin={false}
|
||||
fontSize={14}
|
||||
showGutter
|
||||
enableLiveAutocompletion
|
||||
theme={dark ? "twilight" : "github"}
|
||||
name="CodeEditor"
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className="h-full w-full rounded-lg border-[1px] border-gray-300 custom-scroll dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"whitespace-break-spaces transition-all delay-500" +
|
||||
(error?.detail?.error !== undefined ? "h-2/6" : "h-0")
|
||||
}
|
||||
>
|
||||
<div className="mt-1 h-full max-h-[10rem] w-full overflow-y-auto overflow-x-clip text-left custom-scroll">
|
||||
<h1 className="text-lg text-destructive">
|
||||
{error?.detail?.error}
|
||||
</h1>
|
||||
<div className="ml-2 w-full text-sm text-status-red word-break-break-word">
|
||||
<span className="w-full word-break-break-word">
|
||||
{error?.detail?.traceback}
|
||||
</span>
|
||||
<div onKeyDown={(e) => handlePreventEsc(e)}>
|
||||
<BaseModal open={open} setOpen={setOpen}>
|
||||
<BaseModal.Trigger>{children}</BaseModal.Trigger>
|
||||
<BaseModal.Header description={CODE_PROMPT_DIALOG_SUBTITLE}>
|
||||
<span className="pr-2"> {EDIT_CODE_TITLE} </span>
|
||||
<IconComponent
|
||||
name="prompts"
|
||||
className="h-6 w-6 pl-1 text-primary "
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<Input
|
||||
value={code}
|
||||
readOnly
|
||||
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"
|
||||
setOptions={{ fontFamily: "monospace" }}
|
||||
height={height ?? "100%"}
|
||||
highlightActiveLine={true}
|
||||
showPrintMargin={false}
|
||||
fontSize={14}
|
||||
showGutter
|
||||
enableLiveAutocompletion
|
||||
theme={dark ? "twilight" : "github"}
|
||||
name="CodeEditor"
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className="h-full w-full rounded-lg border-[1px] border-gray-300 custom-scroll dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
"whitespace-break-spaces transition-all delay-500" +
|
||||
(error?.detail?.error !== undefined ? "h-2/6" : "h-0")
|
||||
}
|
||||
>
|
||||
<div className="mt-1 h-full max-h-[10rem] w-full overflow-y-auto overflow-x-clip text-left custom-scroll">
|
||||
<h1 className="text-lg text-destructive">
|
||||
{error?.detail?.error}
|
||||
</h1>
|
||||
<div className="ml-2 w-full text-sm text-status-red word-break-break-word">
|
||||
<span className="w-full word-break-break-word">
|
||||
{error?.detail?.traceback}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-fit w-full justify-end">
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={handleClick}
|
||||
type="submit"
|
||||
id="checkAndSaveBtn"
|
||||
disabled={readonly}
|
||||
>
|
||||
Check & Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-fit w-full justify-end">
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={handleClick}
|
||||
type="submit"
|
||||
id="checkAndSaveBtn"
|
||||
disabled={readonly}
|
||||
>
|
||||
Check & Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
</BaseModal.Content>
|
||||
</BaseModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import _ from "lodash";
|
||||
import _, { cloneDeep } from "lodash";
|
||||
import { MouseEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
|
|
@ -32,6 +32,7 @@ import {
|
|||
isValidConnection,
|
||||
reconnectEdges,
|
||||
scapeJSONParse,
|
||||
updateIds,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
|
|
@ -97,6 +98,56 @@ export default function Page({
|
|||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const selectedNode = nodes.filter((obj) => obj.selected);
|
||||
if (
|
||||
selectionMenuVisible &&
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "g"
|
||||
) {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
if (validateSelection(lastSelection!, edges).length === 0) {
|
||||
const clonedNodes = cloneDeep(nodes);
|
||||
const clonedEdges = cloneDeep(edges);
|
||||
const clonedSelection = cloneDeep(lastSelection);
|
||||
updateIds(
|
||||
{ nodes: clonedNodes, edges: clonedEdges },
|
||||
clonedSelection!
|
||||
);
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
clonedSelection!,
|
||||
clonedNodes,
|
||||
clonedEdges,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(newFlow, getNodeId);
|
||||
const newEdges = reconnectEdges(newGroupNode, removedEdges);
|
||||
setNodes([
|
||||
...clonedNodes.filter(
|
||||
(oldNodes) =>
|
||||
!clonedSelection?.nodes.some(
|
||||
(selectionNode) => selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges([
|
||||
...clonedEdges.filter(
|
||||
(oldEdge) =>
|
||||
!clonedSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
)
|
||||
),
|
||||
...newEdges,
|
||||
]);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: INVALID_SELECTION_ERROR_ALERT,
|
||||
list: validateSelection(lastSelection!, edges),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "p" &&
|
||||
|
|
@ -201,7 +252,7 @@ export default function Page({
|
|||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, [lastCopiedSelection, lastSelection, takeSnapshot]);
|
||||
}, [lastCopiedSelection, lastSelection, takeSnapshot, selectionMenuVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance && currentFlowId) {
|
||||
|
|
@ -437,10 +488,17 @@ export default function Page({
|
|||
if (
|
||||
validateSelection(lastSelection!, edges).length === 0
|
||||
) {
|
||||
const clonedNodes = cloneDeep(nodes);
|
||||
const clonedEdges = cloneDeep(edges);
|
||||
const clonedSelection = cloneDeep(lastSelection);
|
||||
updateIds(
|
||||
{ nodes: clonedNodes, edges: clonedEdges },
|
||||
clonedSelection!
|
||||
);
|
||||
const { newFlow, removedEdges } = generateFlow(
|
||||
lastSelection!,
|
||||
nodes,
|
||||
edges,
|
||||
clonedSelection!,
|
||||
clonedNodes,
|
||||
clonedEdges,
|
||||
getRandomName()
|
||||
);
|
||||
const newGroupNode = generateNodeFromFlow(
|
||||
|
|
@ -451,20 +509,20 @@ export default function Page({
|
|||
newGroupNode,
|
||||
removedEdges
|
||||
);
|
||||
setNodes((oldNodes) => [
|
||||
...oldNodes.filter(
|
||||
setNodes([
|
||||
...clonedNodes.filter(
|
||||
(oldNodes) =>
|
||||
!lastSelection?.nodes.some(
|
||||
!clonedSelection?.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldNodes.id
|
||||
)
|
||||
),
|
||||
newGroupNode,
|
||||
]);
|
||||
setEdges((oldEdges) => [
|
||||
...oldEdges.filter(
|
||||
setEdges([
|
||||
...clonedEdges.filter(
|
||||
(oldEdge) =>
|
||||
!lastSelection!.nodes.some(
|
||||
!clonedSelection!.nodes.some(
|
||||
(selectionNode) =>
|
||||
selectionNode.id === oldEdge.target ||
|
||||
selectionNode.id === oldEdge.source
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { cloneDeep, zip } from "lodash";
|
||||
import {
|
||||
Edge,
|
||||
EdgeChange,
|
||||
|
|
@ -26,6 +26,7 @@ import {
|
|||
ChatOutputType,
|
||||
FlowPoolObjectType,
|
||||
FlowStoreType,
|
||||
VertexLayerElementType,
|
||||
chatInputType,
|
||||
} from "../types/zustand/flow";
|
||||
import { buildVertices } from "../utils/buildUtils";
|
||||
|
|
@ -36,6 +37,7 @@ import {
|
|||
getNodeId,
|
||||
scapeJSONParse,
|
||||
scapedJSONStringfy,
|
||||
updateGroupRecursion,
|
||||
validateNodes,
|
||||
} from "../utils/reactflowUtils";
|
||||
import { getInputsAndOutputs } from "../utils/storeUtils";
|
||||
|
|
@ -221,6 +223,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
);
|
||||
},
|
||||
paste: (selection, position) => {
|
||||
function updateGroup() {}
|
||||
|
||||
if (
|
||||
selection.nodes.some((node) => node.data.type === "ChatInput") &&
|
||||
checkChatInput(get().nodes)
|
||||
|
|
@ -257,6 +261,8 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
let newId = getNodeId(node.data.type);
|
||||
idsMap[node.id] = newId;
|
||||
|
||||
updateGroupRecursion(node, selection.edges);
|
||||
|
||||
// Create a new node object
|
||||
const newNode: NodeType = {
|
||||
id: newId,
|
||||
|
|
@ -459,9 +465,18 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
// verticesLayers is a list of list of vertices ids, where each list is a layer of vertices
|
||||
// we want to add a new layer (next_vertices_ids) to the list of layers (verticesLayers)
|
||||
// and the values of next_vertices_ids to the list of vertices ids (verticesIds)
|
||||
|
||||
// const nextVertices will be the zip of vertexBuildData.next_vertices_ids and
|
||||
// vertexBuildData.top_level_vertices
|
||||
// the VertexLayerElementType as {id: next_vertices_id, layer: top_level_vertex}
|
||||
const nextVertices: VertexLayerElementType[] = zip(
|
||||
vertexBuildData.next_vertices_ids,
|
||||
vertexBuildData.top_level_vertices
|
||||
).map(([id, reference]) => ({ id: id!, reference }));
|
||||
|
||||
const newLayers = [
|
||||
...get().verticesBuild!.verticesLayers,
|
||||
vertexBuildData.next_vertices_ids,
|
||||
nextVertices,
|
||||
];
|
||||
const newIds = [
|
||||
...get().verticesBuild!.verticesIds,
|
||||
|
|
@ -473,7 +488,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
runId: runId,
|
||||
});
|
||||
get().updateBuildStatus(
|
||||
vertexBuildData.next_vertices_ids,
|
||||
vertexBuildData.top_level_vertices,
|
||||
BuildStatus.TO_BUILD
|
||||
);
|
||||
}
|
||||
|
|
@ -508,12 +523,19 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
get().setIsBuilding(false);
|
||||
},
|
||||
onBuildUpdate: handleBuildUpdate,
|
||||
onBuildError: (title, list, idList) => {
|
||||
onBuildError: (title: string, list: string[], elementList) => {
|
||||
const idList = elementList
|
||||
.map((element) => element.id)
|
||||
.filter(Boolean) as string[];
|
||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILT);
|
||||
setErrorData({ list, title });
|
||||
get().setIsBuilding(false);
|
||||
},
|
||||
onBuildStart: (idList) => {
|
||||
onBuildStart: (elementList) => {
|
||||
const idList = elementList
|
||||
// reference is the id of the vertex or the id of the parent in a group node
|
||||
.map((element) => element.reference)
|
||||
.filter(Boolean) as string[];
|
||||
useFlowStore.getState().updateBuildStatus(idList, BuildStatus.BUILDING);
|
||||
},
|
||||
validateNodes: validateSubgraph,
|
||||
|
|
@ -531,7 +553,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
updateVerticesBuild: (
|
||||
vertices: {
|
||||
verticesIds: string[];
|
||||
verticesLayers: string[][];
|
||||
verticesLayers: VertexLayerElementType[][];
|
||||
runId: string;
|
||||
} | null
|
||||
) => {
|
||||
|
|
@ -562,6 +584,7 @@ const useFlowStore = create<FlowStoreType>((set, get) => ({
|
|||
},
|
||||
updateBuildStatus: (nodeIdList: string[], status: BuildStatus) => {
|
||||
const newFlowBuildStatus = { ...get().flowBuildStatus };
|
||||
console.log("newFlowBuildStatus", newFlowBuildStatus);
|
||||
nodeIdList.forEach((id) => {
|
||||
newFlowBuildStatus[id] = {
|
||||
status,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
import { AxiosError } from "axios";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { cloneDeep, debounce } from "lodash";
|
||||
import { Edge, Node, Viewport, XYPosition } from "reactflow";
|
||||
import { create } from "zustand";
|
||||
import { STARTER_FOLDER_NAME } from "../constants/constants";
|
||||
import {
|
||||
SAVE_DEBOUNCE_TIME,
|
||||
STARTER_FOLDER_NAME,
|
||||
} from "../constants/constants";
|
||||
import {
|
||||
deleteFlowFromDatabase,
|
||||
readFlowsFromDatabase,
|
||||
|
|
@ -92,22 +95,18 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
|||
});
|
||||
});
|
||||
},
|
||||
autoSaveCurrentFlow: (nodes: Node[], edges: Edge[], viewport: Viewport) => {
|
||||
// Clear the previous timeout if it exists.
|
||||
if (saveTimeoutId) {
|
||||
clearTimeout(saveTimeoutId);
|
||||
}
|
||||
set({ saveLoading: true });
|
||||
// Set up a new timeout.
|
||||
saveTimeoutId = setTimeout(() => {
|
||||
autoSaveCurrentFlow: debounce(
|
||||
(nodes: Node[], edges: Edge[], viewport: Viewport) => {
|
||||
set({ saveLoading: true });
|
||||
if (get().currentFlow) {
|
||||
get().saveFlow(
|
||||
{ ...get().currentFlow!, data: { nodes, edges, viewport } },
|
||||
true
|
||||
);
|
||||
}
|
||||
}, 500); // Delay of 500ms because chat message depends on it.
|
||||
},
|
||||
},
|
||||
SAVE_DEBOUNCE_TIME
|
||||
),
|
||||
saveFlow: (flow: FlowType, silent?: boolean) => {
|
||||
set({ saveLoading: true });
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ export type VertexBuildTypeAPI = {
|
|||
id: string;
|
||||
inactivated_vertices: Array<string> | null;
|
||||
next_vertices_ids: Array<string>;
|
||||
top_level_vertices: Array<string>;
|
||||
run_id: string;
|
||||
valid: boolean;
|
||||
params: string;
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@ export type FlowPoolObjectType = {
|
|||
buildId: string;
|
||||
};
|
||||
|
||||
export type VertexLayerElementType = {
|
||||
id: string;
|
||||
reference?: string;
|
||||
};
|
||||
|
||||
export type FlowPoolType = {
|
||||
[key: string]: Array<FlowPoolObjectType>;
|
||||
};
|
||||
|
|
@ -103,7 +108,7 @@ export type FlowStoreType = {
|
|||
updateVerticesBuild: (
|
||||
vertices: {
|
||||
verticesIds: string[];
|
||||
verticesLayers: string[][];
|
||||
verticesLayers: VertexLayerElementType[][];
|
||||
runId: string;
|
||||
} | null
|
||||
) => void;
|
||||
|
|
@ -111,7 +116,7 @@ export type FlowStoreType = {
|
|||
removeFromVerticesBuild: (vertices: string[]) => void;
|
||||
verticesBuild: {
|
||||
verticesIds: string[];
|
||||
verticesLayers: string[][];
|
||||
verticesLayers: VertexLayerElementType[][];
|
||||
runId: string;
|
||||
} | null;
|
||||
updateBuildStatus: (nodeId: string[], status: BuildStatus) => void;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { getVerticesOrder, postBuildVertex } from "../controllers/API";
|
|||
import useAlertStore from "../stores/alertStore";
|
||||
import useFlowStore from "../stores/flowStore";
|
||||
import { VertexBuildTypeAPI } from "../types/api";
|
||||
import { VertexLayerElementType } from "../types/zustand/flow";
|
||||
|
||||
type BuildVerticesParams = {
|
||||
flowId: string; // Assuming FlowType is the type for your flow
|
||||
|
|
@ -17,8 +18,8 @@ type BuildVerticesParams = {
|
|||
buildId: string
|
||||
) => void; // Replace any with the actual type if it's not any
|
||||
onBuildComplete?: (allNodesValid: boolean) => void;
|
||||
onBuildError?: (title, list, idList: string[]) => void;
|
||||
onBuildStart?: (idList: string[]) => void;
|
||||
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
|
||||
onBuildStart?: (idList: VertexLayerElementType[]) => void;
|
||||
validateNodes?: (nodes: string[]) => void;
|
||||
};
|
||||
|
||||
|
|
@ -35,6 +36,7 @@ function getInactiveVertexData(vertexId: string): VertexBuildTypeAPI {
|
|||
inactivated_vertices: null,
|
||||
run_id: "",
|
||||
next_vertices_ids: [],
|
||||
top_level_vertices: [],
|
||||
inactive_vertices: null,
|
||||
valid: false,
|
||||
timestamp: new Date().toISOString(),
|
||||
|
|
@ -48,7 +50,7 @@ export async function updateVerticesOrder(
|
|||
startNodeId?: string | null,
|
||||
stopNodeId?: string | null
|
||||
): Promise<{
|
||||
verticesLayers: string[][];
|
||||
verticesLayers: VertexLayerElementType[][];
|
||||
verticesIds: string[];
|
||||
runId: string;
|
||||
}> {
|
||||
|
|
@ -66,7 +68,14 @@ export async function updateVerticesOrder(
|
|||
useFlowStore.getState().setIsBuilding(false);
|
||||
throw new Error("Invalid nodes");
|
||||
}
|
||||
let verticesLayers: Array<Array<string>> = [orderResponse.data.ids];
|
||||
// orderResponse.data.ids,
|
||||
// for each id we need to build the VertexLayerElementType object as
|
||||
// {id: id, reference: id}
|
||||
let verticesLayers: Array<Array<VertexLayerElementType>> =
|
||||
orderResponse.data.ids.map((id: string) => {
|
||||
return [{ id: id, reference: id }];
|
||||
});
|
||||
|
||||
const runId = orderResponse.data.run_id;
|
||||
// if (nodeId) {
|
||||
// for (let i = 0; i < verticesOrder.length; i += 1) {
|
||||
|
|
@ -160,17 +169,17 @@ export async function buildVertices({
|
|||
if (onBuildStart) onBuildStart(currentLayer);
|
||||
// Build each vertex in the current layer
|
||||
await Promise.all(
|
||||
currentLayer.map(async (vertexId) => {
|
||||
currentLayer.map(async (element) => {
|
||||
// Check if id is in the list of inactive nodes
|
||||
if (
|
||||
!useFlowStore
|
||||
.getState()
|
||||
.verticesBuild?.verticesIds.includes(vertexId) &&
|
||||
.verticesBuild?.verticesIds.includes(element.id) &&
|
||||
onBuildUpdate
|
||||
) {
|
||||
// If it is, skip building and set the state to inactive
|
||||
onBuildUpdate(
|
||||
getInactiveVertexData(vertexId),
|
||||
getInactiveVertexData(element.id),
|
||||
BuildStatus.INACTIVE,
|
||||
runId
|
||||
);
|
||||
|
|
@ -181,7 +190,7 @@ export async function buildVertices({
|
|||
// Build the vertex
|
||||
await buildVertex({
|
||||
flowId,
|
||||
id: vertexId,
|
||||
id: element.id,
|
||||
input_value,
|
||||
onBuildUpdate: (data: VertexBuildTypeAPI, status: BuildStatus) => {
|
||||
if (onBuildUpdate) onBuildUpdate(data, status, runId);
|
||||
|
|
@ -226,7 +235,7 @@ async function buildVertex({
|
|||
id: string;
|
||||
input_value: string;
|
||||
onBuildUpdate?: (data: any, status: BuildStatus) => void;
|
||||
onBuildError?: (title, list, idList: string[]) => void;
|
||||
onBuildError?: (title, list, idList: VertexLayerElementType[]) => void;
|
||||
verticesIds: string[];
|
||||
buildResults: boolean[];
|
||||
stopBuild: () => void;
|
||||
|
|
@ -240,7 +249,7 @@ async function buildVertex({
|
|||
onBuildError!(
|
||||
"Error Building Component",
|
||||
[buildData.params],
|
||||
verticesIds
|
||||
verticesIds.map((id) => ({ id }))
|
||||
);
|
||||
stopBuild();
|
||||
}
|
||||
|
|
@ -251,7 +260,7 @@ async function buildVertex({
|
|||
onBuildError!(
|
||||
"Error Building Component",
|
||||
[(error as AxiosError<any>).response?.data?.detail ?? "Unknown Error"],
|
||||
verticesIds
|
||||
verticesIds.map((id) => ({ id }))
|
||||
);
|
||||
stopBuild();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,23 +204,33 @@ export const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
|
|||
return data;
|
||||
};
|
||||
|
||||
export function updateIds(newFlow: ReactFlowJsonObject) {
|
||||
export function updateIds(
|
||||
{ edges, nodes }: { edges: Edge[]; nodes: Node[] },
|
||||
selection?: { edges: Edge[]; nodes: Node[] }
|
||||
) {
|
||||
let idsMap = {};
|
||||
|
||||
if (newFlow.nodes)
|
||||
newFlow.nodes.forEach((node: NodeType) => {
|
||||
const selectionIds = selection?.nodes.map((n) => n.id);
|
||||
if (nodes) {
|
||||
nodes.forEach((node: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId(
|
||||
node.data.node?.flow ? "GroupNode" : node.data.type
|
||||
);
|
||||
let newId = getNodeId(node.data.type);
|
||||
if (selection && !selectionIds?.includes(node.id)) {
|
||||
newId = node.id;
|
||||
}
|
||||
idsMap[node.id] = newId;
|
||||
node.id = newId;
|
||||
node.data.id = newId;
|
||||
// Add the new node to the list of nodes in state
|
||||
});
|
||||
|
||||
if (newFlow.edges)
|
||||
newFlow.edges.forEach((edge: Edge) => {
|
||||
selection?.nodes.forEach((sNode: NodeType) => {
|
||||
let newId = idsMap[sNode.id];
|
||||
sNode.id = newId;
|
||||
sNode.data.id = newId;
|
||||
});
|
||||
}
|
||||
const concatedEdges = [...edges, ...(selection?.edges ?? [])];
|
||||
if (concatedEdges)
|
||||
concatedEdges.forEach((edge: Edge) => {
|
||||
edge.source = idsMap[edge.source];
|
||||
edge.target = idsMap[edge.target];
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
|
|
@ -273,6 +283,8 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
|||
node: { template },
|
||||
} = node.data;
|
||||
|
||||
const displayName = node.data.node.display_name;
|
||||
|
||||
return Object.keys(template).reduce((errors: Array<string>, t) => {
|
||||
if (
|
||||
template[t].required &&
|
||||
|
|
@ -288,7 +300,9 @@ export function validateNode(node: NodeType, edges: Edge[]): Array<string> {
|
|||
node.id
|
||||
)
|
||||
) {
|
||||
errors.push(`${type} is missing ${getFieldTitle(template, t)}.`);
|
||||
errors.push(
|
||||
`${displayName || type} is missing ${getFieldTitle(template, t)}.`
|
||||
);
|
||||
} else if (
|
||||
template[t].type === "dict" &&
|
||||
template[t].required &&
|
||||
|
|
@ -633,8 +647,8 @@ export function generateFlow(
|
|||
}
|
||||
|
||||
export function reconnectEdges(groupNode: NodeType, excludedEdges: Edge[]) {
|
||||
let newEdges = cloneDeep(excludedEdges);
|
||||
if (!groupNode.data.node!.flow) return [];
|
||||
let newEdges = cloneDeep(excludedEdges);
|
||||
const { nodes, edges } = groupNode.data.node!.flow!.data!;
|
||||
const lastNode = findLastNode(groupNode.data.node!.flow!.data!);
|
||||
newEdges.forEach((edge) => {
|
||||
|
|
@ -951,7 +965,7 @@ export function connectedInputNodesOnHandle(
|
|||
return connectedNodes;
|
||||
}
|
||||
|
||||
function updateProxyIdsOnTemplate(
|
||||
export function updateProxyIdsOnTemplate(
|
||||
template: APITemplateType,
|
||||
idsMap: { [key: string]: string }
|
||||
) {
|
||||
|
|
@ -962,12 +976,16 @@ function updateProxyIdsOnTemplate(
|
|||
});
|
||||
}
|
||||
|
||||
function updateEdgesIds(edges: Edge[], idsMap: { [key: string]: string }) {
|
||||
export function updateEdgesIds(
|
||||
edges: Edge[],
|
||||
idsMap: { [key: string]: string }
|
||||
) {
|
||||
edges.forEach((edge) => {
|
||||
let targetHandle: targetHandleType = edge.data.targetHandle;
|
||||
if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
|
||||
targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
|
||||
}
|
||||
console.log("edge", edge);
|
||||
edge.data.targetHandle = targetHandle;
|
||||
edge.targetHandle = scapedJSONStringfy(targetHandle);
|
||||
});
|
||||
|
|
@ -1255,3 +1273,18 @@ export function isInputType(type: string): boolean {
|
|||
export function isOutputType(type: string): boolean {
|
||||
return OUTPUT_TYPES.has(type);
|
||||
}
|
||||
|
||||
export function updateGroupRecursion(groupNode: NodeType, edges: Edge[]) {
|
||||
if (groupNode.data.node?.flow) {
|
||||
groupNode.data.node.flow.data!.nodes.forEach((node) => {
|
||||
if (node.data.node?.flow) {
|
||||
updateGroupRecursion(node, node.data.node.flow.data!.edges);
|
||||
}
|
||||
});
|
||||
let newFlow = groupNode.data.node!.flow;
|
||||
const idsMap = updateIds(newFlow.data!);
|
||||
updateProxyIdsOnTemplate(groupNode.data.node!.template, idsMap);
|
||||
let flowEdges = edges;
|
||||
updateEdgesIds(flowEdges, idsMap);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -705,3 +705,8 @@ export function sortFields(a, b, fieldOrder) {
|
|||
// You might want to sort them alphabetically or in another specific manner
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
export function freezeObject(obj: any) {
|
||||
if (!obj) return obj;
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue