import { useWorkflowStore } from "@/app/store/workflow.store"; import { NodeKind, NodeRuntimeHistory, } from "lib/ai/workflow/workflow.interface"; import { useReactFlow } from "@xyflow/react"; import { useObjectState } from "@/hooks/use-object-state"; import { UINode } from "lib/ai/workflow/workflow.interface"; import { cn, createDebounce, errorToString } from "lib/utils"; import { useCallback, useMemo, useRef, useState } from "react"; import { GraphEndEvent } from "ts-edge"; import { allNodeValidate } from "lib/ai/workflow/node-validate"; import { toast } from "sonner"; import { decodeWorkflowEvents } from "lib/ai/workflow/shared.workflow"; import { Alert, AlertDescription, AlertTitle } from "ui/alert"; import { AlertTriangleIcon, Loader, Loader2, Copy, Check, WandSparklesIcon, XIcon, Maximize2, } from "lucide-react"; import JsonView from "ui/json-view"; import { Button } from "ui/button"; import { Separator } from "ui/separator"; import { FlipWords } from "ui/flip-words"; import { Label } from "ui/label"; import { Input } from "ui/input"; import { Switch } from "ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "ui/select"; import { Textarea } from "ui/textarea"; import { NodeIcon } from "../node-icon"; import { TextShimmer } from "ui/text-shimmer"; import { generateObjectAction } from "@/app/api/chat/actions"; import { appStore } from "@/app/store"; import { notify } from "lib/notify"; import { SelectModel } from "@/components/select-model"; import { useCopy } from "@/hooks/use-copy"; import { NodeResultPopup } from "../node-result-popup"; import { useTranslations } from "next-intl"; const debounce = createDebounce(); export function ExecuteTab({ close, onSave, }: { close: () => void; onSave: () => Promise; }) { const { addProcess, processIds, workflow } = useWorkflowStore(); const tabs = useMemo( () => [ { label: "Input", value: "input", }, { label: "Result", value: "result", }, ], [], ); const [tab, setTab] = useState<(typeof tabs)[number]["value"]>(tabs[0].value); const t = useTranslations(); const [isRunning, setIsRunning] = useState(false); const [histories, setHistories] = useState([]); const [result, setResult] = useState(); const { copied, copy } = useCopy(); const isProcessing = useMemo( () => Boolean(processIds.length), [processIds.length], ); const { getEdges, getNodes, fitView, getNode, updateNodeData, setNodes } = useReactFlow(); const nodes = getNodes(); const historyRef = useRef(null); const [query, setQuery] = useObjectState({} as Record); const startNodeData = useMemo(() => { return nodes.find((node) => node.data.kind === NodeKind.Input)!.data; }, [nodes]); const inputSchema = useMemo(() => { return startNodeData.outputSchema; }, [startNodeData]); const inputSchemaIterator = useMemo(() => { return Object.entries(inputSchema.properties ?? {}); }, [inputSchema]); const handleGenerateInputWithAI = useCallback(async () => { let model = appStore.getState().chatModel; const result = await notify.prompt({ title: t("Common.generateInputWithAI"), description: (

{t("Workflow.generateInputWithAIDescription")}

