From 75457e3f339e4f29b673466809509f6eebd3840f Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Mon, 20 Apr 2026 23:42:14 +0530 Subject: [PATCH] feat: live workflow trace + completed execution diagram in chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit During pending: - TypingBubble simplified to plain subtitle text (no bubble box): Sparkles icon + animated dots + status label (context-aware) - LiveWorkflowTrace below the text: nodes appear one by one with timing (Input→Context→LLM→Tools→Response), each node glows purple while active, turns green when complete, current node spins Loader2 After response (on each Nebula message): - WorkflowTrace renders below the message with a collapsible one-line summary: '⚡ 285ms · 4o-mini · 2 tools · 3 chunks ▶' - Expanded view: icon nodes in a horizontal scrollable flow with coloured circles, tool names, and detail labels per node (e.g. 'List Agents · 3 agents', 'Create Agent · ...abc123') - Nodes built from real response data: toolActions, chunksUsed, latencyMs, modelUsed (now stored on ChatMessage) - Shows RAG node only when chunks_used > 0 - Shows Synthesis node only when tools were called - Model prefix stripped (gpt-, openai/, anthropic/) for compact display Types: - Added modelUsed?: string to ChatMessage - aiMsg now stores resp.model_used --- .../src/components/layout/ChatWorkspace.tsx | 314 +++++++++++++++--- 1 file changed, 277 insertions(+), 37 deletions(-) diff --git a/webapp/src/components/layout/ChatWorkspace.tsx b/webapp/src/components/layout/ChatWorkspace.tsx index fd343b20..d08f7600 100644 --- a/webapp/src/components/layout/ChatWorkspace.tsx +++ b/webapp/src/components/layout/ChatWorkspace.tsx @@ -45,6 +45,7 @@ interface ChatMessage { toolActions?: ToolAction[] chunksUsed?: number latencyMs?: number + modelUsed?: string tryItData?: TryItData } @@ -756,25 +757,138 @@ function InlineSteps({ taskId }: { taskId: string }) { ) } -// ── Typing bubble — inline in thread, not a floating footer ─────────────── +// ── Typing indicator + live workflow trace ─────────────────────────────────── -function inferStatusLabel(lastUserMsg: string): string[] { +const TOOL_LABEL: Record = { + list_agents: 'List Agents', + create_agent: 'Create Agent', + run_agent: 'Run Agent', + create_plugin: 'Create Plugin', + publish_plugin: 'Publish Plugin', + list_integrations: 'Integrations', + connect_integration: 'Connect', + list_provider_tools: 'List Tools', + try_it_out: 'Run Demo', +} + +const TOOL_ICON: Record = { + list_agents: Bot, + create_agent: Bot, + run_agent: Zap, + create_plugin: Package, + publish_plugin: Globe, + list_integrations: Plug, + connect_integration: Plug, + list_provider_tools: Database, + try_it_out: Zap, +} + +function toolDetail(ta: ToolAction): string { + const r = ta.result as Record + if (ta.tool === 'list_agents') { + const agents = (r.agents as unknown[]) ?? [] + return `${agents.length} agent${agents.length !== 1 ? 's' : ''}` + } + if (ta.tool === 'create_agent') return r.agent_id ? String(r.agent_id).slice(-6) : 'created' + if (ta.tool === 'run_agent') return r.task_id ? String(r.task_id).slice(-6) : 'dispatched' + if (ta.tool === 'create_plugin') return r.draft ? 'draft' : (r.name as string) ?? 'saved' + if (ta.tool === 'publish_plugin') return 'published' + if (ta.tool === 'list_integrations') return 'catalog' + if (ta.tool === 'try_it_out') return (r.template_name as string) ?? 'demo' + return r.success === false ? 'error' : 'done' +} + +// — Live trace: builds up during pending ———————————————————————————— + +type LiveNode = { id: string; Icon: React.ElementType; label: string } + +function LiveWorkflowTrace({ lastUserMsg }: { lastUserMsg: string }) { const m = lastUserMsg.toLowerCase() - if (/plugin|tool|build.*plugin|write.*plugin/.test(m)) - return ['Thinking…', 'Writing plugin code…', 'Validating code…', 'Almost ready…'] - if (/run|execute|launch|start.*agent|dispatch/.test(m)) - return ['Thinking…', 'Dispatching task…', 'Agent is running…'] + const likelyTools = /plugin|agent|run|execute|search|integrat|connect|remember|save|fetch/.test(m) + + const allNodes: LiveNode[] = [ + { id: 'input', Icon: Send, label: 'Input' }, + { id: 'context', Icon: Database, label: 'Context' }, + { id: 'llm', Icon: Cpu, label: 'LLM' }, + ...(likelyTools ? [{ id: 'tools', Icon: Zap, label: 'Tools' }] : []), + { id: 'response', Icon: CheckCircle2, label: 'Response' }, + ] + + const [visible, setVisible] = useState(1) + const [running, setRunning] = useState(0) + + useEffect(() => { + const DELAYS = [150, 900, 2200, 4200] + let cancelled = false + DELAYS.forEach((delay, i) => { + setTimeout(() => { + if (cancelled) return + setVisible(i + 2) + setRunning(i + 1) + }, delay) + }) + return () => { cancelled = true } + }, []) + + return ( +
+ {allNodes.slice(0, visible).map((node, i) => { + const { Icon } = node + const isRunning = i === running + const isDone = i < running + return ( + + {i > 0 && ( + + )} +
+ {isRunning + ? + : } + {node.label} +
+
+ ) + })} +
+ ) +} + +function inferStatusLabel(msg: string): string[] { + const m = msg.toLowerCase() + if (/plugin|build.*plugin|write.*plugin/.test(m)) + return ['Writing plugin code…', 'Validating…', 'Almost ready…'] + if (/run|execute|launch|dispatch/.test(m)) + return ['Dispatching task…', 'Agent running…'] if (/agent|create.*agent|new.*agent/.test(m)) - return ['Thinking…', 'Checking agents…', 'Preparing response…'] + return ['Checking agents…', 'Preparing…'] if (/remember|save.*this|add.*memory|store/.test(m)) - return ['Thinking…', 'Checking memory…', 'Saving…'] + return ['Checking memory…'] if (/search|find|look.*up|fetch/.test(m)) - return ['Thinking…', 'Searching knowledge base…', 'Gathering results…'] + return ['Searching knowledge…', 'Gathering results…'] if (/integrat|connect|slack|github|jira|notion/.test(m)) - return ['Thinking…', 'Checking integrations…', 'Preparing response…'] - if (/status|health|running|active/.test(m)) - return ['Thinking…', 'Querying system state…', 'Preparing summary…'] - return ['Thinking…', 'Calling tools…', 'Preparing response…'] + return ['Checking integrations…', 'Preparing…'] + return ['Calling tools…', 'Preparing response…'] } function TypingBubble({ lastUserMsg }: { lastUserMsg: string }) { @@ -782,44 +896,161 @@ function TypingBubble({ lastUserMsg }: { lastUserMsg: string }) { const [step, setStep] = useState(0) useEffect(() => { - const DELAYS = [2000, 3500, 5000] + setStep(0) + const DELAYS = [2500, 5000] let idx = 0 function advance() { idx += 1 if (idx >= steps.length) return setStep(idx) - if (idx < steps.length - 1) { - setTimeout(advance, DELAYS[idx] ?? 4000) - } + if (idx < steps.length - 1) setTimeout(advance, DELAYS[idx] ?? 4000) } const t = setTimeout(advance, DELAYS[0]) return () => clearTimeout(t) }, [steps.length]) return ( -
-
- - Nebula -
-
- - - - +
+
+ + + {steps[step]}
+ +
+ ) +} + +// — Completed workflow trace: attached to the AI message after response —————— + +interface TraceNodeDef { + id: string + Icon: React.ElementType + label: string + detail?: string + color: string +} + +function WorkflowTrace({ + toolActions, chunksUsed, latencyMs, modelUsed, +}: { + toolActions?: ToolAction[] + chunksUsed?: number + latencyMs?: number + modelUsed?: string +}) { + const [open, setOpen] = useState(false) + + const nodes: TraceNodeDef[] = [] + + nodes.push({ id: 'input', Icon: Send, label: 'Input', color: 'var(--nbl-cyan)' }) + + if (chunksUsed && chunksUsed > 0) { + nodes.push({ + id: 'rag', Icon: BookOpen, label: 'Knowledge', + detail: `${chunksUsed} chunk${chunksUsed !== 1 ? 's' : ''}`, + color: 'var(--nbl-green)', + }) + } + + const hasTools = toolActions && toolActions.length > 0 + nodes.push({ + id: 'llm1', Icon: Cpu, + label: modelUsed ? modelUsed.replace(/^gpt-/, '') : 'LLM', + detail: hasTools ? undefined : latencyMs ? `${latencyMs}ms` : undefined, + color: 'var(--nbl-nebula)', + }) + + if (hasTools) { + for (const ta of toolActions) { + nodes.push({ + id: `tool-${ta.tool}`, Icon: TOOL_ICON[ta.tool] ?? Code2, + label: TOOL_LABEL[ta.tool] ?? ta.tool, + detail: toolDetail(ta), + color: 'var(--nbl-amber, #f59e0b)', + }) + } + nodes.push({ + id: 'synth', Icon: Sparkles, label: 'Synthesis', + detail: latencyMs ? `${latencyMs}ms` : undefined, + color: 'var(--nbl-nebula)', + }) + } + + nodes.push({ id: 'done', Icon: CheckCircle2, label: 'Done', color: 'var(--nbl-green)' }) + + const parts = [ + latencyMs ? `${latencyMs}ms` : null, + modelUsed ? modelUsed.replace(/^(openai\/|anthropic\/|gpt-)/, '') : null, + hasTools ? `${toolActions.length} tool${toolActions.length !== 1 ? 's' : ''}` : null, + chunksUsed ? `${chunksUsed} chunk${chunksUsed !== 1 ? 's' : ''}` : null, + ].filter(Boolean) + + if (parts.length === 0) return null + + return ( +
+ + + {open && ( +
+
+ {nodes.map((node, i) => ( + + {i > 0 && ( +
+ )} +
+
+ +
+ + {node.label} + + {node.detail && ( + + {node.detail} + + )} +
+
+ ))} +
+
+ )}
) } @@ -1033,6 +1264,14 @@ function MessageBubble({ />
)} + {msg.mode === 'nebula' && msg.role === 'agent' && ( + + )} {stepsOpen && msg.taskId && (
@@ -1531,6 +1770,7 @@ export function ChatWorkspace() { toolActions: resp.tool_actions, chunksUsed: resp.chunks_used, latencyMs: resp.latency_ms, + modelUsed: resp.model_used, createdAt: new Date().toISOString(), tryItData, }