241 lines
9.3 KiB
TypeScript
241 lines
9.3 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
AlertTriangle,
|
|
ArrowRightCircle,
|
|
Bot,
|
|
CheckCircle2,
|
|
ClipboardList,
|
|
ChevronDown,
|
|
FileText,
|
|
GitBranch,
|
|
History,
|
|
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 { containedJsonTextClass, containedLongTextClass, containedPreservedLongTextClass } from '@/lib/text-wrapping';
|
|
import type { TaskTimelineCard as TaskTimelineCardView, TaskTimelineCardType } from '@/types';
|
|
|
|
import { TaskAcceptanceControls, type TaskFeedbackItem, type TaskFeedbackType } from './TaskAcceptanceCard';
|
|
|
|
type Props = {
|
|
card: TaskTimelineCardView;
|
|
resultAcceptance?: TaskResultAcceptance;
|
|
reviewTargetId?: string;
|
|
};
|
|
|
|
export type TaskResultAcceptance = {
|
|
sessionId: string;
|
|
runId: string | null;
|
|
taskStatus: string;
|
|
feedbackItems: TaskFeedbackItem[];
|
|
actionBusy: string | null;
|
|
revision?: string;
|
|
onRevisionChange?: (value: string) => void;
|
|
onSubmit: (feedbackType: TaskFeedbackType, comment?: string) => Promise<unknown>;
|
|
};
|
|
|
|
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 'result_history':
|
|
return History;
|
|
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'],
|
|
result_history: ['历史结果', 'Result history'],
|
|
acceptance: ['验收', 'Acceptance'],
|
|
};
|
|
const label = labels[type];
|
|
return pickAppText(locale, label[0], label[1]);
|
|
}
|
|
|
|
function humanStatus(status: string, locale: 'zh-CN' | 'en-US') {
|
|
const labels: Record<string, [string, string]> = {
|
|
open: ['已创建', 'Open'],
|
|
running: ['执行中', 'Running'],
|
|
awaiting_acceptance: ['等待验收', 'Awaiting acceptance'],
|
|
needs_revision: ['需要修改', 'Needs revision'],
|
|
closed: ['已完成', 'Closed'],
|
|
abandoned: ['已放弃', 'Abandoned'],
|
|
accept: ['接受', 'Accepted'],
|
|
satisfied: ['接受', 'Accepted'],
|
|
revise: ['请求修改', 'Revision requested'],
|
|
abandon: ['放弃任务', 'Abandoned'],
|
|
warning: ['提醒', 'Warning'],
|
|
};
|
|
const label = labels[status];
|
|
return label ? pickAppText(locale, label[0], label[1]) : status;
|
|
}
|
|
|
|
function historyVersions(details: Record<string, unknown> | undefined): Array<Record<string, unknown>> {
|
|
const versions = details?.versions;
|
|
return Array.isArray(versions) ? versions.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === 'object') : [];
|
|
}
|
|
|
|
function renderHistoryStatus(version: Record<string, unknown>, locale: 'zh-CN' | 'en-US') {
|
|
const status = String(version.acceptanceType || version.status || '');
|
|
return status ? humanStatus(status, locale) : pickAppText(locale, '历史版本', 'Previous version');
|
|
}
|
|
|
|
function TaskResultHistory({ card }: { card: TaskTimelineCardView }) {
|
|
const { locale } = useAppI18n();
|
|
const versions = historyVersions(card.details);
|
|
|
|
return (
|
|
<details className="mt-3 min-w-0 max-w-full overflow-hidden rounded-md border border-border bg-muted/20 px-3 py-2 text-sm">
|
|
<summary className="flex cursor-pointer select-none items-center justify-between gap-3 font-medium">
|
|
<span>{pickAppText(locale, '展开历史版本', 'Show previous versions')}</span>
|
|
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
|
</summary>
|
|
<div className="mt-3 space-y-3">
|
|
{versions.map((version, index) => (
|
|
<div key={String(version.runId || index)} className="min-w-0 max-w-full overflow-hidden rounded-md border border-border bg-background p-3">
|
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
<div className="text-sm font-medium">
|
|
{pickAppText(locale, `第 ${index + 1} 轮结果`, `Version ${index + 1}`)}
|
|
</div>
|
|
<Badge variant="outline" className="text-[11px]">
|
|
{renderHistoryStatus(version, locale)}
|
|
</Badge>
|
|
</div>
|
|
{version.result ? <p className={`mt-2 text-sm leading-6 text-muted-foreground ${containedPreservedLongTextClass}`}>{String(version.result)}</p> : null}
|
|
{version.comment ? (
|
|
<div className={`mt-3 rounded-md bg-muted/35 p-2 text-xs text-muted-foreground ${containedLongTextClass}`}>
|
|
{pickAppText(locale, '修改意见', 'Revision note')}: {String(version.comment)}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</details>
|
|
);
|
|
}
|
|
|
|
export function TaskTimelineCard({ card, resultAcceptance, reviewTargetId }: Props) {
|
|
const { locale } = useAppI18n();
|
|
const Icon = iconForType(card.type);
|
|
const shouldRenderResultAcceptance = Boolean(card.type === 'result' && resultAcceptance && card.runId === resultAcceptance.runId);
|
|
|
|
return (
|
|
<Card id={shouldRenderResultAcceptance ? reviewTargetId : undefined} className="scroll-mt-28 overflow-hidden 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 items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<h3 className="min-w-0 flex-1 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 className={containedLongTextClass}>{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="shrink-0 text-[11px]">
|
|
{humanStatus(card.status, locale)}
|
|
</Badge>
|
|
)
|
|
) : null}
|
|
</div>
|
|
|
|
{card.summary ? <p className={`mt-3 text-sm leading-6 text-muted-foreground ${containedPreservedLongTextClass}`}>{card.summary}</p> : null}
|
|
|
|
{shouldRenderResultAcceptance ? (
|
|
<div className="mt-4 border-t border-border pt-4">
|
|
<TaskAcceptanceControls {...resultAcceptance!} />
|
|
</div>
|
|
) : null}
|
|
|
|
{card.type === 'result_history' ? <TaskResultHistory card={card} /> : card.details ? (
|
|
<details className="mt-3 min-w-0 max-w-full overflow-hidden 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 text-[11px] leading-5 text-muted-foreground ${containedJsonTextClass}`}>
|
|
{detailsJson(card.details)}
|
|
</pre>
|
|
</details>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|