'use client'; import React from 'react'; import { CheckCircle2, Loader2, Sparkles, Square } from 'lucide-react'; import type { ProcessArtifact, ProcessEvent, ProcessRun } from '@/types'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { appArtifactPreview, appFeedRoleLabel, appStatusLabel } from '@/lib/i18n/common'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { cn } from '@/lib/utils'; type RunCardPhase = 'live' | 'exiting' | 'collapsed'; type AgentFeedItem = { key: string; created_at: string; role: 'user' | 'assistant' | 'system' | 'tool'; text: string; tone?: ProcessRun['status']; }; const TERMINAL_STATUSES = new Set(['done', 'error', 'cancelled']); const AGENT_ACCENTS = [ { frame: 'border-sky-500/25 bg-sky-500/[0.05]', title: 'text-sky-300', dot: 'bg-sky-400', result: 'border-sky-500/25 bg-sky-500/[0.08]', }, { frame: 'border-emerald-500/25 bg-emerald-500/[0.05]', title: 'text-emerald-300', dot: 'bg-emerald-400', result: 'border-emerald-500/25 bg-emerald-500/[0.08]', }, { frame: 'border-amber-500/25 bg-amber-500/[0.05]', title: 'text-amber-300', dot: 'bg-amber-400', result: 'border-amber-500/25 bg-amber-500/[0.08]', }, { frame: 'border-fuchsia-500/25 bg-fuchsia-500/[0.05]', title: 'text-fuchsia-300', dot: 'bg-fuchsia-400', result: 'border-fuchsia-500/25 bg-fuchsia-500/[0.08]', }, ] as const; function accentFor(index: number) { return AGENT_ACCENTS[index % AGENT_ACCENTS.length]; } function statusTone(status: ProcessRun['status']) { if (status === 'done') return 'border-emerald-500/20 bg-emerald-500/10 text-emerald-300'; if (status === 'error') return 'border-rose-500/20 bg-rose-500/10 text-rose-300'; if (status === 'cancelled') return 'border-zinc-500/20 bg-zinc-500/10 text-zinc-300'; if (status === 'waiting') return 'border-amber-500/20 bg-amber-500/10 text-amber-300'; if (status === 'queued') return 'border-sky-500/20 bg-sky-500/10 text-sky-300'; return 'border-sky-500/20 bg-sky-500/10 text-sky-300'; } function feedTone(role: AgentFeedItem['role']) { if (role === 'user') { return 'ml-6 border-border/70 bg-muted/60 text-foreground'; } if (role === 'system') { return 'mx-4 border-border/60 bg-accent/60 text-foreground/85'; } if (role === 'tool') { return 'mr-6 border-border/70 bg-background/80 text-foreground'; } return 'mr-6 border-border/70 bg-background/80 text-foreground'; } function delegatedTask(run: ProcessRun): string | null { const value = run.metadata?.delegated_task; return typeof value === 'string' && value.trim() ? value.trim() : null; } function buildFeed( run: ProcessRun, events: ProcessEvent[], artifacts: ProcessArtifact[], locale: 'zh-CN' | 'en-US', ): AgentFeedItem[] { const items: AgentFeedItem[] = []; let hasLeadBubble = false; for (const event of events) { if (!event.text?.trim()) { continue; } if (event.kind === 'run_message') { const role = event.message_role || 'assistant'; if (role === 'user') { hasLeadBubble = true; } items.push({ key: event.event_id, created_at: event.created_at, role, text: event.text.trim(), }); continue; } if (event.kind === 'run_progress') { items.push({ key: event.event_id, created_at: event.created_at, role: 'assistant', text: event.text.trim(), }); continue; } if (event.kind === 'run_status' && event.status && event.status !== 'running') { items.push({ key: event.event_id, created_at: event.created_at, role: 'system', text: event.text.trim(), tone: event.status, }); } } for (const artifact of artifacts) { items.push({ key: artifact.artifact_id, created_at: artifact.created_at, role: artifact.actor_type === 'mcp' ? 'tool' : 'assistant', text: appArtifactPreview(artifact, locale), }); } if (!hasLeadBubble) { const task = delegatedTask(run); if (task) { items.push({ key: `${run.run_id}:delegated-task`, created_at: run.started_at, role: 'user', text: task, }); } } return items .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()) .slice(-8); } function runSummary(run: ProcessRun, feed: AgentFeedItem[], locale: 'zh-CN' | 'en-US'): string { if (run.summary?.trim()) { return run.summary.trim(); } const latestAssistant = [...feed].reverse().find((item) => item.role === 'assistant' || item.role === 'tool'); return latestAssistant?.text || pickAppText(locale, '已完成子任务处理', 'Subtask processing completed'); } function useRunCardPhases(runs: ProcessRun[]) { const [phases, setPhases] = React.useState>(() => Object.fromEntries( runs.map((run) => [run.run_id, TERMINAL_STATUSES.has(run.status) ? 'collapsed' : 'live']) ) ); const timersRef = React.useRef>>({}); React.useEffect(() => { setPhases((prev) => { const next = { ...prev }; const seen = new Set(); for (const run of runs) { seen.add(run.run_id); const isTerminal = TERMINAL_STATUSES.has(run.status); const current = next[run.run_id]; if (!current) { next[run.run_id] = isTerminal ? 'collapsed' : 'live'; continue; } if (!isTerminal) { next[run.run_id] = 'live'; if (timersRef.current[run.run_id]) { clearTimeout(timersRef.current[run.run_id]); delete timersRef.current[run.run_id]; } continue; } if (current === 'live') { next[run.run_id] = 'exiting'; timersRef.current[run.run_id] = setTimeout(() => { setPhases((snapshot) => { if (snapshot[run.run_id] !== 'exiting') { return snapshot; } return { ...snapshot, [run.run_id]: 'collapsed' }; }); delete timersRef.current[run.run_id]; }, 420); } } for (const runId of Object.keys(next)) { if (!seen.has(runId)) { if (timersRef.current[runId]) { clearTimeout(timersRef.current[runId]); delete timersRef.current[runId]; } delete next[runId]; } } return next; }); return () => { for (const timer of Object.values(timersRef.current)) { clearTimeout(timer); } timersRef.current = {}; }; }, [runs]); return phases; } function AgentBubble({ item, locale, }: { item: AgentFeedItem; locale: 'zh-CN' | 'en-US'; }) { return (
{appFeedRoleLabel(item.role, locale)}
{item.text}
); } function LiveAgentCard({ run, feed, artifactCount, selected, phase, accentIndex, onSelect, locale, }: { run: ProcessRun; feed: AgentFeedItem[]; artifactCount: number; selected: boolean; phase: RunCardPhase; accentIndex: number; onSelect: () => void; locale: 'zh-CN' | 'en-US'; }) { const showSpinner = !TERMINAL_STATUSES.has(run.status); const accent = accentFor(accentIndex); return ( ); } function ResultCard({ run, summary, artifactCount, selected, accentIndex, onSelect, locale, }: { run: ProcessRun; summary: string; artifactCount: number; selected: boolean; accentIndex: number; onSelect: () => void; locale: 'zh-CN' | 'en-US'; }) { const accent = accentFor(accentIndex); return ( ); } export function AgentTeamBlock({ rootRun, memberRuns, events, artifacts, selectedRunId, onSelectRun, onCancelRun, }: { rootRun: ProcessRun; memberRuns: ProcessRun[]; events: ProcessEvent[]; artifacts: ProcessArtifact[]; selectedRunId: string | null; onSelectRun: (runId: string) => void; onCancelRun: (runId: string) => void; }) { const { locale } = useAppI18n(); const phases = useRunCardPhases(memberRuns); const sortedRuns = React.useMemo( () => [...memberRuns].sort((a, b) => { const at = new Date(a.started_at).getTime(); const bt = new Date(b.started_at).getTime(); return at - bt; }), [memberRuns] ); const liveRuns = sortedRuns.filter((run) => phases[run.run_id] === 'live'); const terminalRuns = sortedRuns.filter((run) => TERMINAL_STATUSES.has(run.status)); const collapsedRuns = sortedRuns.filter((run) => phases[run.run_id] === 'collapsed'); const liveCount = liveRuns.filter((run) => !TERMINAL_STATUSES.has(run.status)).length; const canCancelRoot = !rootRun.parent_run_id && (rootRun.status === 'running' || rootRun.status === 'waiting'); if (liveRuns.length === 0 && terminalRuns.length > 0) { return (
{pickAppText(locale, '智能体结果', 'Agent results')}
{rootRun.title}
{terminalRuns.map((run, index) => { const runEvents = events.filter((event) => event.run_id === run.run_id); const runArtifacts = artifacts.filter((artifact) => artifact.run_id === run.run_id); const feed = buildFeed(run, runEvents, runArtifacts, locale); return ( onSelectRun(run.run_id)} locale={locale} /> ); })}
); } return (
{pickAppText(locale, '智能体团队', 'Agent team')}
{rootRun.title}

{liveCount > 0 ? pickAppText(locale, `主 agent 正在协调 ${liveCount} 个运行中的 sub-agent`, `Lead agent is coordinating ${liveCount} running sub-agents`) : pickAppText(locale, '子 agent 已完成,结果已折叠为摘要卡片', 'Sub-agents are done. Results are folded into summary cards')}

{canCancelRoot && ( )} {pickAppText(locale, `${memberRuns.length} 个 sub-agent`, `${memberRuns.length} sub-agents`)} {appStatusLabel(rootRun.status, locale)}
{liveRuns.length > 0 && (
{liveRuns.map((run, index) => { const runEvents = events.filter((event) => event.run_id === run.run_id); const runArtifacts = artifacts.filter((artifact) => artifact.run_id === run.run_id); const feed = buildFeed(run, runEvents, runArtifacts, locale); return ( onSelectRun(run.run_id)} locale={locale} /> ); })}
)} {collapsedRuns.length > 0 && (
{collapsedRuns.map((run, index) => { const runEvents = events.filter((event) => event.run_id === run.run_id); const runArtifacts = artifacts.filter((artifact) => artifact.run_id === run.run_id); const feed = buildFeed(run, runEvents, runArtifacts, locale); return ( onSelectRun(run.run_id)} locale={locale} /> ); })}
)}
); }