Files
beaver_project/app-instance/frontend/components/task-detail/TaskTimelineCard.tsx

151 lines
4.9 KiB
TypeScript

'use client';
import {
AlertTriangle,
ArrowRightCircle,
Bot,
CheckCircle2,
ClipboardList,
FileText,
GitBranch,
ListChecks,
Sparkles,
TerminalSquare,
ThumbsUp,
Users,
Wrench,
} from 'lucide-react';
import { TaskRuntimeStatusBadge, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent } from '@/components/ui/card';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import type { TaskRuntimeStatus } from '@/lib/task-runtime';
import type { TaskTimelineCard as TaskTimelineCardView, TaskTimelineCardType } from '@/types';
type Props = {
card: TaskTimelineCardView;
};
const RUNTIME_STATUSES = new Set<string>(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']);
function isRuntimeStatus(status: string): status is TaskRuntimeStatus {
return RUNTIME_STATUSES.has(status);
}
function iconForType(type: TaskTimelineCardType) {
switch (type) {
case 'task_created':
return ClipboardList;
case 'plan':
return ListChecks;
case 'skill':
return Sparkles;
case 'tool_call':
return Wrench;
case 'tool_result':
return TerminalSquare;
case 'next_step':
return ArrowRightCircle;
case 'agent_team':
return Users;
case 'agent_progress':
return Bot;
case 'agent_handoff':
return GitBranch;
case 'artifact':
return FileText;
case 'error':
return AlertTriangle;
case 'result':
return CheckCircle2;
case 'acceptance':
return ThumbsUp;
}
}
function detailsJson(details: Record<string, unknown>): string {
try {
return JSON.stringify(details, null, 2);
} catch {
return String(details);
}
}
function cardTypeLabel(type: TaskTimelineCardType, locale: 'zh-CN' | 'en-US') {
const labels: Record<TaskTimelineCardType, [string, string]> = {
task_created: ['任务', 'Task'],
plan: ['计划', 'Plan'],
skill: ['Skill', 'Skill'],
tool_call: ['工具调用', 'Tool call'],
tool_result: ['工具结果', 'Tool result'],
next_step: ['下一步', 'Next step'],
agent_team: ['Agent Team', 'Agent team'],
agent_progress: ['Agent', 'Agent'],
agent_handoff: ['交接', 'Handoff'],
artifact: ['产物', 'Artifact'],
error: ['异常', 'Error'],
result: ['结果', 'Result'],
acceptance: ['验收', 'Acceptance'],
};
const label = labels[type];
return pickAppText(locale, label[0], label[1]);
}
export function TaskTimelineCard({ card }: Props) {
const { locale } = useAppI18n();
const Icon = iconForType(card.type);
return (
<Card className="rounded-md">
<CardContent className="p-4">
<div className="flex gap-3">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md bg-muted">
<Icon className="h-4 w-4 text-muted-foreground" />
</div>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<h3 className="truncate text-sm font-semibold">{card.title}</h3>
<Badge variant="secondary" className="shrink-0 text-[11px]">
{cardTypeLabel(card.type, locale)}
</Badge>
</div>
<div className="mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
{card.actorName ? <span>{card.actorName}</span> : null}
<span>{formatTaskRuntimeTime(card.createdAt, locale)}</span>
{card.runId ? <span className="font-mono">{card.runId.slice(0, 8)}</span> : null}
</div>
</div>
{card.status ? (
isRuntimeStatus(card.status) ? (
<TaskRuntimeStatusBadge status={card.status} />
) : (
<Badge variant="outline" className="text-[11px]">
{card.status}
</Badge>
)
) : null}
</div>
{card.summary ? <p className="mt-3 whitespace-pre-wrap text-sm leading-6 text-muted-foreground">{card.summary}</p> : null}
{card.details ? (
<details className="mt-3 rounded-md border border-border bg-muted/20 px-3 py-2 text-xs">
<summary className="cursor-pointer select-none font-medium text-muted-foreground">
{pickAppText(locale, '详情 JSON', 'Details JSON')}
</summary>
<pre className="mt-2 max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] leading-5 text-muted-foreground">
{detailsJson(card.details)}
</pre>
</details>
) : null}
</div>
</div>
</CardContent>
</Card>
);
}