Ai Agent Orchestrator
blockaiMulti-agent workflow visualizer and execution graph. Displays real-time agent pipeline topology, active reasoning streams, tool invocation metrics, live code artifact generation, and pipeline control actions.
Installation
$pnpm dlx uipkge-ng@latest add ai-agent-orchestrator$npx uipkge-ng@latest add ai-agent-orchestrator$yarn dlx uipkge-ng@latest add ai-agent-orchestrator$bunx uipkge-ng@latest add ai-agent-orchestratorManifest:
https://uipkge.dev/r/angular/ai-agent-orchestrator.jsonInstalls to:src/app/components/blocks/ai-agent-orchestrator/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
ThoughtStepinterface ThoughtStep {
id: string
title: string
duration: string
status: 'completed' | 'running' | 'queued'
detail: string
}ToolInvocationinterface ToolInvocation {
name: string
count: number
icon: string
}AgentNodeinterface AgentNode {
id: AgentId
stepNumber: number
name: string
role: string
iconName: string
model: string
status: NodeStatus
duration: string
task: string
tokens: string
speed: string
progress: number
tools: ToolInvocation[]
thoughts: ThoughtStep[]
artifactFileName: string
artifactSize: string
artifactCode: string
}AgentWorkflowConfiginterface AgentWorkflowConfig {
pipelineId: string
name: string
autonomousMode: boolean
maxRetries: number
timeoutMs: number
}ExecutionArtifactinterface ExecutionArtifact {
fileName: string
language: string
content: string
checksum: string
}npm dependencies
Files installed (2)
src/app/components/blocks/ai-agent-orchestrator/ai-agent-orchestrator.component.ts44.9 kB
// Edit this file to change the pipeline (`initialNodes`), the copy or the controls. // The pipeline is sample data: wire `stepForward`, `retryStep`, `rerunPipeline` and // `cancelWorkflow` to your agent runtime. import { Component, DestroyRef, Input, afterNextRender, computed, inject, signal, ChangeDetectionStrategy, } from '@angular/core' import { Activity, Bot, Check, CheckCircle2, ChevronDown, ChevronRight, Clock, Code2, Compass, Copy, FastForward, FileCode2, FileText, Globe, Layers, LucideAngularModule, Pause, Play, RefreshCw, RotateCcw, Search, ShieldCheck, Sparkles, Terminal, X, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiProgressComponent } from '@/ui/progress/progress.component' type AgentId = 'planner' | 'researcher' | 'engineer' | 'reviewer' type NodeStatus = 'completed' | 'running' | 'queued' interface ThoughtStep { id: string title: string duration: string status: 'completed' | 'running' | 'queued' detail: string } interface ToolInvocation { name: string count: number icon: string } interface AgentNode { id: AgentId stepNumber: number name: string role: string iconName: string model: string status: NodeStatus duration: string task: string tokens: string speed: string progress: number tools: ToolInvocation[] thoughts: ThoughtStep[] artifactFileName: string artifactSize: string artifactCode: string } const initialCodeSnippet = `import * as React from 'react' import { useAgentGraph } from '@/hooks/useAgentGraph' export interface AgentWorkflowConfig { pipelineId: string name: string autonomousMode: boolean maxRetries: number timeoutMs: number } export interface ExecutionArtifact { fileName: string language: string content: string checksum: string } export async function dispatchAgentWorkflow( config: AgentWorkflowConfig ): Promise<ExecutionArtifact[]> { const orchestrator = new MultiAgentEngine({ model: 'claude-3-5-sonnet', telemetry: true, }) // Stream reasoning thoughts and token outputs const stream = await orchestrator.executePipeline(config.pipelineId) return stream.collectArtifacts() }` const initialNodes: AgentNode[] = [ { id: 'planner', stepNumber: 1, name: 'Planner Agent', role: 'Task Decomposition & Schema', iconName: 'Compass', model: 'Claude 3.5', status: 'completed', duration: '12s', task: 'Deconstruct task into 4 sub-goals', tokens: '842 tokens', speed: '52 tok/s', progress: 100, tools: [ { name: 'spec_parse', count: 1, icon: 'Terminal' }, { name: 'context_index', count: 4, icon: 'FileText' }, ], thoughts: [ { id: 'p1', title: 'Analyze project manifest and user constraints', duration: '3.2s', status: 'completed', detail: 'Identified dual-framework registry requirement (Vue SFC + React TSX) with Tailwind v4 OKLCH token mapping.', }, { id: 'p2', title: 'Partition workflow into 4 autonomous agent stages', duration: '8.8s', status: 'completed', detail: 'Created topological execution graph: Planner -> Researcher -> Code Engineer -> Reviewer & QA.', }, ], artifactFileName: 'pipeline-manifest.json', artifactSize: '1.1 kB', artifactCode: `{\n "pipeline": "autonomous-research-codegen",\n "stages": 4,\n "target": "UIPKGE Block Registry",\n "strictMode": true\n}`, }, { id: 'researcher', stepNumber: 2, name: 'Research Agent', role: 'Documentation & API Contracts', iconName: 'Search', model: 'GPT-4o', status: 'completed', duration: '45s', task: 'Query documentation and verify API contracts', tokens: '1,420 tokens', speed: '64 tok/s', progress: 100, tools: [ { name: 'web_search', count: 2, icon: 'Globe' }, { name: 'doc_scrape', count: 6, icon: 'FileText' }, ], thoughts: [ { id: 'r1', title: 'Verify Reka UI and Radix UI primitive specifications', duration: '18s', status: 'completed', detail: 'Confirmed Radix Progress and Reka Progress Root/Indicator element bindings for zero-drift parity.', }, { id: 'r2', title: 'Check Lucide icon availability in Vue and React packages', duration: '27s', status: 'completed', detail: 'Validated Compass, Search, Code2, ShieldCheck, FastForward, RotateCcw, RefreshCw icons across registries.', }, ], artifactFileName: 'api-contracts.d.ts', artifactSize: '1.8 kB', artifactCode: `export interface NodeContract {\n id: string\n status: 'completed' | 'running' | 'queued'\n duration: string\n}`, }, { id: 'engineer', stepNumber: 3, name: 'Code Engineer Agent', role: 'Component Synthesis & Tests', iconName: 'Code2', model: 'Claude 3.5', status: 'running', duration: '1m 17s', task: 'Generate component architecture & unit tests', tokens: '2,840 tokens', speed: '48 tok/s', progress: 68, tools: [ { name: 'web_search', count: 2, icon: 'Globe' }, { name: 'file_read', count: 8, icon: 'FileText' }, { name: 'ast_parse', count: 1, icon: 'Terminal' }, { name: 'typecheck', count: 1, icon: 'ShieldCheck' }, ], thoughts: [ { id: 't1', title: 'Parse upstream specifications from Planner & Research Agent', duration: '14ms', status: 'completed', detail: 'Loaded 4 sub-goal definitions. Validated schema definitions for Reka UI and Radix UI primitive bindings.', }, { id: 't2', title: 'Synthesize component state machine & reactive bindings', duration: '420ms', status: 'completed', detail: 'Constructed state flow machine for pipeline progression. Hooked up active execution timers and streaming buffers.', }, { id: 't3', title: 'Construct test matrix with boundary coverage', duration: '850ms', status: 'completed', detail: 'Generated unit test cases covering step navigation, retry triggers, state pausing, and copy payload verification.', }, { id: 't4', title: 'Stream production TypeScript & TSX definitions', duration: '1m 17s', status: 'running', detail: 'Writing template layout with responsive CSS grid, glowing node state markers, tool invocation badge metrics, and dark-mode safe styling.', }, ], artifactFileName: 'AiAgentOrchestrator.tsx', artifactSize: '2.4 kB', artifactCode: initialCodeSnippet, }, { id: 'reviewer', stepNumber: 4, name: 'Reviewer & QA Agent', role: 'Typecheck & Token Audit', iconName: 'ShieldCheck', model: 'Claude 3.5', status: 'queued', duration: '0s', task: 'Validate TypeScript types and token standards', tokens: '0 tokens', speed: '0 tok/s', progress: 0, tools: [ { name: 'typecheck', count: 1, icon: 'ShieldCheck' }, { name: 'token_audit', count: 1, icon: 'Terminal' }, ], thoughts: [ { id: 'q1', title: 'Perform static type-safety and interface contract validation', duration: '0s', status: 'queued', detail: 'Awaiting Code Engineer stream completion to run full TypeScript compiler validation without any types.', }, { id: 'q2', title: 'Verify minimum 12px font scale and OKLCH color token conformance', duration: '0s', status: 'queued', detail: 'Scanning AST for forbidden sub-12px micro-classes and hardcoded hex color values.', }, ], artifactFileName: 'qa-audit-report.md', artifactSize: '0.9 kB', artifactCode: `# QA Verification Gate\n- [ ] TypeScript parity check\n- [ ] Design token conformance\n- [ ] Accessibility keyboard focus`, }, ] const NODE_BASE = 'group focus-visible:ring-ring relative flex w-full cursor-pointer flex-col justify-between rounded-xl border p-4 text-left transition-[border-color,box-shadow] duration-200 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:outline-none' /** Per-node class variations, exactly as the React block writes them for each of its four node buttons. */ const NODE_STYLES: Record< AgentId, { selected: string; idle: string; running: string; queued: string; iconRunning: string; iconQueued: string } > = { planner: { selected: 'border-primary ring-primary/30 shadow-xs ring-2', idle: 'border-border hover:border-muted-foreground/40', running: 'border-primary/60 bg-primary/[0.03]', queued: 'bg-muted/20 opacity-75', iconRunning: 'border-primary/30 bg-primary/10 text-primary', iconQueued: 'border-border bg-muted text-muted-foreground', }, researcher: { selected: 'border-primary ring-primary/30 shadow-xs ring-2', idle: 'border-border hover:border-muted-foreground/40', running: 'border-primary/60 bg-primary/[0.03]', queued: 'bg-muted/20 opacity-75', iconRunning: 'border-primary/30 bg-primary/10 text-primary', iconQueued: 'border-border bg-muted text-muted-foreground', }, engineer: { selected: 'border-primary ring-primary/40 shadow-xs ring-2', idle: 'border-border hover:border-muted-foreground/40', running: 'border-primary/60 bg-primary/[0.04]', queued: 'bg-muted/20 opacity-75', iconRunning: 'border-primary/40 bg-primary/15 text-primary', iconQueued: 'border-border bg-muted text-muted-foreground', }, reviewer: { selected: 'border-primary ring-primary/30 shadow-xs ring-2', idle: 'border-border/80 hover:border-muted-foreground/40', running: 'border-primary/60 bg-primary/[0.03]', queued: 'bg-muted/15 border-dashed', iconRunning: 'border-primary/30 bg-primary/10 text-primary', iconQueued: 'border-border bg-muted/60 text-muted-foreground', }, } const AGENT_ICONS: Record<AgentId, LucideIconData> = { planner: Compass, researcher: Search, engineer: Code2, reviewer: ShieldCheck, } /** * AiAgentOrchestrator -- the React block, 1:1: a multi-agent pipeline dashboard with a run * header (status, live timer, pause / cancel), a four-node execution graph, and a detail * panel for the selected node (progress, telemetry, thought stream, tool invocations, the * generated artifact) plus pipeline controls (step forward, retry, re-run, pause). * * Icon notes (lucide-angular wraps each <svg> in a <lucide-icon> host, so the Badge's * `[&>svg]:size-3` and the Button's `has-[>svg]:px-*` never see it): icons inside badges are * written at the size-3 React renders them at, and icon buttons carry the `px-*` React's * `has-[>svg]` resolves to. Icons with a margin use `[class]` so the margin lands on the svg only. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-ai-agent-orchestrator, [ui-ai-agent-orchestrator]', standalone: true, host: { '[attr.data-slot]': '"ai-agent-orchestrator"', '[class]': 'hostClass', }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiProgressComponent, ], template: ` <!-- Top Header Bar --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-content class="flex flex-col gap-4 p-5 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-start gap-3.5 sm:items-center"> <div class="bg-primary/10 text-primary border-primary/20 flex size-11 shrink-0 items-center justify-center rounded-xl border" > <lucide-icon [img]="Bot" class="size-5" /> </div> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <h2 class="text-foreground text-lg font-semibold tracking-tight sm:text-xl"> Autonomous Research & Code Generation Squad </h2> </div> <div class="text-muted-foreground flex flex-wrap items-center gap-2 text-xs"> <span class="font-mono">Pipeline ID: pipe-892f4c</span> <span>•</span> <span>4 Agents Orchestrated</span> <span>•</span> <span>Target: UIPKGE Registry Block</span> </div> </div> </div> <div class="flex flex-wrap items-center gap-2.5"> <!-- Status Badge --> @if (!isPaused() && activeStepIndex() < 4) { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary shadow-xs"> <span class="relative mr-1.5 flex size-2"> <span class="bg-primary absolute inline-flex h-full w-full rounded-full opacity-75"></span> <span class="bg-primary relative inline-flex size-2 rounded-full"></span> </span> Running • Step {{ stepLabel() }} of 4 </span> } @else if (isPaused()) { <span ui-badge variant="warning" class="shadow-xs"> <lucide-icon [img]="Pause" [class]="'mr-1 size-3'" /> Paused • Step {{ stepLabel() }} of 4 </span> } @else { <span ui-badge variant="success" class="shadow-xs"> <lucide-icon [img]="CheckCircle2" [class]="'mr-1 size-3'" /> Completed • 4 of 4 Steps </span> } <!-- Timer --> <div class="border-border bg-muted/40 text-foreground flex items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs font-medium" > <lucide-icon [img]="Clock" class="text-muted-foreground size-3.5" /> <span>{{ formattedTime() }}</span> </div> <!-- Controls --> <button ui-button variant="outline" size="sm" class="h-8 gap-1.5 px-2.5 text-xs" (click)="togglePause()"> <lucide-icon [img]="isPaused() ? Play : Pause" class="size-3.5" /> <span>{{ isPaused() ? 'Resume Workflow' : 'Pause Workflow' }}</span> </button> <button ui-button variant="ghost" size="sm" class="text-muted-foreground hover:text-foreground h-8 gap-1 px-2.5 text-xs" (click)="cancelWorkflow()" > <lucide-icon [img]="X" class="size-3.5" /> <span>Cancel</span> </button> </div> </div> </div> <!-- Agent Flow Pipeline Nodes (Horizontal Connected Flow) --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> <div> <h3 ui-card-title class="text-base font-semibold">Agent Execution Graph</h3> <p ui-card-description class="text-xs"> Real-time multi-agent execution pipeline. Select any node to inspect telemetry. </p> </div> <div class="flex flex-wrap items-center gap-2"> <span class="text-muted-foreground text-xs font-medium">Pipeline Progress</span> <div class="bg-muted h-2 w-28 overflow-hidden rounded-full"> <div class="bg-primary h-full transition-[width] duration-500 ease-out" [style.width.%]="overallProgress()" ></div> </div> <span class="font-mono text-xs font-semibold">{{ overallProgress() }}%</span> </div> </div> </div> <div ui-card-content class="p-6 pt-2"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4"> <!-- Node 1: Planner Agent --> <button type="button" [class]="nodeClass('planner')" [attr.aria-pressed]="selectedAgentId() === 'planner'" (click)="selectAgent('planner')" > <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex flex-wrap items-center gap-2.5"> <div [class]="nodeIconClass('planner')"> <lucide-icon [img]="Compass" class="size-4" /> </div> <div> <p class="text-foreground text-xs leading-none font-semibold">Planner Agent</p> <p class="text-muted-foreground mt-0.5 text-xs">Stage 01</p> </div> </div> @if (statusOf('planner') === 'completed') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> 12s </span> } @else if (statusOf('planner') === 'running') { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary text-xs"> Running </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground text-xs">Queued</span> } </div> <div class="space-y-1"> <p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed"> Deconstruct task into 4 sub-goals </p> </div> </div> <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2.5 text-xs"> <span class="text-muted-foreground font-mono">Claude 3.5</span> <span class="text-foreground font-medium">100% done</span> </div> </button> <!-- Node 2: Research Agent --> <button type="button" [class]="nodeClass('researcher')" [attr.aria-pressed]="selectedAgentId() === 'researcher'" (click)="selectAgent('researcher')" > <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex flex-wrap items-center gap-2.5"> <div [class]="nodeIconClass('researcher')"> <lucide-icon [img]="Search" class="size-4" /> </div> <div> <p class="text-foreground text-xs leading-none font-semibold">Research Agent</p> <p class="text-muted-foreground mt-0.5 text-xs">Stage 02</p> </div> </div> @if (statusOf('researcher') === 'completed') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> 45s </span> } @else if (statusOf('researcher') === 'running') { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary text-xs"> Running </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground text-xs">Queued</span> } </div> <div class="space-y-1"> <p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed"> Query documentation and verify API contracts </p> </div> </div> <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2.5 text-xs"> <span class="text-muted-foreground font-mono">GPT-4o</span> <span class="text-foreground font-medium">100% done</span> </div> </button> <!-- Node 3: Code Engineer Agent --> <button type="button" [class]="nodeClass('engineer')" [attr.aria-pressed]="selectedAgentId() === 'engineer'" (click)="selectAgent('engineer')" > <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex flex-wrap items-center gap-2.5"> <div [class]="nodeIconClass('engineer')"> <lucide-icon [img]="Code2" class="size-4" /> </div> <div> <p class="text-foreground text-xs leading-none font-semibold">Code Engineer</p> <p class="text-muted-foreground mt-0.5 text-xs">Stage 03</p> </div> </div> @if (statusOf('engineer') === 'completed') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> 1m 17s </span> } @else if (statusOf('engineer') === 'running') { <span ui-badge variant="outline" class="border-primary/50 bg-primary/15 text-primary text-xs font-medium" > <span class="relative mr-1 flex size-1.5"> <span class="bg-primary absolute inline-flex h-full w-full rounded-full opacity-75"></span> <span class="bg-primary relative inline-flex size-1.5 rounded-full"></span> </span> Streaming </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground text-xs">Queued</span> } </div> <div class="space-y-1"> <p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed"> Generate component architecture & unit tests </p> </div> </div> <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2.5 text-xs"> <span class="text-muted-foreground font-mono">Claude 3.5</span> <span class="text-primary font-medium">{{ progressOf('engineer') }}% active</span> </div> </button> <!-- Node 4: Reviewer & QA Agent --> <button type="button" [class]="nodeClass('reviewer')" [attr.aria-pressed]="selectedAgentId() === 'reviewer'" (click)="selectAgent('reviewer')" > <div class="space-y-3"> <div class="flex items-center justify-between"> <div class="flex flex-wrap items-center gap-2.5"> <div [class]="nodeIconClass('reviewer')"> <lucide-icon [img]="ShieldCheck" class="size-4" /> </div> <div> <p class="text-foreground text-xs leading-none font-semibold">Reviewer & QA</p> <p class="text-muted-foreground mt-0.5 text-xs">Stage 04</p> </div> </div> @if (statusOf('reviewer') === 'completed') { <span ui-badge variant="success" class="gap-1 text-xs"> <lucide-icon [img]="Check" class="size-3" /> Passed </span> } @else if (statusOf('reviewer') === 'running') { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary text-xs"> Running </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground border-border/80 text-xs">Queued</span> } </div> <div class="space-y-1"> <p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed"> Validate TypeScript types and token standards </p> </div> </div> <div class="border-border/60 mt-3 flex items-center justify-between border-t pt-2.5 text-xs"> <span class="text-muted-foreground font-mono">Claude 3.5</span> <span class="text-muted-foreground font-medium">Pending</span> </div> </button> </div> </div> </div> <!-- Active Execution Detail Panel (2-Column Bento) --> <div class="grid grid-cols-1 gap-6 lg:grid-cols-12"> <!-- Left Column: Agent Overview, Thought Stream & Tool Invocations --> <div class="space-y-6 lg:col-span-7"> <!-- Active Agent Header & Progress --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-4"> <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-center gap-3"> <div class="bg-primary/10 text-primary border-primary/20 flex size-10 items-center justify-center rounded-lg border" > <lucide-icon [img]="agentIcon()" class="size-5" /> </div> <div> <div class="flex flex-wrap items-center gap-2"> <h3 ui-card-title class="text-base font-semibold">{{ selectedAgent().name }}</h3> <span ui-badge variant="outline" class="font-mono text-xs">{{ selectedAgent().model }}</span> </div> <p ui-card-description class="text-xs">{{ selectedAgent().task }}</p> </div> </div> <div class="flex flex-wrap items-center gap-2"> @if (selectedAgent().status === 'completed') { <span ui-badge variant="success" class="text-xs">Completed in {{ selectedAgent().duration }}</span> } @else if (selectedAgent().status === 'running') { <span ui-badge variant="outline" class="border-primary/40 bg-primary/10 text-primary text-xs"> <lucide-icon [img]="Activity" [class]="'mr-1 size-3 animate-pulse'" /> Streaming • {{ selectedAgent().duration }} </span> } @else { <span ui-badge variant="outline" class="text-muted-foreground text-xs">Queued</span> } </div> </div> </div> <div ui-card-content class="space-y-4 pt-0"> <div class="space-y-1.5"> <div class="text-muted-foreground flex justify-between text-xs font-medium"> <span>Task Execution State</span> <span class="font-mono">{{ selectedAgent().progress }}%</span> </div> <ui-progress [value]="selectedAgent().progress" class="h-2" /> </div> <div class="bg-muted/30 border-border grid grid-cols-2 gap-3 rounded-lg border p-3 sm:grid-cols-4"> <div> <p class="text-muted-foreground text-xs">Duration</p> <p class="text-foreground font-mono text-xs font-semibold">{{ selectedAgent().duration }}</p> </div> <div> <p class="text-muted-foreground text-xs">Tokens Output</p> <p class="text-foreground font-mono text-xs font-semibold">{{ selectedAgent().tokens }}</p> </div> <div> <p class="text-muted-foreground text-xs">Throughput</p> <p class="text-foreground font-mono text-xs font-semibold">{{ selectedAgent().speed }}</p> </div> <div> <p class="text-muted-foreground text-xs">Agent Role</p> <p class="text-foreground truncate text-xs font-semibold">{{ selectedAgent().role }}</p> </div> </div> </div> </div> <!-- Agent Thought Stream Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-3"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Sparkles" class="text-primary size-4" /> <h3 ui-card-title class="text-sm font-semibold">Agent Thought Stream & Reasoning</h3> </div> <button ui-button variant="ghost" size="xs" class="text-muted-foreground hover:text-foreground h-7 text-xs" (click)="toggleAllThoughts()" > Toggle Details </button> </div> <div ui-card-content class="space-y-3 pt-0"> @for (thought of selectedAgent().thoughts; track thought.id) { <div class="border-border bg-muted/20 hover:bg-muted/30 rounded-lg border transition-colors"> <button type="button" class="focus-visible:ring-ring flex w-full cursor-pointer items-center justify-between rounded-lg p-3 text-left focus-visible:ring-2 focus-visible:outline-none" [attr.aria-expanded]="expandedAttr(thought.id)" (click)="toggleThought(thought.id)" > <div class="flex flex-wrap items-center gap-2.5"> <div [class]="thoughtIconClass(thought.status)"> @if (thought.status === 'completed') { <lucide-icon [img]="CheckCircle2" class="size-4" /> } @else if (thought.status === 'running') { <lucide-icon [img]="Activity" class="size-4" /> } @else { <div class="border-muted-foreground size-3 rounded-full border border-dashed"></div> } </div> <p class="text-foreground text-xs font-medium">{{ thought.title }}</p> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="outline" class="font-mono text-xs">{{ thought.duration }}</span> <lucide-icon [img]="isExpanded(thought.id) ? ChevronDown : ChevronRight" class="text-muted-foreground size-3.5" /> </div> </button> @if (isExpanded(thought.id)) { <div class="border-border/60 bg-muted/40 text-muted-foreground border-t px-3 py-2.5 text-xs leading-relaxed" > {{ thought.detail }} </div> } </div> } </div> </div> <!-- Tool Invocations Badge Row & Logs --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Terminal" class="text-primary size-4" /> <h3 ui-card-title class="text-sm font-semibold">Active Tool Invocations</h3> </div> </div> <div ui-card-content class="space-y-3 pt-0"> <!-- Badge Row --> <div class="flex flex-wrap items-center gap-2"> @for (tool of selectedAgent().tools; track tool.name) { <span ui-badge variant="outline" class="border-border bg-muted/40 hover:bg-muted text-foreground gap-1.5 px-2.5 py-1 text-xs font-medium" > <!-- size-3: React writes size-3.5, but the Badge's [&>svg]:size-3 wins there. --> <lucide-icon [img]="toolIcon(tool.icon)" class="text-muted-foreground size-3" /> <span class="font-mono">{{ tool.name }}</span> <span class="bg-primary/10 text-primary py-0.2 rounded-full px-1.5 text-xs font-bold"> {{ tool.count }} </span> </span> } </div> <!-- Execution Log Terminal Row --> <div class="border-border bg-muted/50 overflow-x-auto rounded-lg border p-3 font-mono text-xs"> <div class="text-muted-foreground space-y-1"> <div class="flex flex-wrap items-center gap-2 whitespace-nowrap"> <span class="text-success font-semibold">[02:14:02]</span> <span>GET https://uipkge.dev/r/button.json → 200 OK (38ms)</span> </div> <div class="flex flex-wrap items-center gap-2 whitespace-nowrap"> <span class="text-success font-semibold">[02:14:05]</span> <span>READ packages/registry-react/components/card/card.tsx (12ms)</span> </div> <div class="flex flex-wrap items-center gap-2"> <span class="text-primary font-semibold">[02:14:09]</span> <span>AST parse: 4 exports identified, 0 cycle violations</span> </div> <div class="flex flex-wrap items-center gap-2 whitespace-nowrap"> <span class="text-success font-semibold">[02:14:14]</span> <span>Typecheck: 0 errors across 6 test suites</span> </div> </div> </div> </div> </div> </div> <!-- Right Column: Live Artifact Output & Pipeline Actions --> <div class="space-y-6 lg:col-span-5"> <!-- Live Artifact Output Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="flex flex-row items-center justify-between pb-3"> <div class="flex flex-wrap items-center gap-2 overflow-hidden"> <lucide-icon [img]="FileCode2" class="text-primary size-4 shrink-0" /> <h3 ui-card-title class="truncate font-mono text-xs font-medium"> {{ selectedAgent().artifactFileName }} </h3> </div> <div class="flex flex-wrap items-center gap-2"> <span ui-badge variant="secondary" class="font-mono text-xs">{{ selectedAgent().artifactSize }}</span> <button ui-button variant="outline" size="xs" class="h-7 gap-1 px-1.5 text-xs" (click)="copyArtifact()"> <lucide-icon [img]="copied() ? Check : Copy" class="size-3" /> <span>{{ copied() ? 'Copied!' : 'Copy' }}</span> </button> </div> </div> <div ui-card-content class="pt-0"> <div class="border-border bg-muted/40 relative overflow-hidden rounded-lg border font-mono text-xs"> <!-- Window top bar --> <div class="border-border/60 bg-muted/60 flex items-center justify-between border-b px-3 py-1.5"> <div class="flex items-center gap-1.5"> <div class="bg-destructive/60 size-2.5 rounded-full"></div> <div class="bg-warning/60 size-2.5 rounded-full"></div> <div class="bg-success/60 size-2.5 rounded-full"></div> </div> <span class="text-muted-foreground text-xs">Live Generated Output</span> </div> <!-- Code area with syntax styling --> <div class="max-h-[380px] overflow-auto p-3 text-xs leading-relaxed"> <pre class="text-foreground font-mono whitespace-pre-wrap" ><code>{{ selectedAgent().artifactCode }}</code></pre> @if (selectedAgent().status === 'running') { <span class="bg-primary ml-0.5 inline-block h-3.5 w-1.5 animate-pulse align-middle"></span> } </div> </div> </div> </div> <!-- Pipeline Action Controls --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center gap-2"> <lucide-icon [img]="Layers" class="text-primary size-4" /> <h3 ui-card-title class="text-sm font-semibold">Pipeline Orchestrator Controls</h3> </div> </div> <div ui-card-content class="space-y-3 pt-0"> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> <button ui-button variant="default" size="sm" class="gap-1.5 px-2.5 text-xs font-medium" [disabled]="activeStepIndex() >= 4" (click)="stepForward()" > <lucide-icon [img]="FastForward" class="size-3.5" /> <span>Step Forward</span> </button> <button ui-button variant="outline" size="sm" class="gap-1.5 px-2.5 text-xs font-medium" (click)="retryStep()" > <lucide-icon [img]="RotateCcw" class="size-3.5" /> <span>Retry Step</span> </button> <button ui-button variant="outline" size="sm" class="gap-1.5 px-2.5 text-xs font-medium" (click)="rerunPipeline()" > <lucide-icon [img]="RefreshCw" class="size-3.5" /> <span>Re-run Pipeline</span> </button> <button ui-button variant="outline" size="sm" class="gap-1.5 px-2.5 text-xs font-medium" (click)="togglePause()" > <lucide-icon [img]="isPaused() ? Play : Pause" class="size-3.5" /> <span>{{ isPaused() ? 'Resume' : 'Pause' }}</span> </button> </div> <p class="text-muted-foreground pt-1 text-center text-xs"> Actions dispatch commands across connected autonomous agent nodes. </p> </div> </div> </div> </div> `, }) export class UiAiAgentOrchestratorComponent { @Input('class') className?: string readonly isPaused = signal(false) readonly elapsedSeconds = signal(134) readonly selectedAgentId = signal<AgentId>('engineer') readonly activeStepIndex = signal(2) readonly copied = signal(false) readonly nodes = signal<AgentNode[]>(initialNodes) readonly expandedThoughts = signal<Record<string, boolean>>({ t1: false, t2: false, t3: false, t4: true, }) readonly selectedAgent = computed(() => { const nodes = this.nodes() return nodes.find((n) => n.id === this.selectedAgentId()) ?? nodes[2]! }) readonly formattedTime = computed(() => { const elapsed = this.elapsedSeconds() const mins = Math.floor(elapsed / 60) const secs = elapsed % 60 return `${mins.toString().padStart(2, '0')}m:${secs.toString().padStart(2, '0')}s` }) readonly overallProgress = computed(() => { const nodes = this.nodes() const completedCount = nodes.filter((n) => n.status === 'completed').length const running = nodes.find((n) => n.status === 'running') const runningBonus = running ? running.progress / 100 : 0 return Math.round(((completedCount + runningBonus) / nodes.length) * 100) }) readonly stepLabel = computed(() => Math.min(this.activeStepIndex() + 1, 4)) readonly agentIcon = computed(() => AGENT_ICONS[this.selectedAgent().id]) protected readonly Activity = Activity protected readonly Bot = Bot protected readonly Check = Check protected readonly CheckCircle2 = CheckCircle2 protected readonly ChevronDown = ChevronDown protected readonly ChevronRight = ChevronRight protected readonly Clock = Clock protected readonly Code2 = Code2 protected readonly Compass = Compass protected readonly Copy = Copy protected readonly FastForward = FastForward protected readonly FileCode2 = FileCode2 protected readonly Layers = Layers protected readonly Pause = Pause protected readonly Play = Play protected readonly RefreshCw = RefreshCw protected readonly RotateCcw = RotateCcw protected readonly Search = Search protected readonly ShieldCheck = ShieldCheck protected readonly Sparkles = Sparkles protected readonly Terminal = Terminal protected readonly X = X private timer: ReturnType<typeof setInterval> | undefined private copyTimer: ReturnType<typeof setTimeout> | undefined constructor() { // Browser only, after the first render (React's useEffect). afterNextRender(() => { this.timer = setInterval(() => { if (!this.isPaused() && this.activeStepIndex() < 4) { this.elapsedSeconds.update((prev) => prev + 1) } }, 1000) }) inject(DestroyRef).onDestroy(() => { clearInterval(this.timer) clearTimeout(this.copyTimer) }) } get hostClass(): string { return cn('block w-full space-y-6', this.className) } protected statusOf(id: AgentId): NodeStatus { return this.nodes().find((n) => n.id === id)?.status ?? 'queued' } protected progressOf(id: AgentId): number { return this.nodes().find((n) => n.id === id)?.progress ?? 0 } protected nodeClass(id: AgentId): string { const style = NODE_STYLES[id] const status = this.statusOf(id) return cn( NODE_BASE, this.selectedAgentId() === id ? style.selected : style.idle, status === 'completed' ? 'bg-card' : status === 'running' ? style.running : style.queued, ) } protected nodeIconClass(id: AgentId): string { const style = NODE_STYLES[id] const status = this.statusOf(id) return cn( 'flex size-9 items-center justify-center rounded-lg border text-xs', status === 'completed' ? 'border-success/30 bg-success/10 text-success' : status === 'running' ? style.iconRunning : style.iconQueued, ) } protected thoughtIconClass(status: ThoughtStep['status']): string { return cn( 'flex size-5 shrink-0 items-center justify-center rounded-full', status === 'completed' ? 'text-success' : status === 'running' ? 'text-primary animate-spin' : 'text-muted-foreground', ) } protected toolIcon(icon: string): LucideIconData { switch (icon) { case 'Globe': return Globe case 'FileText': return FileText case 'Terminal': return Terminal default: return ShieldCheck } } protected isExpanded(id: string): boolean { return this.expandedThoughts()[id] === true } /** React's `aria-expanded={expandedThoughts[id]}`: omitted for a thought that was never toggled. */ protected expandedAttr(id: string): boolean | null { return this.expandedThoughts()[id] ?? null } togglePause(): void { this.isPaused.update((prev) => !prev) } selectAgent(id: AgentId): void { this.selectedAgentId.set(id) } toggleThought(id: string): void { this.expandedThoughts.update((prev) => ({ ...prev, [id]: !prev[id] })) } toggleAllThoughts(): void { const expanded = this.expandedThoughts() const thoughts = this.selectedAgent().thoughts const anyClosed = thoughts.some((t) => !expanded[t.id]) const next: Record<string, boolean> = { ...expanded } thoughts.forEach((t) => { next[t.id] = anyClosed }) this.expandedThoughts.set(next) } stepForward(): void { const active = this.activeStepIndex() const nodes = this.nodes() if (active < nodes.length - 1) { const nextIdx = active + 1 this.nodes.update((prev) => prev.map((node, idx) => idx === active ? { ...node, status: 'completed', progress: 100 } : idx === nextIdx ? { ...node, status: 'running', progress: 45 } : node, ), ) this.activeStepIndex.set(nextIdx) this.selectedAgentId.set(nodes[nextIdx]!.id) } else if (active === nodes.length - 1) { this.nodes.update((prev) => prev.map((node, idx) => (idx === active ? { ...node, status: 'completed', progress: 100 } : node)), ) this.activeStepIndex.set(4) } } retryStep(): void { const active = this.activeStepIndex() const nodes = this.nodes() if (active < nodes.length) { this.nodes.update((prev) => prev.map((node, idx) => (idx === active ? { ...node, status: 'running', progress: 10 } : node)), ) this.selectedAgentId.set(nodes[active]!.id) } } rerunPipeline(): void { this.elapsedSeconds.set(0) this.isPaused.set(false) this.activeStepIndex.set(0) this.nodes.update((prev) => prev.map((node, idx) => ({ ...node, status: idx === 0 ? 'running' : 'queued', progress: idx === 0 ? 25 : 0, })), ) this.selectedAgentId.set('planner') } cancelWorkflow(): void { this.isPaused.set(true) } async copyArtifact(): Promise<void> { try { await navigator.clipboard.writeText(this.selectedAgent().artifactCode) this.copied.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => { this.copied.set(false) }, 2000) } catch { // Clipboard fallback } } }src/app/components/blocks/ai-agent-orchestrator/index.ts0.1 kB
export { UiAiAgentOrchestratorComponent } from './ai-agent-orchestrator.component'
Raw manifest:https://uipkge.dev/r/angular/ai-agent-orchestrator.json