Many things

This commit is contained in:
Laurie Voss
2025-07-15 16:54:46 -07:00
parent 95ba2c63b3
commit 1b14c85b01
19 changed files with 12173 additions and 403 deletions
+35
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
}
}
}
+1 -1
View File
@@ -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.
+11538 -103
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -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",
+3 -1
View File
@@ -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;
+106 -184
View File
@@ -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;
+43 -90
View File
@@ -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;
+91
View File
@@ -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;
+7 -3
View File
@@ -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;
+88
View File
@@ -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,
}
}
+3 -3
View File
@@ -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;
+12
View File
@@ -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[] = [];
+58
View File
@@ -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);
};
+71
View File
@@ -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
View File
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,