{ model = m; }} />
), }); if (!result) return; toast.promise( generateObjectAction({ model, prompt: { system: `You are a parameter generator for tool execution. Analyze the user's request and generate creative JSON data that matches the provided schema. If information cannot be inferred from the user's question, use your creativity to generate engaging data. Fill all required fields and return only valid JSON without explanations. tool-name: ${workflow!.name} ${workflow!.description ? `tool-description: ${workflow!.description}` : ""}`, user: result, }, schema: inputSchema, }).then((res) => { setQuery(res); }), { loading: t("Common.generatingInputWithAI"), success: t("Common.inputGeneratedSuccessfully"), error: t("Common.failedToGenerateInput"), }, ); }, [inputSchema]); const handleClick = async () => { await onSave(); const failSchema = inputSchemaIterator.find(([key]) => { if (inputSchema.required?.includes(key) && query[key] === undefined) return true; }); if (failSchema) { return toast.warning(`${failSchema[0]} is Empty`); } const validateResult = allNodeValidate({ nodes, edges: getEdges(), }); if (validateResult !== true) { if (validateResult.node) { setNodes((nds) => { return nds.map((node) => { if (node.id === validateResult.node?.id) { return { ...node, selected: true }; } if (node.selected) { return { ...node, selected: false }; } return node; }); }); } return toast.warning(validateResult.errorMessage); } run(query); }; const fitviewWithDebounce = useCallback((id: string) => { const node = getNode(id); if (!node) return; const nextNodes = getEdges() .filter((edge) => edge.source == id) .map((edge) => getNode(edge.target)) .filter(Boolean) as UINode[]; const fitviewNodes = [node, ...nextNodes]; debounce(() => { fitView({ duration: 300, nodes: fitviewNodes, maxZoom: 1.8, }); }, 300); }, []); const run = useCallback( async (query: Record) => { const stop = addProcess(); const abortController = new AbortController(); setHistories([]); setIsRunning(true); setNodes((nds) => { return nds.map((node) => { if (node.data.runtime?.status) { return { ...node, data: { ...node.data, runtime: { status: undefined } }, }; } return node; }); }); try { const response = await fetch(`/api/workflow/${workflow!.id}/execute`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ query }), signal: abortController.signal, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body?.getReader(); if (!reader) { throw new Error("No readable stream available"); } const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) { break; } buffer += decoder.decode(value, { stream: true }); const { events, remainingBuffer } = decodeWorkflowEvents(buffer); buffer = remainingBuffer; for (const event of events) { switch (event.eventType) { case "WORKFLOW_START": setTab("result"); break; case "WORKFLOW_END": setResult(event); stop(); break; case "NODE_START": { fitviewWithDebounce(event.node.name); historyRef.current?.scrollTo({ top: historyRef.current?.scrollHeight, behavior: "smooth", }); updateNodeData(event.node.name, { runtime: { status: "running" }, }); setHistories((prev) => { const uiNode = getNode(event.node.name); if (!uiNode) return prev; return [ ...prev, { nodeId: event.node.name, startedAt: Date.now(), id: event.nodeExecutionId, name: uiNode.data.name, kind: uiNode.data.kind, status: "running", }, ]; }); break; } case "NODE_END": { updateNodeData(event.node.name, { runtime: { status: event.isOk ? "success" : "fail" }, }); setHistories((prev) => { const prevHistory = prev.find( (h) => h.id === event.nodeExecutionId, ); if (!prevHistory) return prev; return prev.map((n) => { if (n != prevHistory) return n; const source = event.isOk ? event.node.output : event.node.input; return { ...prevHistory, endedAt: Date.now(), status: event.isOk ? "success" : "fail", error: event.error, result: { output: source?.outputs?.[prevHistory.nodeId], input: source?.inputs?.[prevHistory.nodeId], }, } as NodeRuntimeHistory; }); }); } } } } } finally { reader.releaseLock(); stop(); } } catch (error) { if (error instanceof Error && error.name === "AbortError") { console.log("Workflow execution was aborted"); } else { console.error("Workflow execution error:", error); } stop(); } finally { setIsRunning(false); } }, [workflow!.id], ); const lastOutput = useMemo(() => { const outputNodes = histories .filter((h) => h.kind == NodeKind.Output) .map((h) => h.result?.output) .filter(Boolean); if (outputNodes.length == 0) return undefined; if (outputNodes.length == 1) return outputNodes[0]; return outputNodes; }, [histories]); const resultView = useMemo(() => { if (isRunning) return; if (result?.isOk === false) return ( Error ); return (
); }, [isRunning, result]); return (
Test Run
{tabs.map((t) => ( ))}
{tab == tabs[0].value ? (
{inputSchemaIterator.length == 0 ? (
) : ( <>
{t("Common.generateInputWithAI")}
{inputSchemaIterator.map(([key, schema], i) => { return (
{schema.type == "number" ? ( setQuery({ ...query, [key]: Number(e.target.value) }) } /> ) : schema.type == "boolean" ? ( setQuery({ ...query, [key]: checked }) } /> ) : schema.type == "string" && schema.enum ? ( ) : schema.type == "string" ? (