mirror of
https://github.com/run-llama/flow-maker.git
synced 2026-06-30 21:17:56 -04:00
Many things
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const authorization = req.headers.get('authorization');
|
||||
if (!authorization) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const projectId = searchParams.get('projectId');
|
||||
|
||||
if (!projectId) {
|
||||
return NextResponse.json({ error: 'projectId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`https://api.cloud.llamaindex.ai/api/v1/pipelines?project_id=${projectId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': authorization,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return NextResponse.json({ error: `Failed to fetch from LlamaCloud: ${errorText}` }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const authorization = req.headers.get('authorization');
|
||||
if (!authorization) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('https://api.cloud.llamaindex.ai/api/v1/projects/current', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': authorization,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
return NextResponse.json({ error: `Failed to fetch from LlamaCloud: ${errorText}` }, { status: response.status });
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data);
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: (error as Error).message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
+7
-4
@@ -22,7 +22,7 @@ All colors MUST be HSL.
|
||||
--primary: 262 83% 58%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--primary-glow: 262 100% 75%;
|
||||
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
@@ -54,11 +54,11 @@ All colors MUST be HSL.
|
||||
/* Gradients */
|
||||
--gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow)));
|
||||
--gradient-node: linear-gradient(135deg, hsl(var(--card)), hsl(var(--muted)));
|
||||
|
||||
|
||||
/* Shadows */
|
||||
--shadow-node: 0 4px 20px -8px hsl(var(--primary) / 0.2);
|
||||
--shadow-glow: 0 0 20px hsl(var(--primary-glow) / 0.3);
|
||||
|
||||
|
||||
/* Animations */
|
||||
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
@@ -301,4 +301,7 @@ All colors MUST be HSL.
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: var(--radius);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
}
|
||||
/* ---break---
|
||||
*/
|
||||
@source '../node_modules/@llamaindex/chat-ui/**/*.{ts,tsx}'
|
||||
+74
-10
@@ -2,21 +2,76 @@
|
||||
import { useState } from "react";
|
||||
import AgentFlow from "@/components/AgentFlow";
|
||||
import RunView from "@/components/RunView";
|
||||
import CompileView from "@/components/CompileView";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Sidebar } from "@/components/ui/sidebar";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { Node, Edge, useNodesState, useEdgesState, OnNodesChange, NodeChange } from '@xyflow/react';
|
||||
import { initialNodes, initialEdges } from "@/lib/initial-graph";
|
||||
import { SettingsData, defaultSettings } from "@/components/AgentBuilderSettings";
|
||||
|
||||
const getInitialNodes = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedNodes = localStorage.getItem('agent-builder-nodes');
|
||||
if (savedNodes) {
|
||||
try {
|
||||
return JSON.parse(savedNodes);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse saved nodes:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return initialNodes;
|
||||
}
|
||||
|
||||
const getInitialEdges = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedEdges = localStorage.getItem('agent-builder-edges');
|
||||
if (savedEdges) {
|
||||
try {
|
||||
return JSON.parse(savedEdges);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse saved edges:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return initialEdges;
|
||||
}
|
||||
|
||||
const getInitialSettings = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedSettings = localStorage.getItem('agent-builder-settings');
|
||||
if (savedSettings) {
|
||||
try {
|
||||
return JSON.parse(savedSettings);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse saved settings:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
export default function IndexPage() {
|
||||
const isMobile = useIsMobile();
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<'edit' | 'run'>('edit');
|
||||
const [activeTab, setActiveTab] = useState<'edit' | 'compile' | 'run'>('edit');
|
||||
|
||||
const [nodes, setNodes, onNodesChangeOriginal] = useNodesState(getInitialNodes());
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(getInitialEdges());
|
||||
const [settings, setSettings] = useState<SettingsData>(getInitialSettings());
|
||||
|
||||
const onNodesChange: OnNodesChange = (changes) => {
|
||||
for (const change of changes) {
|
||||
if (change.type === 'remove') {
|
||||
const nodeToRemove = nodes.find(n => n.id === change.id);
|
||||
if (nodeToRemove && nodeToRemove.type === 'agentTool') {
|
||||
localStorage.removeItem(`agent-tool-config-${change.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
onNodesChangeOriginal(changes);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col">
|
||||
@@ -29,6 +84,13 @@ export default function IndexPage() {
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'compile' ? 'default' : 'ghost'}
|
||||
onClick={() => setActiveTab('compile')}
|
||||
className="rounded-none border-r border-border h-full"
|
||||
>
|
||||
Compile
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'run' ? 'default' : 'ghost'}
|
||||
onClick={() => setActiveTab('run')}
|
||||
@@ -40,7 +102,9 @@ export default function IndexPage() {
|
||||
|
||||
{/* Tab Content */}
|
||||
<div style={{ height: 'calc(100vh - 3rem)' }}>
|
||||
{activeTab === 'edit' ? <AgentFlow /> : <RunView />}
|
||||
{activeTab === 'edit' && <AgentFlow nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} setNodes={setNodes} setEdges={setEdges} settings={settings} setSettings={setSettings} />}
|
||||
{activeTab === 'compile' && <CompileView nodes={nodes} edges={edges} />}
|
||||
{activeTab === 'run' && <RunView />}
|
||||
</div>
|
||||
<Toaster />
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "src/index.css",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
@@ -17,4 +17,4 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -2,4 +2,4 @@
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
Generated
+11538
-103
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -10,6 +10,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@llamaindex/chat-ui": "^0.5.16",
|
||||
"@llamaindex/workflow-core": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
@@ -46,13 +48,14 @@
|
||||
"embla-carousel-react": "^8.3.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"lucide-react": "^0.462.0",
|
||||
"next": "^15.4.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"next": "14.2.5",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-resizable-panels": "^2.1.3",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"recharts": "^2.12.7",
|
||||
"sonner": "^1.5.0",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
@@ -66,6 +69,7 @@
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.9.0",
|
||||
|
||||
@@ -134,6 +134,7 @@ const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderS
|
||||
onChange={(e) => updateSetting('llamaCloudApiKey', e.target.value)}
|
||||
placeholder="Enter LlamaCloud API key"
|
||||
className="text-xs pr-8"
|
||||
data-1p-ignore
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -177,6 +178,7 @@ const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderS
|
||||
onChange={(e) => updateCurrentApiKey(e.target.value)}
|
||||
placeholder={getApiKeyPlaceholder()}
|
||||
className="text-xs pr-8"
|
||||
data-1p-ignore
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -197,4 +199,4 @@ const AgentBuilderSettings = memo(({ settings, onUpdateSettings }: AgentBuilderS
|
||||
|
||||
AgentBuilderSettings.displayName = 'AgentBuilderSettings';
|
||||
|
||||
export default AgentBuilderSettings;
|
||||
export default AgentBuilderSettings;
|
||||
|
||||
@@ -1,88 +1,55 @@
|
||||
import { memo } from 'react';
|
||||
import { Card } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from './ui/accordion';
|
||||
import {
|
||||
Play,
|
||||
Square,
|
||||
MessageCircle,
|
||||
Brain,
|
||||
Bot,
|
||||
Wrench,
|
||||
Split,
|
||||
Merge,
|
||||
GitBranch,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
Settings,
|
||||
Blocks
|
||||
} from 'lucide-react';
|
||||
import AgentBuilderSettings from './AgentBuilderSettings';
|
||||
import { SettingsData } from './AgentBuilderSettings';
|
||||
import {
|
||||
Card,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion"
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Settings, Trash2, FileJson, Blocks, MessageCircle, Brain, Bot, Wrench, Split, Merge, GitBranch, Play, Square, AlertTriangle } from 'lucide-react';
|
||||
import AgentBuilderSettings, { SettingsData } from './AgentBuilderSettings';
|
||||
|
||||
interface NodeTemplate {
|
||||
type: string;
|
||||
label: string;
|
||||
icon: React.ComponentType<any>;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const nodeTemplates: NodeTemplate[] = [
|
||||
{
|
||||
type: 'start',
|
||||
label: 'Start',
|
||||
icon: Play,
|
||||
description: 'Entry point for the agent flow'
|
||||
},
|
||||
{
|
||||
type: 'stop',
|
||||
label: 'Stop',
|
||||
icon: Square,
|
||||
description: 'End point for the agent flow'
|
||||
},
|
||||
{
|
||||
type: 'userInput',
|
||||
label: 'User Input',
|
||||
icon: MessageCircle,
|
||||
description: 'Capture user input or prompts'
|
||||
},
|
||||
{
|
||||
type: 'promptLLM',
|
||||
label: 'Prompt LLM',
|
||||
icon: Brain,
|
||||
description: 'Send prompts to language models'
|
||||
},
|
||||
{
|
||||
type: 'promptAgent',
|
||||
label: 'Prompt Agent',
|
||||
icon: Bot,
|
||||
description: 'Configure and prompt AI agents'
|
||||
},
|
||||
{
|
||||
type: 'agentTool',
|
||||
label: 'Agent Tool',
|
||||
icon: Wrench,
|
||||
description: 'Tools and functions for agents'
|
||||
},
|
||||
{
|
||||
type: 'splitter',
|
||||
label: 'Splitter',
|
||||
icon: Split,
|
||||
description: 'Split flow into multiple paths'
|
||||
},
|
||||
{
|
||||
type: 'collector',
|
||||
label: 'Collector',
|
||||
icon: Merge,
|
||||
description: 'Merge multiple flows together'
|
||||
},
|
||||
{
|
||||
type: 'decision',
|
||||
label: 'Decision',
|
||||
icon: GitBranch,
|
||||
description: 'Conditional branching logic'
|
||||
interface NodeTemplateProps {
|
||||
type: string;
|
||||
label: string;
|
||||
description: string;
|
||||
icon: React.ElementType;
|
||||
onAddNode: (type: string) => void;
|
||||
}
|
||||
];
|
||||
|
||||
const NodeTemplate = memo(({ type, label, description, icon: Icon, onAddNode }: NodeTemplateProps) => {
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="p-2 cursor-grab active:cursor-grabbing border-border hover:border-primary/50 transition-colors"
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, type)}
|
||||
onClick={() => onAddNode(type)}
|
||||
>
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="p-1.5 rounded-md bg-primary/10 flex-shrink-0">
|
||||
<Icon className="w-3.5 h-3.5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{label}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground leading-tight">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
NodeTemplate.displayName = 'NodeTemplate';
|
||||
|
||||
interface AgentBuilderSidebarProps {
|
||||
onAddNode: (type: string) => void;
|
||||
@@ -91,111 +58,66 @@ interface AgentBuilderSidebarProps {
|
||||
onUpdateSettings: (settings: SettingsData) => void;
|
||||
}
|
||||
|
||||
const AgentBuilderSidebar = memo(({ onAddNode, onReset, settings, onUpdateSettings }: AgentBuilderSidebarProps) => {
|
||||
const onDragStart = (event: React.DragEvent, nodeType: string) => {
|
||||
event.dataTransfer.setData('application/reactflow', nodeType);
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
};
|
||||
const nodeTemplates = [
|
||||
{ type: 'start', label: 'Start', description: 'Entry point for the agent flow', icon: Play },
|
||||
{ type: 'stop', label: 'Stop', description: 'End point for the agent flow', icon: Square },
|
||||
{ type: 'userInput', label: 'User Input', description: 'Capture user input or prompts', icon: MessageCircle },
|
||||
{ type: 'promptLLM', label: 'Prompt LLM', description: 'Send prompts to language models', icon: Brain },
|
||||
{ type: 'promptAgent', label: 'Prompt Agent', description: 'Configure and prompt AI agents', icon: Bot },
|
||||
{ type: 'agentTool', label: 'Agent Tool', description: 'Tools and functions for agents', icon: Wrench },
|
||||
{ type: 'splitter', label: 'Splitter', description: 'Split flow into multiple paths', icon: Split },
|
||||
{ type: 'collector', label: 'Collector', description: 'Merge multiple flows together', icon: Merge },
|
||||
{ type: 'decision', label: 'Decision', description: 'Conditional branching logic', icon: GitBranch },
|
||||
];
|
||||
|
||||
const AgentBuilderSidebar = ({ onAddNode, onReset, settings, onUpdateSettings }: AgentBuilderSidebarProps) => {
|
||||
|
||||
return (
|
||||
<div className="w-80 h-full bg-card border-r border-border p-4 overflow-y-auto">
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-bold text-foreground mb-2">Agent Builder</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Drag and drop components to build your LlamaIndex agent flow
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Accordion type="multiple" defaultValue={["components", "settings"]} className="w-full">
|
||||
{/* Components Section */}
|
||||
<AccordionItem value="components">
|
||||
<AccordionTrigger className="text-sm font-semibold hover:no-underline">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Blocks className="w-4 h-4 text-primary" />
|
||||
<span>Components</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-3 pt-2">
|
||||
{nodeTemplates.map((template) => {
|
||||
const IconComponent = template.icon;
|
||||
return (
|
||||
<Card
|
||||
key={template.type}
|
||||
className="p-2 cursor-grab active:cursor-grabbing border-border hover:border-primary/50 transition-colors"
|
||||
draggable
|
||||
onDragStart={(e) => onDragStart(e, template.type)}
|
||||
onClick={() => onAddNode(template.type)}
|
||||
>
|
||||
<div className="flex items-start space-x-2">
|
||||
<div className="p-1.5 rounded-md bg-primary/10 flex-shrink-0">
|
||||
<IconComponent className="w-3.5 h-3.5 text-primary" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="text-sm font-medium text-foreground truncate">
|
||||
{template.label}
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground leading-tight">
|
||||
{template.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Settings Section */}
|
||||
<AccordionItem value="settings">
|
||||
<AccordionTrigger className="text-sm font-semibold hover:no-underline">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Settings className="w-4 h-4 text-primary" />
|
||||
<span>Settings</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="pt-2">
|
||||
<AgentBuilderSettings settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
{/* Danger Zone Section */}
|
||||
<AccordionItem value="danger-zone">
|
||||
<AccordionTrigger className="text-sm font-semibold hover:no-underline">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertTriangle className="w-4 h-4 text-destructive" />
|
||||
<span>Danger Zone</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="pt-2">
|
||||
<Card className="p-3 border-destructive/20 bg-destructive/5">
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<aside className="w-80 bg-card border-r border-border p-4 flex flex-col space-y-4">
|
||||
<h2 className="text-xl font-semibold">Agent Builder</h2>
|
||||
|
||||
<div className="flex-grow overflow-y-auto">
|
||||
<Accordion type="multiple" defaultValue={['nodes', 'settings']} className="w-full">
|
||||
<AccordionItem value="nodes">
|
||||
<AccordionTrigger className="text-base font-medium">
|
||||
<Blocks className="mr-2 h-4 w-4" /> Nodes
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="p-2 space-y-2">
|
||||
{nodeTemplates.map((template) => (
|
||||
<NodeTemplate key={template.type} {...template} onAddNode={onAddNode} />
|
||||
))}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem value="settings">
|
||||
<AccordionTrigger className="text-base font-medium">
|
||||
<Settings className="mr-2 h-4 w-4" /> Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<AgentBuilderSettings settings={settings} onUpdateSettings={onUpdateSettings} />
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="danger-zone">
|
||||
<AccordionTrigger className="text-base font-medium text-destructive">
|
||||
<AlertTriangle className="mr-2 h-4 w-4" /> Danger Zone
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="p-2 space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This will permanently delete all nodes, connections, and settings.
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
className="w-full"
|
||||
>
|
||||
<Trash2 className="w-3 h-3 mr-2" />
|
||||
</p>
|
||||
<Button variant="destructive" onClick={onReset} className="w-full justify-start">
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Reset Everything
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</Button>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 space-y-2">
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
AgentBuilderSidebar.displayName = 'AgentBuilderSidebar';
|
||||
|
||||
export default AgentBuilderSidebar;
|
||||
export default AgentBuilderSidebar;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import { useCallback, useRef, useState, useEffect, Dispatch, SetStateAction } from 'react';
|
||||
import {
|
||||
ReactFlow,
|
||||
Background,
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
Node,
|
||||
MarkerType,
|
||||
useReactFlow,
|
||||
ReactFlowProvider
|
||||
ReactFlowProvider,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
} from '@xyflow/react';
|
||||
|
||||
// Import custom nodes
|
||||
@@ -43,67 +45,27 @@ const nodeTypes = {
|
||||
decision: DecisionNode,
|
||||
};
|
||||
|
||||
// Initial nodes and edges
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'start',
|
||||
position: { x: 100, y: 200 },
|
||||
data: { label: 'Start' }
|
||||
}
|
||||
];
|
||||
interface AgentFlowInnerProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
onNodesChange: OnNodesChange;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
setNodes: Dispatch<SetStateAction<Node[]>>;
|
||||
setEdges: Dispatch<SetStateAction<Edge[]>>;
|
||||
settings: SettingsData;
|
||||
setSettings: Dispatch<SetStateAction<SettingsData>>;
|
||||
}
|
||||
|
||||
const initialEdges: Edge[] = [];
|
||||
|
||||
// Load saved graph from localStorage
|
||||
const loadSavedGraph = () => {
|
||||
try {
|
||||
const savedNodes = localStorage.getItem('agent-builder-nodes');
|
||||
const savedEdges = localStorage.getItem('agent-builder-edges');
|
||||
|
||||
if (savedNodes && savedEdges) {
|
||||
return {
|
||||
nodes: JSON.parse(savedNodes),
|
||||
edges: JSON.parse(savedEdges)
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading saved graph:', error);
|
||||
}
|
||||
|
||||
return {
|
||||
nodes: initialNodes,
|
||||
edges: initialEdges
|
||||
};
|
||||
};
|
||||
|
||||
const savedGraph = loadSavedGraph();
|
||||
|
||||
const AgentFlowInner = () => {
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(savedGraph.nodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(savedGraph.edges);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [settings, setSettings] = useState<SettingsData>(defaultSettings);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const AgentFlowInner = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes, setEdges, settings, setSettings }: AgentFlowInnerProps) => {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { screenToFlowPosition } = useReactFlow();
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
|
||||
// onUpdateSettings sets the settings
|
||||
const onUpdateSettings = (newSettings: SettingsData) => {
|
||||
setSettings(newSettings);
|
||||
};
|
||||
|
||||
// Load settings from localStorage on mount
|
||||
useEffect(() => {
|
||||
try {
|
||||
const savedSettings = localStorage.getItem('agent-builder-settings');
|
||||
if (savedSettings) {
|
||||
setSettings(JSON.parse(savedSettings));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Save settings to localStorage whenever they change
|
||||
useEffect(() => {
|
||||
try {
|
||||
@@ -120,31 +82,6 @@ const AgentFlowInner = () => {
|
||||
return (maxId + 1).toString();
|
||||
}, [nodes]);
|
||||
|
||||
// Handle delete key presses
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Delete' || event.key === 'Backspace') {
|
||||
// Don't delete nodes if user is editing text in an input or textarea
|
||||
const activeElement = document.activeElement as HTMLElement;
|
||||
if (activeElement && (
|
||||
activeElement.tagName === 'INPUT' ||
|
||||
activeElement.tagName === 'TEXTAREA' ||
|
||||
activeElement.isContentEditable
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete selected nodes
|
||||
setNodes((nds) => nds.filter((node) => !node.selected));
|
||||
// Delete selected edges
|
||||
setEdges((eds) => eds.filter((edge) => !edge.selected));
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [setNodes, setEdges]);
|
||||
|
||||
// Auto-save to localStorage whenever nodes or edges change
|
||||
useEffect(() => {
|
||||
const saveGraph = async () => {
|
||||
@@ -177,7 +114,10 @@ const AgentFlowInner = () => {
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
stroke: 'hsl(var(--muted-foreground))'
|
||||
}
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
};
|
||||
setEdges((eds) => addEdge(newEdge, eds));
|
||||
},
|
||||
@@ -280,6 +220,7 @@ const AgentFlowInner = () => {
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
nodeTypes={nodeTypes}
|
||||
deleteKeyCode={['Backspace', 'Delete']}
|
||||
fitView
|
||||
className="bg-flow-bg"
|
||||
defaultEdgeOptions={{
|
||||
@@ -304,12 +245,24 @@ const AgentFlowInner = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const AgentFlow = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<AgentFlowInner />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
interface AgentFlowProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
onNodesChange: OnNodesChange;
|
||||
onEdgesChange: OnEdgesChange;
|
||||
setNodes: Dispatch<SetStateAction<Node[]>>;
|
||||
setEdges: Dispatch<SetStateAction<Edge[]>>;
|
||||
settings: SettingsData;
|
||||
setSettings: Dispatch<SetStateAction<SettingsData>>;
|
||||
}
|
||||
|
||||
export default AgentFlow;
|
||||
const AgentFlow = ({ nodes, edges, onNodesChange, onEdgesChange, setNodes, setEdges, settings, setSettings }: AgentFlowProps) => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<AgentFlowInner nodes={nodes} edges={edges} onNodesChange={onNodesChange} onEdgesChange={onEdgesChange} setNodes={setNodes} setEdges={setEdges} settings={settings} setSettings={setSettings} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentFlow;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||
import { vs } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||
import { generateWorkflowJson } from '@/lib/workflow-compiler';
|
||||
import { generateTypescript } from '@/lib/typescript-compiler';
|
||||
import { Node, Edge } from '@xyflow/react';
|
||||
|
||||
interface CompileViewProps {
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
}
|
||||
|
||||
const CompileView = ({ nodes, edges }: CompileViewProps) => {
|
||||
const [generatedCode, setGeneratedCode] = useState<string | null>("// Click 'Compile' to generate the workflow code");
|
||||
const [isDebug, setIsDebug] = useState(false);
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
setIsDebug(searchParams.get('debug') === '1');
|
||||
}, [searchParams]);
|
||||
|
||||
const onCompile = () => {
|
||||
const workflowJson = generateWorkflowJson(nodes, edges);
|
||||
if (workflowJson) {
|
||||
const tsCode = generateTypescript(workflowJson);
|
||||
setGeneratedCode(tsCode);
|
||||
} else {
|
||||
setGeneratedCode("// Could not generate workflow. Make sure you have a Start node.");
|
||||
}
|
||||
};
|
||||
|
||||
const onShowIntermediate = () => {
|
||||
const workflowJson = generateWorkflowJson(nodes, edges);
|
||||
if (workflowJson) {
|
||||
setGeneratedCode(JSON.stringify(workflowJson, null, 2));
|
||||
} else {
|
||||
setGeneratedCode("// Could not generate workflow. Make sure you have a Start node.");
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = () => {
|
||||
if (generatedCode) {
|
||||
const blob = new Blob([generatedCode], { type: 'text/typescript' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'workflow.ts';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex">
|
||||
<div className="w-80 bg-card border-r border-border p-4 flex flex-col space-y-4">
|
||||
<h2 className="text-xl font-semibold">Compile Workflow</h2>
|
||||
<p className='text-sm text-muted-foreground'>
|
||||
Click the compile button to generate the TypeScript code for your workflow.
|
||||
You can then download the file and run it.
|
||||
</p>
|
||||
<Button onClick={onCompile}>Compile</Button>
|
||||
{isDebug && <Button onClick={onShowIntermediate}>Intermediate</Button>}
|
||||
<Button onClick={handleDownload} disabled={!generatedCode || generatedCode.startsWith("//")}>Download</Button>
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-hidden">
|
||||
<div className="h-full bg-card border rounded-md overflow-auto">
|
||||
<SyntaxHighlighter
|
||||
language="typescript"
|
||||
style={vs}
|
||||
showLineNumbers
|
||||
className="h-full"
|
||||
wrapLongLines={true}
|
||||
customStyle={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{generatedCode || ''}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CompileView;
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
useEdgesState,
|
||||
Node,
|
||||
Edge,
|
||||
ReactFlowProvider
|
||||
ReactFlowProvider,
|
||||
MarkerType
|
||||
} from '@xyflow/react';
|
||||
|
||||
// Import custom nodes (read-only versions)
|
||||
@@ -92,7 +93,10 @@ const RunViewInner = () => {
|
||||
style: {
|
||||
strokeWidth: 2,
|
||||
stroke: 'hsl(var(--muted-foreground))'
|
||||
}
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background color="hsl(var(--border))" gap={20} />
|
||||
@@ -116,4 +120,4 @@ const RunView = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default RunView;
|
||||
export default RunView;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
ChatHandler,
|
||||
ChatSection as ChatSectionUI,
|
||||
Message,
|
||||
} from '@llamaindex/chat-ui'
|
||||
|
||||
import '@llamaindex/chat-ui/styles/markdown.css'
|
||||
import '@llamaindex/chat-ui/styles/pdf.css'
|
||||
import '@llamaindex/chat-ui/styles/editor.css'
|
||||
import { useState } from 'react'
|
||||
|
||||
const initialMessages: Message[] = [
|
||||
{
|
||||
content: 'Write simple Javascript hello world code',
|
||||
role: 'user',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
'Got it! Here\'s the simplest JavaScript code to print "Hello, World!" to the console:\n\n```javascript\nconsole.log("Hello, World!");\n```\n\nYou can run this code in any JavaScript environment, such as a web browser\'s console or a Node.js environment. Just paste the code and execute it to see the output.',
|
||||
},
|
||||
{
|
||||
content: 'Write a simple math equation',
|
||||
role: 'user',
|
||||
},
|
||||
{
|
||||
role: 'assistant',
|
||||
content:
|
||||
"Let's explore a simple mathematical equation using LaTeX:\n\n The quadratic formula is: $$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$$\n\nThis formula helps us solve quadratic equations in the form $ax^2 + bx + c = 0$. The solution gives us the x-values where the parabola intersects the x-axis.",
|
||||
},
|
||||
]
|
||||
|
||||
export function ChatSection() {
|
||||
// You can replace the handler with a useChat hook from Vercel AI SDK
|
||||
const handler = useMockChat(initialMessages)
|
||||
return (
|
||||
<div className="flex max-h-[80vh] flex-col gap-6 overflow-y-auto">
|
||||
<ChatSectionUI handler={handler} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function useMockChat(initMessages: Message[]): ChatHandler {
|
||||
const [messages, setMessages] = useState<Message[]>(initMessages)
|
||||
const [input, setInput] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const append = async (message: Message) => {
|
||||
setIsLoading(true)
|
||||
|
||||
const mockResponse: Message = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
setMessages(prev => [...prev, message, mockResponse])
|
||||
|
||||
const mockContent =
|
||||
'This is a mock response. In a real implementation, this would be replaced with an actual AI response.'
|
||||
|
||||
let streamedContent = ''
|
||||
const words = mockContent.split(' ')
|
||||
|
||||
for (const word of words) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
streamedContent += (streamedContent ? ' ' : '') + word
|
||||
setMessages(prev => {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{
|
||||
role: 'assistant',
|
||||
content: streamedContent,
|
||||
},
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
return mockContent
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
input,
|
||||
setInput,
|
||||
isLoading,
|
||||
append,
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ const AgentToolNode = memo(({ id, data, selected }: AgentToolNodeProps) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// First, get the current project
|
||||
const projectResponse = await fetch('/api/llamacloud/api/v1/projects/current', {
|
||||
const projectResponse = await fetch('/api/llamacloud/v1/projects/current', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@@ -148,7 +148,7 @@ const AgentToolNode = memo(({ id, data, selected }: AgentToolNodeProps) => {
|
||||
const projectData: LlamaCloudProject = await projectResponse.json();
|
||||
|
||||
// Then, get the pipelines for this project
|
||||
const pipelinesResponse = await fetch(`/api/llamacloud/api/v1/pipelines?project_id=${projectData.id}`, {
|
||||
const pipelinesResponse = await fetch(`/api/llamacloud/v1/pipelines?projectId=${projectData.id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
@@ -257,4 +257,4 @@ const AgentToolNode = memo(({ id, data, selected }: AgentToolNodeProps) => {
|
||||
|
||||
AgentToolNode.displayName = 'AgentToolNode';
|
||||
|
||||
export default AgentToolNode;
|
||||
export default AgentToolNode;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Node, Edge } from '@xyflow/react';
|
||||
|
||||
export const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'start',
|
||||
position: { x: 100, y: 200 },
|
||||
data: { label: 'Start' }
|
||||
}
|
||||
];
|
||||
|
||||
export const initialEdges: Edge[] = [];
|
||||
@@ -0,0 +1,58 @@
|
||||
import { WorkflowJson, WorkflowNodeJson } from './workflow-compiler';
|
||||
|
||||
const LlamaIndexTemplate = (events: string, handlers: string) => `
|
||||
import { Workflow, workflow } from '@llamaindex/workflow-core';
|
||||
${events}
|
||||
|
||||
const w = new Workflow();
|
||||
${handlers}
|
||||
|
||||
export default w;
|
||||
`;
|
||||
|
||||
const sanitizeId = (id: string) => `event_${id.replace(/[^a-zA-Z0-9_]/g, '_')}`;
|
||||
|
||||
const generateEvents = (nodes: WorkflowNodeJson[]): string => {
|
||||
let eventLines: string[] = [];
|
||||
for (const node of nodes) {
|
||||
const eventName = sanitizeId(node.id);
|
||||
eventLines.push(`const ${eventName} = workflow.defineEvent<any>('${eventName}');`);
|
||||
}
|
||||
return eventLines.join('\n');
|
||||
}
|
||||
|
||||
const generateHandlers = (nodes: WorkflowNodeJson[]): string => {
|
||||
let handlerLines: string[] = [];
|
||||
for (const node of nodes) {
|
||||
const acceptsEventName = node.type === 'start' ? 'workflow.startEvent' : sanitizeId(node.id);
|
||||
|
||||
let handlerBody = '';
|
||||
if (node.emits) {
|
||||
if (typeof node.emits === 'string') {
|
||||
const emitEventName = sanitizeId(node.emits.replace('event-', ''));
|
||||
handlerBody = `w.emit(${emitEventName}, ctx.payload);`;
|
||||
} else if (Array.isArray(node.emits)) {
|
||||
handlerBody = node.emits.map(e => `w.emit(${sanitizeId(e.replace('event-', ''))}, ctx.payload);`).join('\n ');
|
||||
} else { // decision node
|
||||
const cases = Object.entries(node.emits).map(([key, value]) => {
|
||||
const emitEventName = sanitizeId(value.replace('event-', ''));
|
||||
return `if (ctx.payload.condition === '${key}') {\n w.emit(${emitEventName}, ctx.payload);\n }`;
|
||||
}).join(' else ');
|
||||
handlerBody = `${cases}`;
|
||||
}
|
||||
} else if (node.type === 'stop') {
|
||||
handlerBody = `console.log('Workflow stopped.');`;
|
||||
}
|
||||
|
||||
handlerLines.push(`w.on(${acceptsEventName}, (ctx) => {\n console.log('Handling event for node ${node.id}');\n ${handlerBody}\n});`);
|
||||
}
|
||||
return handlerLines.join('\n\n');
|
||||
}
|
||||
|
||||
export const generateTypescript = (json: WorkflowJson): string => {
|
||||
const events = generateEvents(json.nodes);
|
||||
const handlers = generateHandlers(json.nodes);
|
||||
|
||||
return LlamaIndexTemplate(events, handlers);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Node, Edge } from '@xyflow/react';
|
||||
|
||||
export interface WorkflowNodeJson {
|
||||
id: string;
|
||||
type: string;
|
||||
data: any;
|
||||
accepts?: string | string[];
|
||||
emits?: string | string[] | { [key:string]: string };
|
||||
}
|
||||
|
||||
export interface WorkflowJson {
|
||||
nodes: WorkflowNodeJson[];
|
||||
}
|
||||
|
||||
export const generateWorkflowJson = (nodes: Node[], edges: Edge[]): WorkflowJson | null => {
|
||||
if (nodes.length === 0) {
|
||||
return { nodes: [] };
|
||||
}
|
||||
|
||||
const workflowNodes: WorkflowNodeJson[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (!node.type) {
|
||||
console.error(`Node with id ${node.id} has no type`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const data: any = {};
|
||||
if (node.type === 'userInput' && node.data.prompt) {
|
||||
data.prompt = node.data.prompt;
|
||||
} else {
|
||||
for (const key in node.data) {
|
||||
if (key !== 'label') {
|
||||
data[key] = node.data[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workflowNode: WorkflowNodeJson = {
|
||||
id: node.id,
|
||||
type: node.type,
|
||||
data: data,
|
||||
};
|
||||
|
||||
// Determine 'accepts' from incoming edges
|
||||
if (node.type !== 'start') {
|
||||
workflowNode.accepts = `event-${node.id}`;
|
||||
}
|
||||
|
||||
// Determine 'emits' from outgoing edges
|
||||
const outgoingEdges = edges.filter(edge => edge.source === node.id);
|
||||
if (node.type !== 'stop' && outgoingEdges.length > 0) {
|
||||
if (node.type === 'decision') {
|
||||
const emitsObj: { [key: string]: string } = {};
|
||||
for (const edge of outgoingEdges) {
|
||||
const sourceHandle = edge.sourceHandle || 'default';
|
||||
emitsObj[sourceHandle] = `event-${edge.target}`;
|
||||
}
|
||||
workflowNode.emits = emitsObj;
|
||||
} else if (outgoingEdges.length > 1) { // Splitter
|
||||
workflowNode.emits = outgoingEdges.map(edge => `event-${edge.target}`);
|
||||
} else { // Single output
|
||||
workflowNode.emits = `event-${outgoingEdges[0].target}`;
|
||||
}
|
||||
}
|
||||
|
||||
workflowNodes.push(workflowNode);
|
||||
}
|
||||
|
||||
return { nodes: workflowNodes };
|
||||
};
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
Reference in New Issue
Block a user