'use client'; import React from 'react'; import { CheckCircle2, Loader2, Sparkles } from 'lucide-react'; import type { ProcessArtifact, ProcessEvent, ProcessRun } from '@/types'; import { Badge } from '@/components/ui/badge'; import { appArtifactPreview, appFeedRoleLabel, appStatusLabel } from '@/lib/i18n/common'; import type { AppLocale } from '@/lib/i18n/core'; 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-[#BCC4CE] bg-[#E4E7EB]/45', title: 'text-[#697281]', dot: 'bg-[#8C96A3]', result: 'border-[#BCC4CE] bg-[#E4E7EB]/55', }, { frame: 'border-[#B7C2B5] bg-[#E3E8E2]/45', title: 'text-[#657162]', dot: 'bg-[#869683]', result: 'border-[#B7C2B5] bg-[#E3E8E2]/55', }, { frame: 'border-[#B8AEA8] bg-[#E7E2DE]/55', title: 'text-[#5F5550]', dot: 'bg-[#8B7E77]', result: 'border-[#B8AEA8] bg-[#E7E2DE]/65', }, { frame: 'border-[#D8D2CE] bg-[#ECE8E5]/70', title: 'text-[#4F4642]', dot: 'bg-[#6A5E58]', result: 'border-[#D8D2CE] bg-[#ECE8E5]/80', }, ] as const; function accentFor(index: number) { return AGENT_ACCENTS[index % AGENT_ACCENTS.length]; } function statusTone(status: ProcessRun['status']) { if (status === 'done') return 'border-[#B7C2B5] bg-[#E3E8E2] text-[#657162]'; if (status === 'error') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#342E2B]'; if (status === 'cancelled') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#6A5E58]'; if (status === 'waiting') return 'border-[#B8AEA8] bg-[#E7E2DE] text-[#5F5550]'; if (status === 'queued') return 'border-[#D8D2CE] bg-[#ECE8E5] text-[#4F4642]'; return 'border-[#BCC4CE] bg-[#E4E7EB] text-[#697281]'; } 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: AppLocale, ): 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: AppLocale): 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 SkillChips({ metadata }: { metadata?: Record }) { const rawSelected = metadata?.selected_skill_names; const rawEphemeral = metadata?.ephemeral_skill_names; const selected = Array.isArray(rawSelected) ? rawSelected.map(String).filter(Boolean) : []; const ephemeral = Array.isArray(rawEphemeral) ? rawEphemeral.map(String).filter(Boolean) : []; const guidanceId = typeof metadata?.ephemeral_guidance_id === 'string' ? metadata.ephemeral_guidance_id : ''; if (selected.length === 0 && ephemeral.length === 0 && !guidanceId) { return null; } return (
{selected.map((name) => ( skill:{name} ))} {ephemeral.map((name) => ( ephemeral:{name} ))} {guidanceId && ( guidance:{guidanceId.slice(0, 8)} )}
); } 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: AppLocale; }) { 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: AppLocale; }) { 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: AppLocale; }) { const accent = accentFor(accentIndex); return ( ); } export function AgentTeamBlock({ rootRun, memberRuns, events, artifacts, selectedRunId, onSelectRun, }: { rootRun: ProcessRun; memberRuns: ProcessRun[]; events: ProcessEvent[]; artifacts: ProcessArtifact[]; selectedRunId: string | null; onSelectRun: (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; 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, '任务子流程', 'Task subprocess')}
{rootRun.title}

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

{pickAppText(locale, `${memberRuns.length} 个子任务`, `${memberRuns.length} subtasks`)} {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} /> ); })}
)}
); }