{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "flow-routing",
	"type": "registry:block",
	"description": "Agentic routing workflow.",
	"dependencies": [
		"@xyflow/react",
		"zustand",
		"zod",
		"ai",
		"nanoid",
		"@ai-sdk/openai",
		"@ai-sdk/groq",
		"@ai-sdk/deepseek"
	],
	"registryDependencies": [
		"button",
		"card",
		"dialog",
		"input",
		"textarea",
		"sonner",
		"@simple-ai/generate-text-node",
		"@simple-ai/prompt-crafter-node",
		"@simple-ai/text-input-node",
		"@simple-ai/visualize-text-node"
	],
	"files": [
		{
			"path": "./src/registry/blocks/flow-routing/page.tsx",
			"content": "\"use client\";\n\nimport {\n\tBackground,\n\tControls,\n\ttype EdgeTypes,\n\tMiniMap,\n\ttype NodeTypes,\n\tPanel,\n\tReactFlow,\n\tReactFlowProvider,\n\tuseReactFlow,\n} from \"@xyflow/react\";\nimport { type DragEvent, useEffect } from \"react\";\nimport { shallow } from \"zustand/shallow\";\nimport \"@xyflow/react/dist/style.css\";\nimport { Button } from \"@/components/ui/button\";\nimport { ErrorIndicator } from \"@/registry/blocks/flow-routing/components/error-indicator\";\nimport { NodesPanel } from \"@/registry/blocks/flow-routing/components/nodes-panel\";\nimport { CONTENT_CREATOR_ROUTING_WORKFLOW } from \"@/registry/blocks/flow-routing/lib/content-creator-routing\";\nimport { useWorkflow } from \"@/registry/hooks/flow/use-workflow\";\nimport type { FlowNode } from \"@/registry/lib/flow/workflow\";\nimport { GenerateTextNodeController } from \"@/registry/ui/flow/generate-text-node-controller\";\nimport { PromptCrafterNodeController } from \"@/registry/ui/flow/prompt-crafter-node-controller\";\nimport { StatusEdgeController } from \"@/registry/ui/flow/status-edge-controller\";\nimport { TextInputNodeController } from \"@/registry/ui/flow/text-input-node-controller\";\nimport { VisualizeTextNodeController } from \"@/registry/ui/flow/visualize-text-node-controller\";\n\nconst nodeTypes: NodeTypes = {\n\t\"generate-text\": GenerateTextNodeController,\n\t\"visualize-text\": VisualizeTextNodeController,\n\t\"text-input\": TextInputNodeController,\n\t\"prompt-crafter\": PromptCrafterNodeController,\n};\n\nconst edgeTypes: EdgeTypes = {\n\tstatus: StatusEdgeController,\n};\n\nexport function Flow() {\n\tconst store = useWorkflow(\n\t\t(store) => ({\n\t\t\tnodes: store.nodes,\n\t\t\tedges: store.edges,\n\t\t\tonNodesChange: store.onNodesChange,\n\t\t\tonEdgesChange: store.onEdgesChange,\n\t\t\tonConnect: store.onConnect,\n\t\t\tstartExecution: store.startExecution,\n\t\t\tcreateNode: store.createNode,\n\t\t\tworkflowExecutionState: store.workflowExecutionState,\n\t\t\tinitializeWorkflow: store.initializeWorkflow,\n\t\t}),\n\t\tshallow,\n\t);\n\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: We want to initialize the workflow only once\n\tuseEffect(() => {\n\t\tstore.initializeWorkflow(\n\t\t\tCONTENT_CREATOR_ROUTING_WORKFLOW.nodes,\n\t\t\tCONTENT_CREATOR_ROUTING_WORKFLOW.edges,\n\t\t);\n\t}, []);\n\n\tconst { screenToFlowPosition } = useReactFlow();\n\n\tconst onDragOver = (event: DragEvent) => {\n\t\tevent.preventDefault();\n\t\tevent.dataTransfer.dropEffect = \"move\";\n\t};\n\n\tconst onDrop = (event: DragEvent) => {\n\t\tevent.preventDefault();\n\n\t\tconst type = event.dataTransfer.getData(\n\t\t\t\"application/reactflow\",\n\t\t) as FlowNode[\"type\"];\n\n\t\tif (!type) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst position = screenToFlowPosition({\n\t\t\tx: event.clientX,\n\t\t\ty: event.clientY,\n\t\t});\n\n\t\tstore.createNode(type, position);\n\t};\n\n\tconst onStartExecution = async () => {\n\t\tconst result = await store.startExecution();\n\t\tif (result.status === \"error\") {\n\t\t\tconsole.error(result.error);\n\t\t}\n\t};\n\n\treturn (\n\t\t<ReactFlow\n\t\t\tnodes={store.nodes}\n\t\t\tedges={store.edges}\n\t\t\tonNodesChange={store.onNodesChange}\n\t\t\tonEdgesChange={store.onEdgesChange}\n\t\t\tonConnect={store.onConnect}\n\t\t\tnodeTypes={nodeTypes}\n\t\t\tedgeTypes={edgeTypes}\n\t\t\tonDragOver={onDragOver}\n\t\t\tonDrop={onDrop}\n\t\t\tfitView\n\t\t>\n\t\t\t<Background />\n\t\t\t<Controls />\n\t\t\t<MiniMap />\n\t\t\t<NodesPanel />\n\t\t\t<Panel position=\"top-right\" className=\"flex gap-2 items-center\">\n\t\t\t\t<ErrorIndicator errors={store.workflowExecutionState.errors} />\n\t\t\t\t<Button\n\t\t\t\t\tonClick={onStartExecution}\n\t\t\t\t\ttitle={\n\t\t\t\t\t\tstore.workflowExecutionState.timesRun > 1\n\t\t\t\t\t\t\t? \"Disabled for now\"\n\t\t\t\t\t\t\t: \"Run the workflow\"\n\t\t\t\t\t}\n\t\t\t\t\tdisabled={\n\t\t\t\t\t\tstore.workflowExecutionState.errors.length > 0 ||\n\t\t\t\t\t\tstore.workflowExecutionState.isRunning ||\n\t\t\t\t\t\tstore.workflowExecutionState.timesRun > 1\n\t\t\t\t\t}\n\t\t\t\t>\n\t\t\t\t\t{store.workflowExecutionState.isRunning\n\t\t\t\t\t\t? \"Running...\"\n\t\t\t\t\t\t: \"Run Flow\"}\n\t\t\t\t</Button>\n\t\t\t</Panel>\n\t\t</ReactFlow>\n\t);\n}\n\nexport default function Page() {\n\treturn (\n\t\t<div className=\"w-screen h-screen\">\n\t\t\t<ReactFlowProvider>\n\t\t\t\t<Flow />\n\t\t\t</ReactFlowProvider>\n\t\t</div>\n\t);\n}\n",
			"type": "registry:page",
			"target": "app/workflow/page.tsx"
		},
		{
			"path": "./src/registry/blocks/flow-routing/route.ts",
			"content": "import { NextResponse } from \"next/server\";\nimport { serverNodeProcessors } from \"@/registry/lib/flow/server-node-processors\";\nimport { executeServerWorkflow } from \"@/registry/lib/flow/sse-workflow-execution-engine\";\nimport type { WorkflowDefinition } from \"@/registry/lib/flow/workflow\";\n\nexport const maxDuration = 60;\n\nexport async function POST(req: Request) {\n\ttry {\n\t\tconst { workflow } = await req.json();\n\n\t\tif (!workflow) {\n\t\t\treturn NextResponse.json(\n\t\t\t\t{ error: \"No workflow data provided\" },\n\t\t\t\t{ status: 400 },\n\t\t\t);\n\t\t}\n\n\t\tconst workflowDefinition: WorkflowDefinition = workflow;\n\n\t\t// Create a stream for SSE\n\t\tconst stream = new ReadableStream({\n\t\t\tasync start(controller) {\n\t\t\t\tawait executeServerWorkflow(\n\t\t\t\t\tworkflowDefinition,\n\t\t\t\t\tserverNodeProcessors,\n\t\t\t\t\tcontroller,\n\t\t\t\t);\n\t\t\t},\n\t\t});\n\n\t\treturn new Response(stream, {\n\t\t\theaders: {\n\t\t\t\t\"Content-Type\": \"text/event-stream\",\n\t\t\t\t\"Cache-Control\": \"no-cache\",\n\t\t\t\tConnection: \"keep-alive\",\n\t\t\t},\n\t\t});\n\t} catch (error) {\n\t\treturn NextResponse.json(\n\t\t\t{ error: error instanceof Error ? error.message : \"Unknown error\" },\n\t\t\t{ status: 500 },\n\t\t);\n\t}\n}\n",
			"type": "registry:page",
			"target": "app/api/workflow/execute/route.ts"
		},
		{
			"path": "./src/registry/blocks/flow-routing/components/nodes-panel.tsx",
			"content": "import { Panel } from \"@xyflow/react\";\nimport { Eye, PenLine } from \"lucide-react\";\nimport type React from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\nconst nodeTypes = [\n\t{\n\t\ttype: \"visualize-text\",\n\t\tlabel: \"Visualize Text\",\n\t\ticon: Eye,\n\t},\n\t{\n\t\ttype: \"text-input\",\n\t\tlabel: \"Text Input\",\n\t\ticon: PenLine,\n\t},\n];\n\nexport function NodesPanel() {\n\tconst onDragStart = (event: React.DragEvent, nodeType: string) => {\n\t\tevent.dataTransfer.setData(\"application/reactflow\", nodeType);\n\t\tevent.dataTransfer.effectAllowed = \"move\";\n\t};\n\n\treturn (\n\t\t<Panel position=\"top-center\" className=\"flex gap-2\">\n\t\t\t{nodeTypes.map((nodeType) => (\n\t\t\t\t<Button\n\t\t\t\t\tkey={nodeType.type}\n\t\t\t\t\tvariant=\"outline\"\n\t\t\t\t\tclassName=\"cursor-grab\"\n\t\t\t\t\tdraggable\n\t\t\t\t\tonDragStart={(e) => onDragStart(e, nodeType.type)}\n\t\t\t\t>\n\t\t\t\t\t<nodeType.icon className=\"mr-2 h-4 w-4\" />\n\t\t\t\t\t{nodeType.label}\n\t\t\t\t</Button>\n\t\t\t))}\n\t\t</Panel>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/nodes-panel.tsx"
		},
		{
			"path": "./src/registry/blocks/flow-chain/components/error-indicator.tsx",
			"content": "import { AlertCircle } from \"lucide-react\";\nimport type React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport {\n\tPopover,\n\tPopoverContent,\n\tPopoverTrigger,\n} from \"@/components/ui/popover\";\nimport type { WorkflowError } from \"@/registry/lib/flow/workflow\";\n\ninterface ErrorIndicatorProps {\n\terrors: WorkflowError[];\n}\n\nexport function ErrorIndicator({\n\terrors,\n}: ErrorIndicatorProps): React.ReactElement | null {\n\tif (errors.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<Popover>\n\t\t\t<PopoverTrigger asChild>\n\t\t\t\t<Button variant=\"ghost\" size=\"icon\" className=\"text-red-500\">\n\t\t\t\t\t<AlertCircle className=\"h-5 w-5\" />\n\t\t\t\t</Button>\n\t\t\t</PopoverTrigger>\n\t\t\t<PopoverContent align=\"center\" className=\"w-80 mt-4 mr-4\">\n\t\t\t\t<div className=\"space-y-2\">\n\t\t\t\t\t<h4 className=\"font-medium\">Workflow Errors</h4>\n\t\t\t\t\t<div className=\"space-y-1\">\n\t\t\t\t\t\t{errors.map((error) => (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={`${error.type}-${error.message}`}\n\t\t\t\t\t\t\t\tclassName=\"text-sm text-red-500\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{error.message}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</PopoverContent>\n\t\t</Popover>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/error-indicator.tsx"
		},
		{
			"path": "./src/registry/blocks/flow-routing/lib/content-creator-routing.ts",
			"content": "import type { FlowEdge, FlowNode } from \"@/registry/lib/flow/workflow\";\n\nexport const CONTENT_CREATOR_ROUTING_WORKFLOW: {\n\tnodes: FlowNode[];\n\tedges: FlowEdge[];\n} = {\n\tnodes: [\n\t\t{\n\t\t\ttype: \"text-input\",\n\t\t\tid: \"validationSystemPrompt\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tvalue: \"You will receive a request by the user and your task is to route it to the appropiate expert.\\n\\nYou will ALWAYS route the ORIGINAL request to one assistant only\\n\\nRoute the original request without modifying any of its text\\n\\n- Use the blog-expert if the task is about writing blogs, or related\\n\\n- Use the short-form-expert if the task is about creating short form content for social media\\n\\n- Use the seo-web-expert if the task if about SEO optimization\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: -630.950179662337,\n\t\t\t\ty: -272.9151555178262,\n\t\t\t},\n\t\t\twidth: 382,\n\t\t\theight: 340,\n\t\t},\n\t\t{\n\t\t\ttype: \"generate-text\",\n\t\t\tid: \"validateLLM\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tmodel: \"llama-3.3-70b-versatile\",\n\t\t\t\t},\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\ttools: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: \"blog-expert\",\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\"Route the input here if the request is about creating blog content\",\n\t\t\t\t\t\t\tid: \"IKir5iiq4F3eurd1ApK--\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: \"short-form-expert\",\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\"Route the input here if the request is about creating short form content\",\n\t\t\t\t\t\t\tid: \"77ew80gSbzRhvwhf3fnpa\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tname: \"seo-web-expert\",\n\t\t\t\t\t\t\tdescription:\n\t\t\t\t\t\t\t\t\"Route the input here if the request is to optimize content for SEO\",\n\t\t\t\t\t\t\tid: \"Kb5hnlAPXL-4YM7FyfvLX\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: -216.1529010505419,\n\t\t\t\ty: -90.08410976336413,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: \"generate-text\",\n\t\t\tid: \"Nr22stf-aM3K9KZ7fHREZ\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tmodel: \"llama-3.1-8b-instant\",\n\t\t\t\t},\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\ttools: [],\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 1131.0549255629082,\n\t\t\t\ty: -8.558529481810613,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: \"text-input\",\n\t\t\tid: \"97RH-yQMOC0ANhS2vFhcO\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tvalue: \"You are an expert in creating short form content. Your task is to generate creative and well structured short content posts for instagram, twitter and others\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 755.1019295025321,\n\t\t\t\ty: -21.392905970926638,\n\t\t\t},\n\t\t\twidth: 334,\n\t\t\theight: 270,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"lo9ImZY7ZBHw2xTEhj2X_\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 184.84979365379445,\n\t\t\t\ty: -363.4130653501635,\n\t\t\t},\n\t\t\twidth: 361,\n\t\t\theight: 291,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"eYRTRKwrUcn_fmuMKuUEl\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 186.64036397432417,\n\t\t\t\ty: -10.053864581653889,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"generate-text\",\n\t\t\tid: \"ZnL2SgGAMwaZSLNH-bOX3\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tmodel: \"llama-3.1-8b-instant\",\n\t\t\t\t},\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\ttools: [],\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 1137.9900879980153,\n\t\t\t\ty: -360.54142250040667,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: \"text-input\",\n\t\t\tid: \"3nEzzfbTIDDXw3WSEq4FR\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tvalue: \"You are an expert in writing blogs. Your task is to generate creative and well structured blogs, using appropiate sections and subsections\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 754.9699904778432,\n\t\t\t\ty: -390.86846911802775,\n\t\t\t},\n\t\t\twidth: 326,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"generate-text\",\n\t\t\tid: \"lu-X2l3QTJj8RBk4fDwGL\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tmodel: \"llama-3.1-8b-instant\",\n\t\t\t\t},\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\ttools: [],\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 1118.3693399999038,\n\t\t\t\ty: 355.8064333777831,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: \"text-input\",\n\t\t\tid: \"_4RcYkPOEDKn-hmGOAvy9\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tvalue: \"You are an expert in Search Engine Optimization SEO. Your task is to analyze content and provide suggestions on what keywords could be better in order to improve SEO\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: 752.9894919604718,\n\t\t\t\ty: 333.28662944260793,\n\t\t\t},\n\t\t\twidth: 332,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"gPDWeyLIVbkoWEffGe9Xh\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 184.2323574137816,\n\t\t\t\ty: 335.75701834105473,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"kaTYJV52ljshMg0uClQl1\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 1534.857338851561,\n\t\t\t\ty: -403.6387883803827,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"s5NSuCUuEByh_BTCSSMDU\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 1531.4882573594275,\n\t\t\t\ty: -41.46004169307438,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"visualize-text\",\n\t\t\tid: \"9cLCaECGGL5t21iQ3TDc9\",\n\t\t\tdata: {},\n\t\t\tposition: {\n\t\t\t\tx: 1532.942424576666,\n\t\t\t\ty: 309.86942743727576,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t\t{\n\t\t\ttype: \"text-input\",\n\t\t\tid: \"VGFbBVUjlwdQ2cGhrCv72\",\n\t\t\tdata: {\n\t\t\t\tconfig: {\n\t\t\t\t\tvalue: \"I want to create a twitter post for launching my new website simple-ai.dev\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tposition: {\n\t\t\t\tx: -621.3894717331403,\n\t\t\t\ty: 147.6203949447215,\n\t\t\t},\n\t\t\twidth: 350,\n\t\t\theight: 300,\n\t\t},\n\t],\n\tedges: [\n\t\t{\n\t\t\tsource: \"validationSystemPrompt\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"validateLLM\",\n\t\t\ttargetHandle: \"system\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validationSystemPromptresult-validateLLMsystem\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"77ew80gSbzRhvwhf3fnpa\",\n\t\t\ttarget: \"Nr22stf-aM3K9KZ7fHREZ\",\n\t\t\ttargetHandle: \"prompt\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLM77ew80gSbzRhvwhf3fnpa-Nr22stf-aM3K9KZ7fHREZprompt\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"97RH-yQMOC0ANhS2vFhcO\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"Nr22stf-aM3K9KZ7fHREZ\",\n\t\t\ttargetHandle: \"system\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__97RH-yQMOC0ANhS2vFhcOresult-Nr22stf-aM3K9KZ7fHREZsystem\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"IKir5iiq4F3eurd1ApK--\",\n\t\t\ttarget: \"lo9ImZY7ZBHw2xTEhj2X_\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLMIKir5iiq4F3eurd1ApK---lo9ImZY7ZBHw2xTEhj2X_input\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"77ew80gSbzRhvwhf3fnpa\",\n\t\t\ttarget: \"eYRTRKwrUcn_fmuMKuUEl\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLM77ew80gSbzRhvwhf3fnpa-eYRTRKwrUcn_fmuMKuUElinput\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"IKir5iiq4F3eurd1ApK--\",\n\t\t\ttarget: \"ZnL2SgGAMwaZSLNH-bOX3\",\n\t\t\ttargetHandle: \"prompt\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLMIKir5iiq4F3eurd1ApK---ZnL2SgGAMwaZSLNH-bOX3prompt\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"3nEzzfbTIDDXw3WSEq4FR\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"ZnL2SgGAMwaZSLNH-bOX3\",\n\t\t\ttargetHandle: \"system\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__3nEzzfbTIDDXw3WSEq4FRresult-ZnL2SgGAMwaZSLNH-bOX3system\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"Kb5hnlAPXL-4YM7FyfvLX\",\n\t\t\ttarget: \"lu-X2l3QTJj8RBk4fDwGL\",\n\t\t\ttargetHandle: \"prompt\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLMKb5hnlAPXL-4YM7FyfvLX-lu-X2l3QTJj8RBk4fDwGLprompt\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"_4RcYkPOEDKn-hmGOAvy9\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"lu-X2l3QTJj8RBk4fDwGL\",\n\t\t\ttargetHandle: \"system\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge___4RcYkPOEDKn-hmGOAvy9result-lu-X2l3QTJj8RBk4fDwGLsystem\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"validateLLM\",\n\t\t\tsourceHandle: \"Kb5hnlAPXL-4YM7FyfvLX\",\n\t\t\ttarget: \"gPDWeyLIVbkoWEffGe9Xh\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__validateLLMKb5hnlAPXL-4YM7FyfvLX-gPDWeyLIVbkoWEffGe9Xhinput\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"ZnL2SgGAMwaZSLNH-bOX3\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"kaTYJV52ljshMg0uClQl1\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__ZnL2SgGAMwaZSLNH-bOX3result-kaTYJV52ljshMg0uClQl1input\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"Nr22stf-aM3K9KZ7fHREZ\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"s5NSuCUuEByh_BTCSSMDU\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__Nr22stf-aM3K9KZ7fHREZresult-s5NSuCUuEByh_BTCSSMDUinput\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"lu-X2l3QTJj8RBk4fDwGL\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"9cLCaECGGL5t21iQ3TDc9\",\n\t\t\ttargetHandle: \"input\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__lu-X2l3QTJj8RBk4fDwGLresult-9cLCaECGGL5t21iQ3TDc9input\",\n\t\t\tdata: {},\n\t\t},\n\t\t{\n\t\t\tsource: \"VGFbBVUjlwdQ2cGhrCv72\",\n\t\t\tsourceHandle: \"result\",\n\t\t\ttarget: \"validateLLM\",\n\t\t\ttargetHandle: \"prompt\",\n\t\t\ttype: \"status\",\n\t\t\tid: \"xy-edge__VGFbBVUjlwdQ2cGhrCv72result-validateLLMprompt\",\n\t\t\tdata: {},\n\t\t},\n\t],\n};\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/ui/flow/status-edge-controller.tsx",
			"content": "\"use client\";\n\nimport type { EdgeProps } from \"@xyflow/react\";\nimport type { EdgeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport { StatusEdge } from \"@/registry/ui/flow/status-edge\";\n\nexport type StatusEdgeController = Omit<StatusEdge, \"data\"> & {\n\ttype: \"status\";\n\tdata: {\n\t\texecutionState?: EdgeExecutionState;\n\t};\n};\n\nexport function StatusEdgeController({\n\tdata,\n\t...props\n}: EdgeProps<StatusEdgeController>) {\n\treturn (\n\t\t<StatusEdge\n\t\t\t{...props}\n\t\t\tdata={{\n\t\t\t\terror: !!data.executionState?.error,\n\t\t\t}}\n\t\t/>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/status-edge-controller.tsx"
		},
		{
			"path": "./src/registry/ui/flow/status-edge.tsx",
			"content": "import {\n\ttype Edge,\n\ttype EdgeProps,\n\tBaseEdge as FlowBaseEdge,\n\tgetBezierPath,\n} from \"@xyflow/react\";\nimport type { CSSProperties } from \"react\";\n\nexport type StatusEdge = Edge<\n\t{\n\t\terror?: boolean;\n\t},\n\t\"status\"\n> & {\n\ttype: \"status\";\n\tsourceHandle: string;\n\ttargetHandle: string;\n};\n\nexport function StatusEdge({\n\tsourceX,\n\tsourceY,\n\ttargetX,\n\ttargetY,\n\tsourcePosition,\n\ttargetPosition,\n\tdata,\n\tselected,\n}: EdgeProps<StatusEdge>) {\n\tconst [edgePath] = getBezierPath({\n\t\tsourceX,\n\t\tsourceY,\n\t\tsourcePosition,\n\t\ttargetX,\n\t\ttargetY,\n\t\ttargetPosition,\n\t});\n\n\tconst edgeStyle: CSSProperties = {\n\t\tstroke: data?.error ? \"#ef4444\" : selected ? \"#3b82f6\" : \"#b1b1b7\",\n\t\tstrokeWidth: selected ? 3 : 2,\n\t\ttransition: \"stroke 0.2s, stroke-width 0.2s\",\n\t};\n\n\treturn <FlowBaseEdge path={edgePath} style={edgeStyle} />;\n}\n",
			"type": "registry:component",
			"target": "components/flow/status-edge.tsx"
		},
		{
			"path": "./src/registry/ui/flow/visualize-text-node-controller.tsx",
			"content": "\"use client\";\n\nimport type { NodeProps } from \"@xyflow/react\";\nimport { useCallback } from \"react\";\nimport { useWorkflow } from \"@/registry/hooks/flow/use-workflow\";\nimport type { NodeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport { VisualizeTextNode } from \"@/registry/ui/flow/visualize-text-node\";\n\nexport type VisualizeTextNodeController = Omit<VisualizeTextNode, \"data\"> & {\n\ttype: \"visualize-text\";\n\tdata: {\n\t\texecutionState?: NodeExecutionState;\n\t};\n};\n\nexport function VisualizeTextNodeController({\n\tid,\n\tdata,\n\t...props\n}: NodeProps<VisualizeTextNodeController>) {\n\tconst deleteNode = useWorkflow((state) => state.deleteNode);\n\n\tconst handleDeleteNode = useCallback(() => {\n\t\tdeleteNode(id);\n\t}, [id, deleteNode]);\n\n\treturn (\n\t\t<VisualizeTextNode\n\t\t\tid={id}\n\t\t\tdata={{\n\t\t\t\tinput: data.executionState?.targets?.input,\n\t\t\t\tstatus: data.executionState?.status,\n\t\t\t}}\n\t\t\tonDeleteNode={handleDeleteNode}\n\t\t\t{...props}\n\t\t/>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/visualize-text-node-controller.tsx"
		},
		{
			"path": "./src/registry/ui/flow/text-input-node-controller.tsx",
			"content": "\"use client\";\n\nimport type { NodeProps } from \"@xyflow/react\";\nimport { useCallback } from \"react\";\nimport { useWorkflow } from \"@/registry/hooks/flow/use-workflow\";\nimport type { NodeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport { TextInputNode } from \"@/registry/ui/flow/text-input-node\";\n\nexport type TextInputNodeController = Omit<TextInputNode, \"data\"> & {\n\ttype: \"text-input\";\n\tdata: Omit<TextInputNode[\"data\"], \"status\"> & {\n\t\texecutionState?: NodeExecutionState;\n\t};\n};\n\nexport function TextInputNodeController({\n\tid,\n\tdata,\n\t...props\n}: NodeProps<TextInputNodeController>) {\n\tconst updateNode = useWorkflow((state) => state.updateNode);\n\tconst deleteNode = useWorkflow((state) => state.deleteNode);\n\n\tconst handleTextChange = useCallback(\n\t\t(value: string) => {\n\t\t\tupdateNode(id, \"text-input\", { config: { value } });\n\t\t},\n\t\t[id, updateNode],\n\t);\n\n\tconst handleDeleteNode = useCallback(() => {\n\t\tdeleteNode(id);\n\t}, [id, deleteNode]);\n\n\treturn (\n\t\t<TextInputNode\n\t\t\tid={id}\n\t\t\tdata={{\n\t\t\t\tstatus: data.executionState?.status,\n\t\t\t\tconfig: data.config,\n\t\t\t}}\n\t\t\t{...props}\n\t\t\tonTextChange={handleTextChange}\n\t\t\tonDeleteNode={handleDeleteNode}\n\t\t/>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/text-input-node-controller.tsx"
		},
		{
			"path": "./src/registry/ui/flow/prompt-crafter-node-controller.tsx",
			"content": "\"use client\";\n\nimport type { NodeProps } from \"@xyflow/react\";\nimport { useCallback } from \"react\";\nimport { toast } from \"sonner\";\nimport { useWorkflow } from \"@/registry/hooks/flow/use-workflow\";\nimport type { NodeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport { PromptCrafterNode } from \"@/registry/ui/flow/prompt-crafter-node\";\n\nexport type PromptCrafterNodeController = Omit<PromptCrafterNode, \"data\"> & {\n\ttype: \"prompt-crafter\";\n\tdata: Omit<PromptCrafterNode[\"data\"], \"status\"> & {\n\t\texecutionState?: NodeExecutionState;\n\t};\n};\n\nexport function PromptCrafterNodeController({\n\tid,\n\tdata,\n\t...props\n}: NodeProps<PromptCrafterNodeController>) {\n\tconst updateNode = useWorkflow((state) => state.updateNode);\n\tconst addDynamicHandle = useWorkflow((state) => state.addDynamicHandle);\n\tconst removeDynamicHandle = useWorkflow(\n\t\t(state) => state.removeDynamicHandle,\n\t);\n\tconst deleteNode = useWorkflow((state) => state.deleteNode);\n\n\tconst handlePromptTextChange = useCallback(\n\t\t(value: string) => {\n\t\t\tupdateNode(id, \"prompt-crafter\", { config: { template: value } });\n\t\t},\n\t\t[id, updateNode],\n\t);\n\n\tconst handleCreateInput = useCallback(\n\t\t(name: string) => {\n\t\t\tif (!name) {\n\t\t\t\ttoast.error(\"Input name cannot be empty\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst existingInput = data.dynamicHandles[\"template-tags\"]?.find(\n\t\t\t\t(input) => input.name === name,\n\t\t\t);\n\t\t\tif (existingInput) {\n\t\t\t\ttoast.error(\"Input name already exists\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\taddDynamicHandle(id, \"prompt-crafter\", \"template-tags\", {\n\t\t\t\tname,\n\t\t\t});\n\t\t\treturn true;\n\t\t},\n\t\t[id, data.dynamicHandles, addDynamicHandle],\n\t);\n\n\tconst handleRemoveInput = useCallback(\n\t\t(handleId: string) => {\n\t\t\tremoveDynamicHandle(\n\t\t\t\tid,\n\t\t\t\t\"prompt-crafter\",\n\t\t\t\t\"template-tags\",\n\t\t\t\thandleId,\n\t\t\t);\n\t\t},\n\t\t[id, removeDynamicHandle],\n\t);\n\n\tconst handleUpdateInputName = useCallback(\n\t\t(handleId: string, newLabel: string): boolean => {\n\t\t\tif (!newLabel) {\n\t\t\t\ttoast.error(\"Input name cannot be empty\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst existingInput = data.dynamicHandles[\"template-tags\"]?.find(\n\t\t\t\t(input) => input.name === newLabel,\n\t\t\t);\n\t\t\tif (existingInput && existingInput.id !== handleId) {\n\t\t\t\ttoast.error(\"Input name already exists\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst oldInput = data.dynamicHandles[\"template-tags\"]?.find(\n\t\t\t\t(input) => input.id === handleId,\n\t\t\t);\n\t\t\tif (!oldInput) {\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tupdateNode(id, \"prompt-crafter\", {\n\t\t\t\tconfig: {\n\t\t\t\t\t...data.config,\n\t\t\t\t\ttemplate: (data.config.template || \"\").replace(\n\t\t\t\t\t\tnew RegExp(`{{${oldInput.name}}}`, \"g\"),\n\t\t\t\t\t\t`{{${newLabel}}}`,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\t...data.dynamicHandles,\n\t\t\t\t\t\"template-tags\": (\n\t\t\t\t\t\tdata.dynamicHandles[\"template-tags\"] || []\n\t\t\t\t\t).map((input) =>\n\t\t\t\t\t\tinput.id === handleId\n\t\t\t\t\t\t\t? { ...input, name: newLabel }\n\t\t\t\t\t\t\t: input,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn true;\n\t\t},\n\t\t[id, data.dynamicHandles, data.config, updateNode],\n\t);\n\n\tconst handleDeleteNode = useCallback(() => {\n\t\tdeleteNode(id);\n\t}, [id, deleteNode]);\n\n\treturn (\n\t\t<PromptCrafterNode\n\t\t\tid={id}\n\t\t\tdata={{ ...data, status: data.executionState?.status }}\n\t\t\t{...props}\n\t\t\tonPromptTextChange={handlePromptTextChange}\n\t\t\tonCreateInput={handleCreateInput}\n\t\t\tonRemoveInput={handleRemoveInput}\n\t\t\tonUpdateInputName={handleUpdateInputName}\n\t\t\tonDeleteNode={handleDeleteNode}\n\t\t/>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/prompt-crafter-node-controller.tsx"
		},
		{
			"path": "./src/registry/ui/flow/generate-text-node-controller.tsx",
			"content": "\"use client\";\n\nimport type { NodeProps } from \"@xyflow/react\";\nimport { useCallback } from \"react\";\nimport { toast } from \"sonner\";\nimport { useWorkflow } from \"@/registry/hooks/flow/use-workflow\";\nimport type { NodeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport { GenerateTextNode } from \"@/registry/ui/flow/generate-text-node\";\nimport type { Model } from \"@/registry/ui/model-selector\";\n\nexport type GenerateTextNodeController = Omit<GenerateTextNode, \"data\"> & {\n\ttype: \"generate-text\";\n\tdata: Omit<GenerateTextNode[\"data\"], \"status\"> & {\n\t\texecutionState?: NodeExecutionState;\n\t};\n};\n\nexport function GenerateTextNodeController({\n\tid,\n\tdata,\n\t...props\n}: NodeProps<GenerateTextNodeController>) {\n\tconst updateNode = useWorkflow((state) => state.updateNode);\n\tconst addDynamicHandle = useWorkflow((state) => state.addDynamicHandle);\n\tconst removeDynamicHandle = useWorkflow(\n\t\t(state) => state.removeDynamicHandle,\n\t);\n\tconst deleteNode = useWorkflow((state) => state.deleteNode);\n\n\tconst handleModelChange = useCallback(\n\t\t(model: Model) => {\n\t\t\tupdateNode(id, \"generate-text\", {\n\t\t\t\tconfig: {\n\t\t\t\t\t...data.config,\n\t\t\t\t\tmodel,\n\t\t\t\t},\n\t\t\t});\n\t\t},\n\t\t[id, data.config, updateNode],\n\t);\n\n\tconst handleCreateTool = useCallback(\n\t\t(name: string, description?: string) => {\n\t\t\tif (!name) {\n\t\t\t\ttoast.error(\"Tool name cannot be empty\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst existingTool = data.dynamicHandles.tools.find(\n\t\t\t\t(tool) => tool.name === name,\n\t\t\t);\n\t\t\tif (existingTool) {\n\t\t\t\ttoast.error(\"Tool name already exists\");\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\taddDynamicHandle(id, \"generate-text\", \"tools\", {\n\t\t\t\tname,\n\t\t\t\tdescription,\n\t\t\t});\n\t\t\treturn true;\n\t\t},\n\t\t[id, data.dynamicHandles.tools, addDynamicHandle],\n\t);\n\n\tconst handleRemoveTool = useCallback(\n\t\t(handleId: string) => {\n\t\t\tremoveDynamicHandle(id, \"generate-text\", \"tools\", handleId);\n\t\t},\n\t\t[id, removeDynamicHandle],\n\t);\n\n\tconst handleUpdateTool = useCallback(\n\t\t(toolId: string, newName: string, newDescription?: string) => {\n\t\t\tif (!newName) {\n\t\t\t\ttoast.error(\"Tool name cannot be empty\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tconst existingTool = data.dynamicHandles.tools.find(\n\t\t\t\t(tool) => tool.name === newName && tool.id !== toolId,\n\t\t\t);\n\t\t\tif (existingTool) {\n\t\t\t\ttoast.error(\"Tool name already exists\");\n\t\t\t\treturn false;\n\t\t\t}\n\n\t\t\tupdateNode(id, \"generate-text\", {\n\t\t\t\tdynamicHandles: {\n\t\t\t\t\t...data.dynamicHandles,\n\t\t\t\t\ttools: data.dynamicHandles.tools.map((tool) =>\n\t\t\t\t\t\ttool.id === toolId\n\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t...tool,\n\t\t\t\t\t\t\t\t\tname: newName,\n\t\t\t\t\t\t\t\t\tdescription: newDescription,\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t: tool,\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t});\n\t\t\treturn true;\n\t\t},\n\t\t[id, data.dynamicHandles, updateNode],\n\t);\n\n\tconst handleDeleteNode = useCallback(() => {\n\t\tdeleteNode(id);\n\t}, [id, deleteNode]);\n\n\treturn (\n\t\t<GenerateTextNode\n\t\t\tid={id}\n\t\t\tdata={{\n\t\t\t\tstatus: data.executionState?.status,\n\t\t\t\tconfig: data.config,\n\t\t\t\tdynamicHandles: data.dynamicHandles,\n\t\t\t}}\n\t\t\t{...props}\n\t\t\tdisableModelSelector\n\t\t\tonModelChange={handleModelChange}\n\t\t\tonCreateTool={handleCreateTool}\n\t\t\tonRemoveTool={handleRemoveTool}\n\t\t\tonUpdateTool={handleUpdateTool}\n\t\t\tonDeleteNode={handleDeleteNode}\n\t\t/>\n\t);\n}\n",
			"type": "registry:component",
			"target": "components/flow/generate-text-node-controller.tsx"
		},
		{
			"path": "./src/registry/hooks/flow/use-workflow.ts",
			"content": "import type { Connection, EdgeChange, NodeChange } from \"@xyflow/react\";\nimport { addEdge, applyEdgeChanges, applyNodeChanges } from \"@xyflow/react\";\nimport { nanoid } from \"nanoid\";\nimport { createWithEqualityFn } from \"zustand/traditional\";\nimport { createNode } from \"@/registry/lib/flow/node-factory\";\nimport { SSEWorkflowExecutionClient } from \"@/registry/lib/flow/sse-workflow-execution-client\";\nimport {\n\ttype DynamicHandle,\n\ttype FlowEdge,\n\ttype FlowNode,\n\tisNodeOfType,\n\tisNodeWithDynamicHandles,\n\tprepareWorkflow,\n\ttype WorkflowDefinition,\n\ttype WorkflowError,\n} from \"@/registry/lib/flow/workflow\";\nimport type {\n\tEdgeExecutionState,\n\tNodeExecutionState,\n} from \"@/registry/lib/flow/workflow-execution-engine\";\n\nexport interface WorkflowState {\n\tnodes: FlowNode[];\n\tedges: FlowEdge[];\n\tonNodesChange: (changes: NodeChange<FlowNode>[]) => void;\n\tonEdgesChange: (changes: EdgeChange<FlowEdge>[]) => void;\n\tonConnect: (connection: Connection) => void;\n\tgetNodeById: (nodeId: string) => FlowNode;\n\tcreateNode: (\n\t\tnodeType: FlowNode[\"type\"],\n\t\tposition: { x: number; y: number },\n\t) => FlowNode;\n\tupdateNode: <T extends FlowNode[\"type\"]>(\n\t\tid: string,\n\t\tnodeType: T,\n\t\tdata: Partial<FlowNode[\"data\"]>,\n\t) => void;\n\tupdateNodeExecutionState: (\n\t\tnodeId: string,\n\t\tstate: Partial<NodeExecutionState> | undefined,\n\t) => void;\n\tupdateEdgeExecutionState: (\n\t\tedgeId: string,\n\t\tstate: Partial<EdgeExecutionState> | undefined,\n\t) => void;\n\tdeleteNode: (id: string) => void;\n\taddDynamicHandle: <T extends FlowNode[\"type\"]>(\n\t\tnodeId: string,\n\t\tnodeType: T,\n\t\thandleCategory: string,\n\t\thandle: Omit<DynamicHandle, \"id\">,\n\t) => string;\n\tremoveDynamicHandle: <T extends FlowNode[\"type\"]>(\n\t\tnodeId: string,\n\t\tnodeType: T,\n\t\thandleCategory: string,\n\t\thandleId: string,\n\t) => void;\n\t// Workflow validation and execution state\n\tvalidateWorkflow: () => WorkflowDefinition;\n\tworkflowExecutionState: {\n\t\tisRunning: boolean;\n\t\tfinishedAt: string | null;\n\t\terrors: WorkflowError[];\n\t\ttimesRun: number;\n\t};\n\t// execution\n\tstartExecution: () => Promise<{\n\t\tstatus: \"success\" | \"error\";\n\t\tmessage: string;\n\t\terror?: Error;\n\t\tvalidationErrors?: WorkflowError[];\n\t}>;\n\t// Initialize workflow with nodes and edges\n\tinitializeWorkflow: (nodes: FlowNode[], edges: FlowEdge[]) => void;\n}\n\nconst useWorkflow = createWithEqualityFn<WorkflowState>((set, get) => ({\n\tnodes: [],\n\tedges: [],\n\tworkflowExecutionState: {\n\t\tisRunning: false,\n\t\tfinishedAt: null,\n\t\terrors: [],\n\t\ttimesRun: 0,\n\t},\n\tinitializeWorkflow: (nodes: FlowNode[], edges: FlowEdge[]) => {\n\t\tset({ nodes, edges });\n\t\tget().validateWorkflow();\n\t},\n\tvalidateWorkflow: () => {\n\t\tconst { nodes, edges } = get();\n\t\tconst workflow = prepareWorkflow(nodes, edges);\n\n\t\t// Reset edge execution states\n\t\tfor (const edge of workflow.edges) {\n\t\t\tget().updateEdgeExecutionState(edge.id, {\n\t\t\t\terror: undefined,\n\t\t\t});\n\t\t}\n\n\t\t// Update states for errors if any\n\t\tif (workflow.errors.length > 0) {\n\t\t\tfor (const error of workflow.errors) {\n\t\t\t\tswitch (error.type) {\n\t\t\t\t\tcase \"multiple-sources-for-target-handle\":\n\t\t\t\t\tcase \"cycle\":\n\t\t\t\t\t\tfor (const edge of error.edges) {\n\t\t\t\t\t\t\tget().updateEdgeExecutionState(edge.id, {\n\t\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\tcase \"missing-required-connection\":\n\t\t\t\t\t\tget().updateNodeExecutionState(error.node.id, {\n\t\t\t\t\t\t\tstatus: \"idle\",\n\t\t\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\t\t\terror,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tset((state) => ({\n\t\t\tworkflowExecutionState: {\n\t\t\t\t...state.workflowExecutionState,\n\t\t\t\terrors: workflow.errors,\n\t\t\t},\n\t\t}));\n\t\treturn workflow;\n\t},\n\tonNodesChange: (changes) => {\n\t\tset({\n\t\t\tnodes: applyNodeChanges<FlowNode>(changes, get().nodes),\n\t\t});\n\t\tget().validateWorkflow();\n\t},\n\tonEdgesChange: (changes) => {\n\t\tset({\n\t\t\tedges: applyEdgeChanges(changes, get().edges),\n\t\t});\n\t\tget().validateWorkflow();\n\t},\n\tonConnect: (connection) => {\n\t\tconst newEdge = addEdge({ ...connection, type: \"status\" }, get().edges);\n\t\tconst sourceNode = get().getNodeById(connection.source);\n\n\t\tif (!connection.sourceHandle) {\n\t\t\tthrow new Error(\"Source handle not found\");\n\t\t}\n\n\t\tconst sourceExecutionState = sourceNode.data.executionState;\n\n\t\tif (sourceExecutionState?.sources) {\n\t\t\tconst sourceHandleData =\n\t\t\t\tsourceExecutionState.sources[connection.sourceHandle];\n\t\t\tconst nodes = get().nodes.map((node) => {\n\t\t\t\tif (node.id === connection.target && connection.targetHandle) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...node,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...node.data,\n\t\t\t\t\t\t\texecutionState: node.data.executionState\n\t\t\t\t\t\t\t\t? {\n\t\t\t\t\t\t\t\t\t\t...node.data.executionState,\n\t\t\t\t\t\t\t\t\t\ttargets: {\n\t\t\t\t\t\t\t\t\t\t\t...node.data.executionState.targets,\n\t\t\t\t\t\t\t\t\t\t\t[connection.targetHandle]:\n\t\t\t\t\t\t\t\t\t\t\t\tsourceHandleData,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t: {\n\t\t\t\t\t\t\t\t\t\tstatus: \"success\",\n\t\t\t\t\t\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\t\t\t\t\t\ttargets: {\n\t\t\t\t\t\t\t\t\t\t\t[connection.targetHandle]:\n\t\t\t\t\t\t\t\t\t\t\t\tsourceHandleData,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn node;\n\t\t\t});\n\n\t\t\tset({\n\t\t\t\tnodes: nodes as FlowNode[],\n\t\t\t});\n\t\t}\n\n\t\tset({\n\t\t\tedges: newEdge,\n\t\t});\n\t\tget().validateWorkflow();\n\t},\n\tgetNodeById: (nodeId) => {\n\t\tconst node = get().nodes.find((node) => node.id === nodeId);\n\t\tif (!node) {\n\t\t\tthrow new Error(`Node with id ${nodeId} not found`);\n\t\t}\n\t\treturn node;\n\t},\n\tcreateNode(nodeType, position) {\n\t\tconst newNode = createNode(nodeType, position);\n\t\tset((state) => ({\n\t\t\tnodes: [...state.nodes, newNode],\n\t\t}));\n\t\treturn newNode;\n\t},\n\tupdateNode(id, type, data) {\n\t\tset((state) => ({\n\t\t\tnodes: state.nodes.map((node) => {\n\t\t\t\tif (node.id === id && isNodeOfType(node, type)) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...node,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...node.data,\n\t\t\t\t\t\t\t...data,\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn node;\n\t\t\t}),\n\t\t}));\n\t},\n\tupdateNodeExecutionState: (nodeId, state) => {\n\t\tset((currentState) => ({\n\t\t\tnodes: currentState.nodes.map((node) => {\n\t\t\t\tif (node.id === nodeId) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...node,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...node.data,\n\t\t\t\t\t\t\texecutionState: {\n\t\t\t\t\t\t\t\t...node.data?.executionState,\n\t\t\t\t\t\t\t\t...state,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t} as FlowNode;\n\t\t\t\t}\n\t\t\t\treturn node;\n\t\t\t}),\n\t\t}));\n\t},\n\tupdateEdgeExecutionState: (edgeId, state) => {\n\t\tset((currentState) => ({\n\t\t\tedges: currentState.edges.map((edge) => {\n\t\t\t\tif (edge.id === edgeId) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...edge,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...edge.data,\n\t\t\t\t\t\t\texecutionState: {\n\t\t\t\t\t\t\t\t...edge.data?.executionState,\n\t\t\t\t\t\t\t\t...state,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn edge;\n\t\t\t}),\n\t\t}));\n\t},\n\tdeleteNode(id) {\n\t\tset({\n\t\t\tnodes: get().nodes.filter((node) => node.id !== id),\n\t\t\tedges: get().edges.filter(\n\t\t\t\t(edge) => edge.source !== id && edge.target !== id,\n\t\t\t),\n\t\t});\n\t},\n\taddDynamicHandle(nodeId, type, handleCategory, handle) {\n\t\tconst newId = nanoid();\n\t\tset({\n\t\t\tnodes: get().nodes.map((node) => {\n\t\t\t\tif (\n\t\t\t\t\tnode.id === nodeId &&\n\t\t\t\t\tisNodeWithDynamicHandles(node) &&\n\t\t\t\t\tisNodeOfType(node, type)\n\t\t\t\t) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...node,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...node.data,\n\t\t\t\t\t\t\tdynamicHandles: {\n\t\t\t\t\t\t\t\t...node.data.dynamicHandles,\n\t\t\t\t\t\t\t\t[handleCategory]: [\n\t\t\t\t\t\t\t\t\t...(node.data.dynamicHandles[\n\t\t\t\t\t\t\t\t\t\thandleCategory as keyof typeof node.data.dynamicHandles\n\t\t\t\t\t\t\t\t\t] || []),\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t...handle,\n\t\t\t\t\t\t\t\t\t\tid: newId,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\treturn node;\n\t\t\t}),\n\t\t});\n\t\treturn newId;\n\t},\n\tremoveDynamicHandle(nodeId, type, handleCategory, handleId) {\n\t\tset({\n\t\t\tnodes: get().nodes.map((node) => {\n\t\t\t\tif (\n\t\t\t\t\tnode.id === nodeId &&\n\t\t\t\t\tisNodeWithDynamicHandles(node) &&\n\t\t\t\t\tisNodeOfType(node, type)\n\t\t\t\t) {\n\t\t\t\t\tconst dynamicHandles = node.data.dynamicHandles;\n\t\t\t\t\tconst handles = dynamicHandles[\n\t\t\t\t\t\thandleCategory as keyof typeof dynamicHandles\n\t\t\t\t\t] as DynamicHandle[];\n\t\t\t\t\tconst newHandles = handles.filter(\n\t\t\t\t\t\t(handle) => handle.id !== handleId,\n\t\t\t\t\t);\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\t...node,\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t...node.data,\n\t\t\t\t\t\t\tdynamicHandles: {\n\t\t\t\t\t\t\t\t...node.data.dynamicHandles,\n\t\t\t\t\t\t\t\t[handleCategory]: newHandles,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t\treturn node;\n\t\t\t}),\n\t\t\tedges: get().edges.filter((edge) => {\n\t\t\t\tif (edge.source === nodeId && edge.sourceHandle === handleId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (edge.target === nodeId && edge.targetHandle === handleId) {\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\treturn true;\n\t\t\t}),\n\t\t});\n\t},\n\t// Runtime\n\n\tasync startExecution() {\n\t\t// Check if workflow has already run successfully\n\t\tif (get().workflowExecutionState.timesRun > 3) {\n\t\t\tconst message =\n\t\t\t\t\"Workflow has already run successfully and cannot be run again\";\n\t\t\tconsole.warn(message);\n\t\t\treturn {\n\t\t\t\tstatus: \"error\",\n\t\t\t\tmessage,\n\t\t\t\terror: new Error(message),\n\t\t\t};\n\t\t}\n\n\t\t// Reset execution state for all nodes\n\t\tset((state) => ({\n\t\t\tnodes: state.nodes.map((node) => ({\n\t\t\t\t...node,\n\t\t\t\tdata: {\n\t\t\t\t\t...node.data,\n\t\t\t\t\texecutionState: {\n\t\t\t\t\t\tstatus: \"idle\",\n\t\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})) as FlowNode[],\n\t\t}));\n\n\t\tconst workflow = get().validateWorkflow();\n\n\t\tif (workflow.errors.length > 0) {\n\t\t\tconst message = \"Workflow validation failed\";\n\t\t\treturn {\n\t\t\t\tstatus: \"error\",\n\t\t\t\tmessage,\n\t\t\t\terror: new Error(message),\n\t\t\t\tvalidationErrors: workflow.errors,\n\t\t\t};\n\t\t}\n\n\t\t// Set execution state to running\n\t\tset((state) => ({\n\t\t\tworkflowExecutionState: {\n\t\t\t\t...state.workflowExecutionState,\n\t\t\t\tisRunning: true,\n\t\t\t},\n\t\t}));\n\n\t\ttry {\n\t\t\tconst sseClient = new SSEWorkflowExecutionClient();\n\t\t\tconst { updateNodeExecutionState } = get();\n\n\t\t\tawait new Promise((resolve, reject) => {\n\t\t\t\tsseClient.connect(workflow, {\n\t\t\t\t\tonNodeUpdate: (nodeId, state) => {\n\t\t\t\t\t\tupdateNodeExecutionState(nodeId, state);\n\t\t\t\t\t},\n\t\t\t\t\tonError: (error) => {\n\t\t\t\t\t\tconsole.error(\"Error in execution:\", error);\n\t\t\t\t\t\treject(error);\n\t\t\t\t\t},\n\t\t\t\t\tonComplete: ({ timestamp }) => {\n\t\t\t\t\t\tset((state) => ({\n\t\t\t\t\t\t\tworkflowExecutionState: {\n\t\t\t\t\t\t\t\t...state.workflowExecutionState,\n\t\t\t\t\t\t\t\tfinishedAt: timestamp,\n\t\t\t\t\t\t\t\ttimesRun:\n\t\t\t\t\t\t\t\t\tstate.workflowExecutionState.timesRun + 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}));\n\t\t\t\t\t\tresolve(undefined);\n\t\t\t\t\t},\n\t\t\t\t});\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\tstatus: \"success\",\n\t\t\t\tmessage: \"Workflow executed successfully\",\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tconsole.error(\"Workflow execution failed:\", error);\n\t\t\treturn {\n\t\t\t\tstatus: \"error\",\n\t\t\t\tmessage: \"Workflow execution failed\",\n\t\t\t\terror:\n\t\t\t\t\terror instanceof Error ? error : new Error(String(error)),\n\t\t\t};\n\t\t} finally {\n\t\t\t// Reset execution state when done\n\t\t\tset((state) => ({\n\t\t\t\tworkflowExecutionState: {\n\t\t\t\t\t...state.workflowExecutionState,\n\t\t\t\t\tisRunning: false,\n\t\t\t\t},\n\t\t\t}));\n\t\t}\n\t},\n}));\n\nexport { useWorkflow };\n",
			"type": "registry:hook"
		},
		{
			"path": "./src/registry/lib/flow/workflow.ts",
			"content": "import { nanoid } from \"nanoid\";\nimport type { GenerateTextNodeController } from \"@/registry/ui/flow/generate-text-node-controller\";\nimport type { PromptCrafterNodeController } from \"@/registry/ui/flow/prompt-crafter-node-controller\";\nimport type { StatusEdgeController } from \"@/registry/ui/flow/status-edge-controller\";\nimport type { TextInputNodeController } from \"@/registry/ui/flow/text-input-node-controller\";\nimport type { VisualizeTextNodeController } from \"@/registry/ui/flow/visualize-text-node-controller\";\n\ntype Dependency = {\n\tnode: string;\n\tsourceHandle: string;\n};\n\ntype Dependencies = Record<string, Dependency[]>;\n\ntype Dependent = {\n\tnode: string;\n\ttargetHandle: string;\n};\n\ntype Dependents = Record<string, Dependent[]>;\n\nexport type DependencyGraph = {\n\tdependencies: Map<string, { node: string; sourceHandle: string }[]>;\n\tdependents: Map<string, { node: string; targetHandle: string }[]>;\n};\n\nexport type ConnectionMap = Map<string, FlowEdge[]>;\n\n// Error types\n\ntype EdgeErrorInfo = {\n\tid: string;\n\tsource: string;\n\ttarget: string;\n\tsourceHandle: string;\n\ttargetHandle: string;\n};\n\nexport type MultipleSourcesError = {\n\tmessage: string;\n\ttype: \"multiple-sources-for-target-handle\";\n\tedges: EdgeErrorInfo[];\n};\n\nexport type CycleError = {\n\tmessage: string;\n\ttype: \"cycle\";\n\tedges: EdgeErrorInfo[];\n};\n\ntype NodeErrorInfo = {\n\tid: string;\n\thandleId: string;\n};\n\nexport type MissingConnectionError = {\n\tmessage: string;\n\ttype: \"missing-required-connection\";\n\tnode: NodeErrorInfo;\n};\n\nexport type WorkflowError =\n\t| MultipleSourcesError\n\t| CycleError\n\t| MissingConnectionError;\n\nexport interface WorkflowDefinition {\n\tid: string;\n\tnodes: FlowNode[];\n\tedges: FlowEdge[];\n\texecutionOrder: string[];\n\tdependencies: Dependencies;\n\tdependents: Dependents;\n\terrors: WorkflowError[];\n}\n\n// Dynamic Handles\n\nexport type DynamicHandle = {\n\tid: string;\n\tname: string;\n\tdescription?: string;\n};\n\n// Node Configuration\n\nconst NODES_CONFIG: Partial<\n\tRecord<\n\t\tFlowNode[\"type\"],\n\t\t{\n\t\t\trequiredTargets: string[];\n\t\t}\n\t>\n> = {\n\t\"generate-text\": {\n\t\trequiredTargets: [\"prompt\"],\n\t},\n};\n\n// Nodes\n\nexport type FlowNode =\n\t| VisualizeTextNodeController\n\t| TextInputNodeController\n\t| PromptCrafterNodeController\n\t| GenerateTextNodeController;\n\n// Edges\n\nexport type FlowEdge = StatusEdgeController;\n\n// Type Guards\n\nexport function isNodeOfType<T extends FlowNode[\"type\"]>(\n\tnode: FlowNode,\n\ttype: T,\n): node is Extract<FlowNode, { type: T }> {\n\treturn node.type === type;\n}\n\nexport function isNodeWithDynamicHandles<T extends FlowNode>(\n\tnode: T,\n): node is Extract<\n\tT,\n\t{\n\t\tdata: {\n\t\t\tdynamicHandles: {\n\t\t\t\t[key in string]: DynamicHandle[];\n\t\t\t};\n\t\t};\n\t}\n> {\n\treturn \"dynamicHandles\" in node.data;\n}\n\nfunction buildDependencyGraph(edges: FlowEdge[]): {\n\tdependencies: DependencyGraph[\"dependencies\"];\n\tdependents: DependencyGraph[\"dependents\"];\n\tconnectionMap: ConnectionMap;\n} {\n\tconst dependencies = new Map<\n\t\tstring,\n\t\t{ node: string; sourceHandle: string }[]\n\t>();\n\tconst dependents = new Map<\n\t\tstring,\n\t\t{ node: string; targetHandle: string }[]\n\t>();\n\tconst connectionMap = new Map<string, FlowEdge[]>();\n\n\tfor (const edge of edges) {\n\t\t// Track connections per target handle\n\t\tconst targetKey = `${edge.target}-${edge.targetHandle}`;\n\t\tconst existingConnections = connectionMap.get(targetKey) || [];\n\t\tconnectionMap.set(targetKey, [...existingConnections, edge]);\n\n\t\t// Build dependency graph\n\t\tconst existingDependencies = dependencies.get(edge.target) || [];\n\t\tdependencies.set(edge.target, [\n\t\t\t...existingDependencies,\n\t\t\t{\n\t\t\t\tnode: edge.source,\n\t\t\t\tsourceHandle: edge.sourceHandle,\n\t\t\t},\n\t\t]);\n\n\t\tconst existingDependents = dependents.get(edge.source) || [];\n\t\tdependents.set(edge.source, [\n\t\t\t...existingDependents,\n\t\t\t{\n\t\t\t\tnode: edge.target,\n\t\t\t\ttargetHandle: edge.targetHandle,\n\t\t\t},\n\t\t]);\n\t}\n\n\treturn { dependencies, dependents, connectionMap };\n}\n\nfunction topologicalSort(\n\tnodes: FlowNode[],\n\tdependencies: DependencyGraph[\"dependencies\"],\n\tdependents: DependencyGraph[\"dependents\"],\n): string[] {\n\tconst indegree = new Map<string, number>();\n\tconst queue: string[] = [];\n\tconst executionOrder: string[] = [];\n\n\t// Initialize in-degree\n\tfor (const node of nodes) {\n\t\tconst degree = dependencies.get(node.id)?.length || 0;\n\t\tindegree.set(node.id, degree);\n\t\tif (degree === 0) {\n\t\t\tqueue.push(node.id);\n\t\t}\n\t}\n\n\t// Process nodes\n\twhile (queue.length > 0) {\n\t\tconst currentNode = queue.shift();\n\t\tif (!currentNode) {\n\t\t\tcontinue;\n\t\t}\n\n\t\texecutionOrder.push(currentNode);\n\n\t\tconst nodesDependentOnCurrent = dependents.get(currentNode) || [];\n\t\tfor (const dependent of nodesDependentOnCurrent) {\n\t\t\tconst currentDegree = indegree.get(dependent.node);\n\t\t\tif (typeof currentDegree === \"number\") {\n\t\t\t\tconst newDegree = currentDegree - 1;\n\t\t\t\tindegree.set(dependent.node, newDegree);\n\t\t\t\tif (newDegree === 0) {\n\t\t\t\t\tqueue.push(dependent.node);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn executionOrder;\n}\n\nfunction validateMultipleSources(\n\tconnectionMap: ConnectionMap,\n): MultipleSourcesError[] {\n\tconst errors: MultipleSourcesError[] = [];\n\n\tconnectionMap.forEach((edges, targetKey) => {\n\t\tif (edges.length > 1) {\n\t\t\tconst [targetNode, targetHandle] = targetKey.split(\"-\");\n\t\t\terrors.push({\n\t\t\t\ttype: \"multiple-sources-for-target-handle\",\n\t\t\t\tmessage: `Target handle \"${targetHandle}\" on node \"${targetNode}\" has ${edges.length} sources.`,\n\t\t\t\tedges: edges.map((edge) => ({\n\t\t\t\t\tid: edge.id,\n\t\t\t\t\tsource: edge.source,\n\t\t\t\t\ttarget: edge.target,\n\t\t\t\t\tsourceHandle: edge.sourceHandle,\n\t\t\t\t\ttargetHandle: edge.targetHandle,\n\t\t\t\t})),\n\t\t\t});\n\t\t}\n\t});\n\n\treturn errors;\n}\n\nfunction detectCycles(\n\tnodes: FlowNode[],\n\tdependencies: DependencyGraph[\"dependencies\"],\n\tdependents: DependencyGraph[\"dependents\"],\n\tedges: FlowEdge[],\n): CycleError[] {\n\tconst executionOrder = topologicalSort(nodes, dependencies, dependents);\n\tif (executionOrder.length === nodes.length) {\n\t\treturn [];\n\t}\n\n\t// Find cycle participants\n\tconst indegree = new Map<string, number>();\n\tconst queue: string[] = [];\n\n\tfor (const node of nodes) {\n\t\tconst degree = dependencies.get(node.id)?.length || 0;\n\t\tindegree.set(node.id, degree);\n\t\tif (degree === 0) {\n\t\t\tqueue.push(node.id);\n\t\t}\n\t}\n\n\t// Kahn's algorithm to find remaining nodes\n\twhile (queue.length > 0) {\n\t\tconst currentNode = queue.shift();\n\t\tif (!currentNode) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst nodesDependentOnCurrent = dependents.get(currentNode) || [];\n\t\tfor (const dependent of nodesDependentOnCurrent) {\n\t\t\tconst currentDegree = indegree.get(dependent.node);\n\t\t\tif (typeof currentDegree === \"number\") {\n\t\t\t\tconst newDegree = currentDegree - 1;\n\t\t\t\tindegree.set(dependent.node, newDegree);\n\t\t\t\tif (newDegree === 0) {\n\t\t\t\t\tqueue.push(dependent.node);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Identify cycle nodes and edges\n\tconst cycleNodes = Array.from(indegree.entries())\n\t\t.filter(([_, degree]) => degree > 0)\n\t\t.map(([nodeId]) => nodeId);\n\n\tconst cycleEdges = edges.filter(\n\t\t(edge) =>\n\t\t\tcycleNodes.includes(edge.source) &&\n\t\t\tcycleNodes.includes(edge.target),\n\t);\n\n\tif (cycleEdges.length === 0) {\n\t\treturn [];\n\t}\n\n\tconst error: CycleError = {\n\t\ttype: \"cycle\",\n\t\tmessage: `Workflow contains cycles between nodes: ${cycleNodes.join(\", \")}`,\n\t\tedges: cycleEdges.map((edge) => ({\n\t\t\tid: edge.id,\n\t\t\tsource: edge.source,\n\t\t\ttarget: edge.target,\n\t\t\tsourceHandle: edge.sourceHandle,\n\t\t\ttargetHandle: edge.targetHandle,\n\t\t})),\n\t};\n\n\treturn [error];\n}\n\nfunction validateRequiredHandles(\n\tnodes: FlowNode[],\n\tedges: FlowEdge[],\n): MissingConnectionError[] {\n\tconst errors: MissingConnectionError[] = [];\n\tconst connectionsByTarget = new Map<string, FlowEdge[]>();\n\tconst connectionsBySource = new Map<string, FlowEdge[]>();\n\n\t// Build connection maps\n\tfor (const edge of edges) {\n\t\tconst targetKey = `${edge.target}-${edge.targetHandle}`;\n\t\tconst sourceKey = `${edge.source}-${edge.sourceHandle}`;\n\n\t\tconst targetConnections = connectionsByTarget.get(targetKey) || [];\n\t\tconnectionsByTarget.set(targetKey, [...targetConnections, edge]);\n\n\t\tconst sourceConnections = connectionsBySource.get(sourceKey) || [];\n\t\tconnectionsBySource.set(sourceKey, [...sourceConnections, edge]);\n\t}\n\n\t// Check each node against its type configuration\n\tfor (const node of nodes) {\n\t\tconst config = NODES_CONFIG[node.type];\n\n\t\tif (!config) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check required target handles\n\t\tif (config.requiredTargets) {\n\t\t\tfor (const targetHandle of config.requiredTargets) {\n\t\t\t\tconst key = `${node.id}-${targetHandle}`;\n\t\t\t\tconst connections = connectionsByTarget.get(key);\n\n\t\t\t\tif (!connections || connections.length === 0) {\n\t\t\t\t\terrors.push({\n\t\t\t\t\t\ttype: \"missing-required-connection\",\n\t\t\t\t\t\tmessage: `Node \"${node.id}\" requires a connection to its \"${targetHandle}\" input.`,\n\t\t\t\t\t\tnode: {\n\t\t\t\t\t\t\tid: node.id,\n\t\t\t\t\t\t\thandleId: targetHandle,\n\t\t\t\t\t\t},\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors;\n}\n\nexport function prepareWorkflow(\n\tnodes: FlowNode[],\n\tedges: FlowEdge[],\n): WorkflowDefinition {\n\tconst errors: WorkflowError[] = [];\n\n\t// First pass: Build dependency graph and check connection validity\n\tconst { dependencies, dependents, connectionMap } =\n\t\tbuildDependencyGraph(edges);\n\n\t/* console.log(\"dependencies\", dependencies);\n\tconsole.log(\"dependents\", dependents);\n\tconsole.log(\"connectionMap\", connectionMap);\n */\n\t// Second pass: Validate multiple sources for single target handle\n\terrors.push(...validateMultipleSources(connectionMap));\n\n\t// Third pass: Detect cycles\n\tconst cycleErrors = detectCycles(nodes, dependencies, dependents, edges);\n\terrors.push(...cycleErrors);\n\n\t// Fourth pass: Validate required handles\n\terrors.push(...validateRequiredHandles(nodes, edges));\n\n\t// Get execution order if no cycles were detected\n\tconst executionOrder =\n\t\tcycleErrors.length === 0\n\t\t\t? topologicalSort(nodes, dependencies, dependents)\n\t\t\t: [];\n\n\treturn {\n\t\tid: nanoid(),\n\t\tnodes,\n\t\tedges,\n\t\texecutionOrder,\n\t\tdependencies: Object.fromEntries(dependencies),\n\t\tdependents: Object.fromEntries(dependents),\n\t\terrors,\n\t};\n}\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/workflow-execution-engine.ts",
			"content": "import type {\n\tCycleError,\n\tFlowNode,\n\tMissingConnectionError,\n\tMultipleSourcesError,\n\tWorkflowDefinition,\n} from \"@/registry/lib/flow/workflow\";\n\n// Processing\n\nexport type ProcessingNodeError = {\n\tmessage: string;\n\ttype: \"processing-node\";\n};\n\nexport type ProcessedData = Record<string, string> | undefined;\n\nexport type NodeProcessor = (\n\tnode: FlowNode,\n\ttargetsData: ProcessedData,\n) => Promise<ProcessedData>;\n\n// Node Execution State\n\nexport type NodeExecutionStatus = \"success\" | \"error\" | \"processing\" | \"idle\";\n\nexport type NodeExecutionState = {\n\ttimestamp: string;\n\ttargets?: Record<string, string>;\n\tsources?: Record<string, string>;\n\tstatus: NodeExecutionStatus;\n\terror?: MissingConnectionError | ProcessingNodeError;\n};\n\n// Edge Execution State\n\nexport type EdgeExecutionState = {\n\terror?: MultipleSourcesError | CycleError;\n};\n\n// Excution Engine\n\ninterface ExecutionContext {\n\tworkflow: WorkflowDefinition;\n\tprocessNode: (\n\t\tnodeId: string,\n\t\ttargetsData: ProcessedData,\n\t) => Promise<ProcessedData>;\n\tupdateNodeExecutionState: (\n\t\tnodeId: string,\n\t\tstate: Partial<NodeExecutionState>,\n\t) => void;\n}\n\nexport const createWorkflowExecutionEngine = (context: ExecutionContext) => {\n\tconst completedNodes = new Set<string>();\n\tconst failedNodes = new Set<string>();\n\tconst processingNodes = new Set<string>();\n\n\tconst getNodeTargetsData = (\n\t\tworkflow: WorkflowDefinition,\n\t\tnodeId: string,\n\t): ProcessedData => {\n\t\tconst node = workflow.nodes.find((n) => n.id === nodeId);\n\t\tif (!node) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst edgesConnectedToNode = workflow.edges.filter(\n\t\t\t(edge) => edge.target === nodeId,\n\t\t);\n\n\t\tconst targetsData: ProcessedData = {};\n\t\tfor (const edge of edgesConnectedToNode) {\n\t\t\tconst sourceNode = workflow.nodes.find((n) => n.id === edge.source);\n\t\t\tif (!sourceNode?.data.executionState?.sources) {\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tconst sourceNodeResult =\n\t\t\t\tsourceNode.data.executionState.sources[edge.sourceHandle];\n\t\t\ttargetsData[edge.targetHandle] = sourceNodeResult;\n\t\t}\n\n\t\treturn targetsData;\n\t};\n\n\tconst checkBranchNodeStatus = (nodeId: string): NodeExecutionStatus => {\n\t\tconst node = context.workflow.nodes.find((n) => n.id === nodeId);\n\t\tif (!node) {\n\t\t\treturn \"idle\";\n\t\t}\n\n\t\t// If this node is processing, the whole branch is processing\n\t\tif (processingNodes.has(nodeId)) {\n\t\t\treturn \"processing\";\n\t\t}\n\n\t\t// If this node has failed, the branch has failed\n\t\tif (failedNodes.has(nodeId)) {\n\t\t\treturn \"error\";\n\t\t}\n\n\t\t// Get all nodes that this node depends on\n\t\tconst dependencies = context.workflow.dependencies[nodeId] || [];\n\n\t\t// If this node has no dependencies, check its own status\n\t\tif (dependencies.length === 0) {\n\t\t\tif (\n\t\t\t\tcompletedNodes.has(nodeId) &&\n\t\t\t\tnode.data.executionState?.sources\n\t\t\t) {\n\t\t\t\treturn \"success\";\n\t\t\t}\n\t\t\treturn \"idle\";\n\t\t}\n\n\t\t// Check status of all dependencies recursively\n\t\tfor (const dep of dependencies) {\n\t\t\tconst depStatus = checkBranchNodeStatus(dep.node);\n\t\t\t// If any dependency is processing or has error, propagate that status\n\t\t\tif (depStatus === \"processing\" || depStatus === \"error\") {\n\t\t\t\treturn depStatus;\n\t\t\t}\n\t\t}\n\n\t\t// If we got here and the node is complete with data, the branch is successful\n\t\tif (completedNodes.has(nodeId) && node.data.executionState?.sources) {\n\t\t\treturn \"success\";\n\t\t}\n\n\t\treturn \"idle\";\n\t};\n\n\tconst getBranchStatus = (\n\t\tnodeId: string,\n\t\thandleId: string,\n\t): NodeExecutionStatus => {\n\t\tconst node = context.workflow.nodes.find((n) => n.id === nodeId);\n\t\tif (!node) {\n\t\t\treturn \"idle\";\n\t\t}\n\n\t\t// Get all edges that connect to this handle\n\t\tconst incomingEdges = context.workflow.edges.filter(\n\t\t\t(edge) => edge.target === nodeId && edge.targetHandle === handleId,\n\t\t);\n\n\t\t// For each incoming edge, check the status of its source node and all its descendants\n\t\tfor (const edge of incomingEdges) {\n\t\t\tconst branchStatus = checkBranchNodeStatus(edge.source);\n\t\t\tif (branchStatus !== \"idle\") {\n\t\t\t\treturn branchStatus;\n\t\t\t}\n\t\t}\n\n\t\treturn \"idle\";\n\t};\n\n\tconst canProcessNode = (nodeId: string) => {\n\t\tconst node = context.workflow.nodes.find((n) => n.id === nodeId);\n\t\tif (!node) {\n\t\t\treturn false;\n\t\t}\n\n\t\t// Special handling for prompt-crafter nodes\n\t\tif (node.type === \"prompt-crafter\") {\n\t\t\t// Get all target handles from dynamic handles\n\t\t\tconst targetHandles = (\n\t\t\t\tnode.data.dynamicHandles?.[\"template-tags\"] || []\n\t\t\t).map((handle) => handle.id);\n\n\t\t\t// Check each target handle's branch status\n\t\t\tconst branchStatuses = targetHandles.map((handleId) =>\n\t\t\t\tgetBranchStatus(nodeId, handleId),\n\t\t\t);\n\n\t\t\t// Node can process if ALL branches are complete and NONE are processing\n\t\t\tconst allBranchesComplete = branchStatuses.every(\n\t\t\t\t(status) => status === \"success\",\n\t\t\t);\n\t\t\tconst hasProcessingBranch = branchStatuses.some(\n\t\t\t\t(status) => status === \"processing\",\n\t\t\t);\n\n\t\t\treturn allBranchesComplete && !hasProcessingBranch;\n\t\t}\n\n\t\t// For regular nodes, check all dependencies\n\t\tconst nodeDependencies = context.workflow.dependencies[nodeId] || [];\n\t\treturn nodeDependencies.every((dep) => {\n\t\t\t// Check if any dependency has failed\n\t\t\tif (failedNodes.has(dep.node)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\t// Check if the node is completed AND the specific source handle has data\n\t\t\tconst sourceNode = context.workflow.nodes.find(\n\t\t\t\t(n) => n.id === dep.node,\n\t\t\t);\n\t\t\tif (!sourceNode?.data.executionState?.sources) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst sourceHandleData =\n\t\t\t\tsourceNode.data.executionState.sources[dep.sourceHandle];\n\t\t\treturn (\n\t\t\t\tcompletedNodes.has(dep.node) && sourceHandleData !== undefined\n\t\t\t);\n\t\t});\n\t};\n\n\tconst processNode = async (nodeId: string) => {\n\t\ttry {\n\t\t\tconst targetsData = getNodeTargetsData(context.workflow, nodeId);\n\n\t\t\tcontext.updateNodeExecutionState(nodeId, {\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tstatus: \"processing\",\n\t\t\t\ttargets: targetsData,\n\t\t\t});\n\n\t\t\tconst result = await context.processNode(nodeId, targetsData);\n\n\t\t\tcontext.updateNodeExecutionState(nodeId, {\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\ttargets: targetsData,\n\t\t\t\tsources: result,\n\t\t\t\tstatus: \"success\",\n\t\t\t});\n\n\t\t\tcompletedNodes.add(nodeId);\n\t\t\tprocessingNodes.delete(nodeId);\n\t\t} catch (error) {\n\t\t\tcontext.updateNodeExecutionState(nodeId, {\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tstatus: \"error\",\n\t\t\t\terror: {\n\t\t\t\t\ttype: \"processing-node\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\terror instanceof Error\n\t\t\t\t\t\t\t? error.message\n\t\t\t\t\t\t\t: \"Unknown error\",\n\t\t\t\t},\n\t\t\t});\n\t\t\tconsole.error(error);\n\t\t\tprocessingNodes.delete(nodeId);\n\t\t\tfailedNodes.add(nodeId); // Track failed nodes separately\n\t\t}\n\t};\n\n\treturn {\n\t\tasync execute(executionOrder: string[]) {\n\t\t\t// Reset tracking sets\n\t\t\tcompletedNodes.clear();\n\t\t\tfailedNodes.clear();\n\t\t\tprocessingNodes.clear();\n\n\t\t\twhile (\n\t\t\t\tcompletedNodes.size + failedNodes.size <\n\t\t\t\texecutionOrder.length\n\t\t\t) {\n\t\t\t\tconst availableNodes = executionOrder.filter(\n\t\t\t\t\t(nodeId) =>\n\t\t\t\t\t\t!completedNodes.has(nodeId) &&\n\t\t\t\t\t\t!failedNodes.has(nodeId) &&\n\t\t\t\t\t\t!processingNodes.has(nodeId) &&\n\t\t\t\t\t\tcanProcessNode(nodeId),\n\t\t\t\t);\n\n\t\t\t\tif (availableNodes.length === 0) {\n\t\t\t\t\tif (processingNodes.size > 0) {\n\t\t\t\t\t\tawait new Promise((resolve) =>\n\t\t\t\t\t\t\tsetTimeout(resolve, 100),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// If there are no available nodes and nothing is processing,\n\t\t\t\t\t// but we haven't completed all nodes, it means some nodes\n\t\t\t\t\t// couldn't execute due to failed dependencies\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tconst processingPromises = availableNodes.map((nodeId) => {\n\t\t\t\t\tprocessingNodes.add(nodeId);\n\t\t\t\t\treturn processNode(nodeId);\n\t\t\t\t});\n\n\t\t\t\tawait Promise.race(processingPromises);\n\t\t\t}\n\t\t},\n\t};\n};\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/sse-workflow-execution-client.ts",
			"content": "import type { WorkflowDefinition } from \"@/registry/lib/flow/workflow\";\nimport type { NodeExecutionState } from \"@/registry/lib/flow/workflow-execution-engine\";\n\nexport interface SSEWorkflowExecutionEventHandlers {\n\tonNodeUpdate: (nodeId: string, state: NodeExecutionState) => void;\n\tonError: (error: Error, nodeId?: string) => void;\n\tonComplete: ({ timestamp }: { timestamp: string }) => void;\n}\n\nexport class SSEWorkflowExecutionClient {\n\tprivate abortController: AbortController | null = null;\n\tprivate reader: ReadableStreamDefaultReader<Uint8Array> | null = null;\n\n\tasync connect(\n\t\tworkflow: WorkflowDefinition,\n\t\thandlers: SSEWorkflowExecutionEventHandlers,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tthis.abortController = new AbortController();\n\n\t\t\tconst response = await fetch(\"/api/flow/execute\", {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\tAccept: \"text/event-stream\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({ workflow }),\n\t\t\t\tsignal: this.abortController.signal,\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow new Error(`HTTP error! status: ${response.status}`);\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"Response body is null\");\n\t\t\t}\n\n\t\t\tthis.reader = response.body.getReader();\n\t\t\tconst decoder = new TextDecoder();\n\t\t\tlet buffer = \"\";\n\n\t\t\t// eslint-disable-next-line no-constant-condition\n\t\t\twhile (true) {\n\t\t\t\tconst { done, value } = await this.reader.read();\n\t\t\t\tif (done) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\tconst lines = buffer.split(\"\\n\\n\");\n\t\t\t\tbuffer = lines.pop() || \"\";\n\n\t\t\t\tfor (const line of lines) {\n\t\t\t\t\tif (line.startsWith(\"data: \")) {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst data = JSON.parse(line.slice(6));\n\n\t\t\t\t\t\t\tswitch (data.type) {\n\t\t\t\t\t\t\t\tcase \"nodeUpdate\": {\n\t\t\t\t\t\t\t\t\thandlers.onNodeUpdate(\n\t\t\t\t\t\t\t\t\t\tdata.nodeId,\n\t\t\t\t\t\t\t\t\t\tdata.executionState,\n\t\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcase \"error\": {\n\t\t\t\t\t\t\t\t\thandlers.onError(new Error(data.error));\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tcase \"complete\": {\n\t\t\t\t\t\t\t\t\thandlers.onComplete({\n\t\t\t\t\t\t\t\t\t\ttimestamp: data.timestamp,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\tthis.disconnect();\n\t\t\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\tconsole.error(\"Error parsing SSE data:\", error);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof Error && error.name === \"AbortError\") {\n\t\t\t\t// Ignore abort errors as they are expected when disconnecting\n\t\t\t\treturn;\n\t\t\t}\n\t\t\thandlers.onError(\n\t\t\t\terror instanceof Error\n\t\t\t\t\t? error\n\t\t\t\t\t: new Error(\"SSE connection failed\"),\n\t\t\t);\n\t\t} finally {\n\t\t\tthis.disconnect();\n\t\t}\n\t}\n\n\tdisconnect(): void {\n\t\tif (this.reader) {\n\t\t\tthis.reader.cancel();\n\t\t\tthis.reader = null;\n\t\t}\n\t\tif (this.abortController) {\n\t\t\tthis.abortController.abort();\n\t\t\tthis.abortController = null;\n\t\t}\n\t}\n}\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/sse-workflow-execution-engine.ts",
			"content": "import type {\n\tFlowNode,\n\tWorkflowDefinition,\n} from \"@/registry/lib/flow/workflow\";\nimport {\n\tcreateWorkflowExecutionEngine,\n\ttype NodeExecutionState,\n\ttype NodeProcessor,\n} from \"@/registry/lib/flow/workflow-execution-engine\";\n\nfunction createEvent(type: string, data: Record<string, unknown>) {\n\treturn `data: ${JSON.stringify({ type, ...data })}\\n\\n`;\n}\n\nfunction createSSEWorkflowExecutionEngine(\n\tworkflow: WorkflowDefinition,\n\tnodeProcessor: Record<FlowNode[\"type\"], NodeProcessor>,\n\tcontroller: ReadableStreamDefaultController,\n) {\n\treturn createWorkflowExecutionEngine({\n\t\tworkflow,\n\t\tprocessNode: async (nodeId, targetsData) => {\n\t\t\tconst node = workflow.nodes.find((n) => n.id === nodeId);\n\t\t\tif (!node) {\n\t\t\t\tthrow new Error(`Node ${nodeId} not found`);\n\t\t\t}\n\n\t\t\tconst processor = nodeProcessor[node.type];\n\t\t\treturn await processor(node, targetsData);\n\t\t},\n\t\tupdateNodeExecutionState: (\n\t\t\tnodeId,\n\t\t\tstate: Partial<NodeExecutionState>,\n\t\t) => {\n\t\t\tconst node = workflow.nodes.find((n) => n.id === nodeId);\n\t\t\tif (!node) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tnode.data.executionState = {\n\t\t\t\t...node.data.executionState,\n\t\t\t\t...state,\n\t\t\t} as NodeExecutionState;\n\n\t\t\t// Send node update event\n\t\t\tcontroller.enqueue(\n\t\t\t\tcreateEvent(\"nodeUpdate\", {\n\t\t\t\t\tnodeId,\n\t\t\t\t\texecutionState: node.data.executionState,\n\t\t\t\t}),\n\t\t\t);\n\n\t\t\tif (state.status === \"error\") {\n\t\t\t\tcontroller.enqueue(\n\t\t\t\t\tcreateEvent(\"error\", {\n\t\t\t\t\t\terror: state.error,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t},\n\t});\n}\n\nexport async function executeServerWorkflow(\n\tworkflow: WorkflowDefinition,\n\tnodeProcessor: Record<FlowNode[\"type\"], NodeProcessor>,\n\tcontroller: ReadableStreamDefaultController,\n) {\n\tconst engine = createSSEWorkflowExecutionEngine(\n\t\tworkflow,\n\t\tnodeProcessor,\n\t\tcontroller,\n\t);\n\n\ttry {\n\t\tawait engine.execute(workflow.executionOrder);\n\t\tcontroller.enqueue(\n\t\t\tcreateEvent(\"complete\", { timestamp: new Date().toISOString() }),\n\t\t);\n\t} catch (error) {\n\t\tcontroller.enqueue(\n\t\t\tcreateEvent(\"error\", {\n\t\t\t\terror: error instanceof Error ? error.message : \"Unknown error\",\n\t\t\t}),\n\t\t);\n\t} finally {\n\t\tcontroller.close();\n\t}\n}\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/server-node-processors.ts",
			"content": "import { generateAIText } from \"@/registry/lib/flow/generate-ai-text\";\nimport type { FlowNode } from \"@/registry/lib/flow/workflow\";\nimport type { NodeProcessor } from \"@/registry/lib/flow/workflow-execution-engine\";\nimport type { GenerateTextNode } from \"@/registry/ui/flow/generate-text-node\";\nimport type { PromptCrafterNode } from \"@/registry/ui/flow/prompt-crafter-node\";\nimport type { TextInputNode } from \"@/registry/ui/flow/text-input-node\";\n\nexport const serverNodeProcessors: Record<FlowNode[\"type\"], NodeProcessor> = {\n\t\"text-input\": async (node) => {\n\t\tconst textNode = node as TextInputNode;\n\t\treturn {\n\t\t\tresult: textNode.data.config.value,\n\t\t};\n\t},\n\n\t\"prompt-crafter\": async (node, targetsData) => {\n\t\tconst promptNode = node as PromptCrafterNode;\n\t\tif (!targetsData) {\n\t\t\tthrow new Error(\"Targets data not found\");\n\t\t}\n\n\t\tlet parsedTemplate = promptNode.data.config.template;\n\t\tfor (const [targetId, targetValue] of Object.entries(targetsData)) {\n\t\t\tconst tag = promptNode.data.dynamicHandles[\"template-tags\"].find(\n\t\t\t\t(handle) => handle.id === targetId,\n\t\t\t);\n\t\t\tif (!tag) {\n\t\t\t\tthrow new Error(`Tag with id ${targetId} not found`);\n\t\t\t}\n\t\t\tparsedTemplate = parsedTemplate.replaceAll(\n\t\t\t\t`{{${tag.name}}}`,\n\t\t\t\ttargetValue,\n\t\t\t);\n\t\t}\n\t\treturn {\n\t\t\tresult: parsedTemplate,\n\t\t};\n\t},\n\n\t\"generate-text\": async (node, targetsData) => {\n\t\tconst generateNode = node as GenerateTextNode;\n\t\tconst system = targetsData?.system;\n\t\tconst prompt = targetsData?.prompt;\n\t\tif (!prompt) {\n\t\t\tthrow new Error(\"Prompt not found\");\n\t\t}\n\n\t\tconst result = await generateAIText({\n\t\t\tprompt,\n\t\t\tsystem,\n\t\t\tmodel: generateNode.data.config.model,\n\t\t\ttools: generateNode.data.dynamicHandles.tools,\n\t\t});\n\n\t\treturn result.parsedResult;\n\t},\n\n\t\"visualize-text\": async () => {\n\t\treturn undefined;\n\t},\n};\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/node-factory.ts",
			"content": "import { nanoid } from \"nanoid\";\nimport type { FlowNode } from \"@/registry/lib/flow/workflow\";\nimport type { GenerateTextNodeController } from \"@/registry/ui/flow/generate-text-node-controller\";\nimport type { PromptCrafterNodeController } from \"@/registry/ui/flow/prompt-crafter-node-controller\";\nimport type { TextInputNodeController } from \"@/registry/ui/flow/text-input-node-controller\";\nimport type { VisualizeTextNodeController } from \"@/registry/ui/flow/visualize-text-node-controller\";\n\nexport type NodePosition = {\n\tx: number;\n\ty: number;\n};\n\nexport const nodeFactory = {\n\t\"generate-text\": (position: NodePosition): GenerateTextNodeController => ({\n\t\tid: nanoid(),\n\t\ttype: \"generate-text\",\n\t\tposition,\n\t\tdata: {\n\t\t\tconfig: {\n\t\t\t\tmodel: \"llama-3.1-8b-instant\",\n\t\t\t},\n\t\t\tdynamicHandles: {\n\t\t\t\ttools: [],\n\t\t\t},\n\t\t},\n\t}),\n\n\t\"prompt-crafter\": (\n\t\tposition: NodePosition,\n\t): PromptCrafterNodeController => ({\n\t\tid: nanoid(),\n\t\ttype: \"prompt-crafter\",\n\t\tposition,\n\t\tdata: {\n\t\t\tconfig: {\n\t\t\t\ttemplate: \"\",\n\t\t\t},\n\t\t\tdynamicHandles: {\n\t\t\t\t\"template-tags\": [],\n\t\t\t},\n\t\t},\n\t}),\n\n\t\"visualize-text\": (\n\t\tposition: NodePosition,\n\t): VisualizeTextNodeController => ({\n\t\tid: nanoid(),\n\t\ttype: \"visualize-text\",\n\t\tposition,\n\t\tdata: {},\n\t\twidth: 350,\n\t\theight: 300,\n\t}),\n\n\t\"text-input\": (position: NodePosition): TextInputNodeController => ({\n\t\tid: nanoid(),\n\t\ttype: \"text-input\",\n\t\tposition,\n\t\tdata: {\n\t\t\tconfig: {\n\t\t\t\tvalue: \"\",\n\t\t\t},\n\t\t},\n\t\twidth: 350,\n\t\theight: 300,\n\t}),\n};\n\nexport function createNode(\n\tnodeType: FlowNode[\"type\"],\n\tposition: NodePosition,\n): FlowNode {\n\tif (!nodeType) {\n\t\tthrow new Error(\"Node type is required\");\n\t}\n\tconst factory = nodeFactory[nodeType];\n\tif (!factory) {\n\t\tthrow new Error(`Unknown node type: ${nodeType}`);\n\t}\n\treturn factory(position);\n}\n",
			"type": "registry:lib"
		},
		{
			"path": "./src/registry/lib/flow/generate-ai-text.ts",
			"content": "import { deepseek } from \"@ai-sdk/deepseek\";\nimport { groq } from \"@ai-sdk/groq\";\nimport { openai } from \"@ai-sdk/openai\";\nimport { generateText } from \"ai\";\nimport { z } from \"zod\";\nimport type { GenerateTextNode } from \"@/registry/ui/flow/generate-text-node\";\nimport type { Model } from \"@/registry/ui/model-selector\";\n\ninterface ToolResult {\n\tid: string;\n\tname: string;\n\tresult: string;\n}\n\nfunction createAIClient(model: Model) {\n\tswitch (model) {\n\t\tcase \"deepseek-chat\":\n\t\t\treturn deepseek;\n\t\tcase \"llama-3.3-70b-versatile\":\n\t\tcase \"llama-3.1-8b-instant\":\n\t\tcase \"deepseek-r1-distill-llama-70b\":\n\t\t\treturn groq;\n\t\tcase \"gpt-4o\":\n\t\tcase \"gpt-4o-mini\":\n\t\t\treturn openai;\n\t\tdefault:\n\t\t\tthrow new Error(`Unsupported model: ${model}`);\n\t}\n}\n\nfunction mapToolsForAI(\n\ttools: GenerateTextNode[\"data\"][\"dynamicHandles\"][\"tools\"],\n) {\n\treturn Object.fromEntries(\n\t\ttools.map((toolToMap) => [\n\t\t\ttoolToMap.name,\n\t\t\t{\n\t\t\t\tdescription: toolToMap.description,\n\t\t\t\tinputSchema: z.object({\n\t\t\t\t\ttoolValue: z.string(),\n\t\t\t\t}),\n\t\t\t\texecute: async ({ toolValue }: { toolValue: string }) =>\n\t\t\t\t\ttoolValue,\n\t\t\t},\n\t\t]),\n\t);\n}\n\nexport async function generateAIText({\n\tprompt,\n\tsystem,\n\tmodel,\n\ttools,\n}: {\n\tprompt: string;\n\tsystem?: string;\n\tmodel: Model;\n\ttools: GenerateTextNode[\"data\"][\"dynamicHandles\"][\"tools\"];\n}) {\n\tconst client = createAIClient(model);\n\tconst mappedTools = mapToolsForAI(tools);\n\n\tconst result = await generateText({\n\t\tmodel: client(model),\n\t\tsystem,\n\t\tprompt,\n\t\t...(tools.length > 0 && {\n\t\t\ttools: mappedTools,\n\t\t\tmaxSteps: 1,\n\t\t\ttoolChoice: \"required\",\n\t\t}),\n\t});\n\n\tlet toolResults: ToolResult[] = [];\n\tif (tools.length > 0 && result.toolResults) {\n\t\ttoolResults = result.toolResults.map((step) => {\n\t\t\tconst originalTool = tools.find(\n\t\t\t\t(tool) => tool.name === step.toolName,\n\t\t\t);\n\t\t\treturn {\n\t\t\t\tid: originalTool?.id || \"\",\n\t\t\t\tname: step.toolName,\n\t\t\t\tdescription: originalTool?.description || \"\",\n\t\t\t\tresult: step.output as string,\n\t\t\t};\n\t\t});\n\t}\n\n\tconst parsedResult: Record<string, string> = {\n\t\tresult: result.text,\n\t};\n\n\tfor (const toolResult of toolResults) {\n\t\tparsedResult[toolResult.id] = toolResult.result;\n\t}\n\n\treturn {\n\t\ttext: result.text,\n\t\ttoolResults,\n\t\ttotalTokens: result.usage?.totalTokens,\n\t\tparsedResult,\n\t};\n}\n",
			"type": "registry:lib"
		}
	],
	"envVars": {
		"OPENAI_API_KEY": "",
		"GROQ_API_KEY": "",
		"DEEPSEEK_API_KEY": ""
	},
	"docs": "Get API keys from OpenAI (https://platform.openai.com/), Groq (https://console.groq.com/), and DeepSeek (https://platform.deepseek.com/) and add them to the environment variables file and run the dev server. \n\nLearn more about how to use the Vercel AI SDK https://ai-sdk.dev/docs/introduction.",
	"categories": ["flow"]
}
