Menu

Change language

GitHub repo

Ai Agent Orchestrator

blockai

Multi-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

$npx uipkge-ng@latest add ai-agent-orchestrator
Manifest:https://uipkge.dev/r/angular/ai-agent-orchestrator.jsonInstalls to:src/app/components/blocks/ai-agent-orchestrator/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

ThoughtStep
interface ThoughtStep {
  id: string
  title: string
  duration: string
  status: 'completed' | 'running' | 'queued'
  detail: string
}
ToolInvocation
interface ToolInvocation {
  name: string
  count: number
  icon: string
}
AgentNode
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
}
AgentWorkflowConfig
interface AgentWorkflowConfig {
  pipelineId: string
  name: string
  autonomousMode: boolean
  maxRetries: number
  timeoutMs: number
}
ExecutionArtifact
interface ExecutionArtifact {
  fileName: string
  language: string
  content: string
  checksum: string
}

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 &amp; 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 &amp; 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 &amp; 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 &amp; 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

Raw manifest:https://uipkge.dev/r/angular/ai-agent-orchestrator.json