Bot
Initial commit for HF Spaces
05c5ed5
Raw
History Blame Contribute Delete
10.1 kB
"use client";
import { useWorkflowStore } from "@/app/store/workflow.store";
import { DefaultNode } from "@/components/workflow/default-node";
import { WorkflowPanel } from "@/components/workflow/workflow-panel";
import {
ReactFlow,
Background,
Panel,
Edge,
applyNodeChanges,
OnNodesChange,
OnEdgesChange,
applyEdgeChanges,
addEdge,
OnConnect,
OnSelectionChangeFunc,
NodeMouseHandler,
IsValidConnection,
Connection,
} from "@xyflow/react";
import "@xyflow/react/dist/style.css";
import { DBWorkflow } from "app-types/workflow";
import { extractWorkflowDiff } from "lib/ai/workflow/extract-workflow-diff";
import {
convertUIEdgeToDBEdge,
convertUINodeToDBNode,
} from "lib/ai/workflow/shared.workflow";
import { NodeKind, UINode } from "lib/ai/workflow/workflow.interface";
import { wouldCreateCycle } from "lib/ai/workflow/would-create-cycle";
import { createDebounce, fetcher, generateUUID } from "lib/utils";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import useSWR from "swr";
import { safe } from "ts-safe";
const nodeTypes = {
default: DefaultNode,
};
const debounce = createDebounce();
const fitViewOptions = {
duration: 500,
padding: 1,
};
export default function Workflow({
initialNodes,
initialEdges,
workflowId,
hasEditAccess,
}: {
workflowId: string;
initialNodes: UINode[];
hasEditAccess?: boolean;
initialEdges: Edge[];
}) {
const { init, addProcess, processIds } = useWorkflowStore();
const [nodes, setNodes] = useState<UINode[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges);
const isProcessing = useMemo(
() => processIds.length > 0,
[processIds.length],
);
const { data: workflow } = useSWR<DBWorkflow>(
`/api/workflow/${workflowId}`,
fetcher,
{
onSuccess: (workflow) => {
init(workflow, hasEditAccess);
},
},
);
const [activeNodeIds, setActiveNodeIds] = useState<string[]>([]);
const snapshot = useRef({ nodes: initialNodes, edges: initialEdges });
const editable = useMemo(() => {
return !isProcessing && hasEditAccess && !workflow?.isPublished;
}, [isProcessing, hasEditAccess, workflow?.isPublished]);
const save = async () => {
if (workflow?.isPublished) return;
const diff = extractWorkflowDiff(snapshot.current, { nodes, edges });
if (
diff.deleteEdges.length ||
diff.deleteNodes.length ||
diff.updateEdges.length ||
diff.updateNodes.length
) {
const stop = addProcess();
await safe()
.map(() => saveWorkflow(workflowId, diff))
.ifOk(() => {
snapshot.current = {
edges,
nodes,
};
})
.ifFail(() => {
window.location.reload();
})
.watch(stop)
.unwrap();
}
};
const selectedNode = useMemo(() => {
return nodes.findLast((node) => node.selected);
}, [nodes]);
const onNodesChange: OnNodesChange = useCallback(
(changes) => {
if (!editable) {
setNodes((nds) => {
let updatedNodes = nds;
changes.forEach((change) => {
if (change.type === "select") {
updatedNodes = applyNodeChanges(
[change],
updatedNodes,
) as UINode[];
} else if (change.type === "replace" && "item" in change) {
const newNode = change.item as UINode;
updatedNodes = updatedNodes.map((node) => {
if (node.id === change.id) {
return {
...node,
data: {
...node.data,
runtime: newNode.data.runtime,
},
};
}
return node;
});
}
});
return updatedNodes;
});
return;
}
setNodes((nds) => applyNodeChanges(changes, nds) as UINode[]);
},
[editable],
);
const onEdgesChange: OnEdgesChange = useCallback(
(changes) => {
if (!editable) return;
setEdges((eds) => applyEdgeChanges(changes, eds));
},
[editable],
);
const onConnect: OnConnect = useCallback(
(connection) => {
if (!editable) return;
setEdges((eds) =>
addEdge(
{
...connection,
id: generateUUID(),
},
eds,
),
);
},
[editable],
);
const onSelectionChange: OnSelectionChangeFunc = useCallback(
({ nodes: selectedNodes }) => {
setActiveNodeIds(selectedNodes.map((node) => node.id));
},
[],
);
const onNodeMouseEnter: NodeMouseHandler = useCallback((_, node) => {
setActiveNodeIds((prev) => {
return prev.includes(node.id) ? prev : [...prev, node.id];
});
}, []);
const onNodeMouseLeave: NodeMouseHandler = useCallback((_, node) => {
setActiveNodeIds((prev) => prev.filter((id) => id !== node.id));
}, []);
const isValidConnection: IsValidConnection = useCallback(
(connection) => {
if (!editable) return false;
if (connection.source === connection.target) return false;
return !wouldCreateCycle(connection as Connection, edges as Connection[]);
},
[editable, edges],
);
const { errorIds, runningIds, successIds } = useMemo(() => {
return nodes.reduce(
(acc, prev) => {
if (prev.data.runtime?.status === "fail") {
acc.errorIds.push(prev.id);
}
if (prev.data.runtime?.status === "running") {
acc.runningIds.push(prev.id);
}
if (prev.data.runtime?.status === "success") {
acc.successIds.push(prev.id);
}
return acc;
},
{
errorIds: [] as string[],
runningIds: [] as string[],
successIds: [] as string[],
},
);
}, [nodes]);
const styledEdges = useMemo(() => {
return edges.map((edge) => {
const isConnected =
activeNodeIds.includes(edge.source) ||
activeNodeIds.includes(edge.target);
const isErrorEdge =
errorIds.includes(edge.target) &&
(successIds.includes(edge.source) || errorIds.includes(edge.source));
const isRunningEdge =
runningIds.includes(edge.target) && successIds.includes(edge.source);
const isSuccessEdge =
successIds.includes(edge.target) &&
(successIds.includes(edge.source) || runningIds.includes(edge.source));
return {
...edge,
style: {
...edge.style,
stroke: isErrorEdge
? "var(--destructive)"
: isRunningEdge || isSuccessEdge
? "#05df72"
: isConnected
? "oklch(62.3% 0.214 259.815)"
: undefined,
strokeWidth: 2,
transition: "stroke 0.3s",
},
animated: runningIds.includes(edge.source),
};
});
}, [edges, activeNodeIds, errorIds, runningIds]);
useEffect(() => {
const debounceDelay =
snapshot.current.nodes.length !== nodes.length ||
snapshot.current.edges.length !== edges.length
? 200
: 10000;
debounce(save, debounceDelay);
}, [nodes, edges]);
useEffect(() => {
setNodes((nds) => {
return nds.map((node) => {
if (node.data.kind === NodeKind.Input && !node.selected) {
return { ...node, selected: true };
}
return node;
});
});
}, []);
useEffect(() => {
init(workflow, hasEditAccess);
}, [workflow, hasEditAccess]);
return (
<div className="w-full h-full relative text-de text-gree-4">
<ReactFlow
fitView
deleteKeyCode={null}
nodes={nodes}
maxZoom={1.4}
minZoom={0.1}
edges={styledEdges}
multiSelectionKeyCode={null}
id={workflowId}
nodeTypes={nodeTypes}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onSelectionChange={onSelectionChange}
isValidConnection={isValidConnection}
onConnect={onConnect}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseLeave={onNodeMouseLeave}
fitViewOptions={fitViewOptions}
>
<Background gap={12} size={0.6} />
<Panel position="top-right" className="z-20!">
{workflow && (
<WorkflowPanel
hasEditAccess={hasEditAccess}
addProcess={addProcess}
onSave={save}
selectedNode={selectedNode}
workflow={workflow}
isProcessing={isProcessing}
/>
)}
</Panel>
<Panel
position="top-left"
className="h-full w-full m-0! pointer-events-none!"
>
<div className="z-10 absolute inset-0 w-full h-1/12 bg-gradient-to-b to-90% from-background to-transparent pointer-events-none" />
<div className="z-10 absolute inset-0 w-1/12 h-full bg-gradient-to-r from-background to-transparent pointer-events-none" />
<div className="z-10 absolute left-0 bottom-0 w-full h-1/12 bg-gradient-to-t from-background to-transparent pointer-events-none" />
<div className="z-10 absolute right-0 bottom-0 w-1/12 h-full bg-gradient-to-l from-background to-transparent pointer-events-none" />
</Panel>
</ReactFlow>
</div>
);
}
function saveWorkflow(
workflowId: string,
diff: ReturnType<typeof extractWorkflowDiff>,
) {
return fetch(`/api/workflow/${workflowId}/structure`, {
method: "POST",
body: JSON.stringify({
nodes: diff.updateNodes.map((node) =>
convertUINodeToDBNode(workflowId, node),
),
edges: diff.updateEdges.map((edge) =>
convertUIEdgeToDBEdge(workflowId, edge),
),
deleteNodes: diff.deleteNodes.map((node) => node.id),
deleteEdges: diff.deleteEdges.map((edge) => edge.id),
}),
}).then((res) => {
if (res.status >= 400) {
throw new Error(String(res.statusText || res.status || "Error"));
}
});
}