'use client';
import React from 'react';
import {
BarChart3,
CheckCircle2,
ChevronDown,
Clock3,
Database,
Download,
Eye,
FileImage,
FileJson,
FileText,
Globe2,
Grid2X2,
ListFilter,
Network,
PackageOpen,
RefreshCw,
ShieldCheck,
Table2,
UserRound,
} from 'lucide-react';
import type { TaskFeedbackType } from '@/components/task-detail/TaskAcceptanceCard';
import type { TaskResultAcceptance } from '@/components/task-detail/TaskTimelineCard';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { formatTaskRuntimeDuration, formatTaskRuntimeTime } from '@/components/task-runtime/TaskRuntimeShared';
import { pickAppText } from '@/lib/i18n/core';
import { useAppI18n } from '@/lib/i18n/provider';
import {
buildTaskUiModel,
taskUiStatusClass,
taskUiStatusLabel,
type TaskUiAgentNode,
type TaskUiArtifact,
type TaskUiAttempt,
type TaskUiModel,
type TaskUiStatus,
type TaskUiStep,
} from '@/lib/task-ui-model';
import { containedLongTextClass, containedPreservedLongTextClass } from '@/lib/text-wrapping';
import type { BackendTask, SessionProcessProjection, TaskTimelineCard } from '@/types';
type Props = {
task: BackendTask;
process: SessionProcessProjection;
cards: TaskTimelineCard[];
isLive: boolean;
resultAcceptance?: TaskResultAcceptance;
reviewTargetId?: string;
};
function StatusBadge({ status, compact = false }: { status: TaskUiStatus; compact?: boolean }) {
const { locale } = useAppI18n();
return (
{taskUiStatusLabel(status, locale)}
);
}
function Section({
title,
children,
action,
className = '',
}: {
title: string;
children: React.ReactNode;
action?: React.ReactNode;
className?: string;
}) {
return (
{title}
{action}
{children}
);
}
function EmptyState({ children }: { children: React.ReactNode }) {
return (
);
}
function iconForStep(kind: TaskUiStep['kind']) {
if (kind === 'skill') return Grid2X2;
if (kind === 'tool') return Clock3;
if (kind === 'agent') return Network;
if (kind === 'artifact') return FileText;
if (kind === 'result') return BarChart3;
return FileText;
}
function statusDotClass(status: TaskUiStatus) {
if (status === 'done') return 'bg-[#22733A]';
if (status === 'running') return 'bg-[#C47B00]';
if (status === 'error') return 'bg-[#9D3D2F]';
if (status === 'cancelled') return 'bg-[#756A64]';
return 'bg-[#8D8782]';
}
function ExecutionFlow({ model }: { model: TaskUiModel }) {
const { locale } = useAppI18n();
const steps = model.steps.slice(0, 6);
const columnClass = steps.length >= 6 ? 'grid-cols-6' : steps.length >= 4 ? 'grid-cols-4' : steps.length >= 2 ? 'grid-cols-2' : 'grid-cols-1';
return (
查看详情
}
>
{steps.length > 1 ?
: null}
{steps.map((step) => {
const Icon = iconForStep(step.kind);
return (
{step.status === 'done' ? : }
{step.title}
{step.createdAt ? {formatTaskRuntimeTime(step.createdAt, locale)} : null}
{step.summary ? (
{step.summary}
) : null}
);
})}
);
}
function progressColor(status: TaskUiStatus) {
if (status === 'done') return '#137333';
if (status === 'running') return '#D48500';
if (status === 'error') return '#9D3D2F';
if (status === 'cancelled') return '#756A64';
return '#E3DFDC';
}
function AgentCard({ agent, root = false }: { agent: TaskUiAgentNode; root?: boolean }) {
return (
{agent.title || agent.name}
{agent.progress}%
);
}
function AgentDAG({ model }: { model: TaskUiModel }) {
const { locale } = useAppI18n();
const roots = model.agentTree;
const root = roots.find((node) => node.children.length > 0) ?? roots[0];
const children = root?.children.length ? root.children : roots.filter((node) => node.runId !== root?.runId);
const visibleChildren = children.slice(0, 5);
if (!model.team.hasTeam) {
return null;
}
return (
{model.team.outcome}
}
>
{roots.length === 0 ? (
{pickAppText(locale, '暂无 Agent Team 数据', 'No Agent Team data yet')}
) : (
{visibleChildren.length > 0 ? (
<>
{visibleChildren.map((child, index) => (
))}
>
) : null}
{visibleChildren.length > 0 ? (
{visibleChildren.map((agent) => (
))}
) : null}
)}
);
}
function RunPath({
model,
selectedAttemptId,
onSelectAttempt,
}: {
model: TaskUiModel;
selectedAttemptId: string | null;
onSelectAttempt: (attemptId: string) => void;
}) {
const { locale } = useAppI18n();
const [expandedIds, setExpandedIds] = React.useState>(() => new Set());
const attempts = model.attempts.filter((attempt) => attempt.runs.length > 0 || attempt.tools.length > 0 || attempt.result);
if (attempts.length === 0) return null;
return (
{attempts.length} runs
}
>
{attempts.map((attempt) => (
onSelectAttempt(attempt.id)}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
onSelectAttempt(attempt.id);
}
}}
role="group"
tabIndex={0}
aria-label={pickAppText(locale, `选择${attempt.title}`, `Select ${attempt.title}`)}
className={`rounded-lg border p-4 transition-colors ${
selectedAttemptId === attempt.id
? 'border-[#1D1715] bg-white shadow-[0_6px_18px_rgba(31,24,20,0.06)]'
: 'border-[#E1DCD8] bg-[#FBFAF9]'
} cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#1D1715] focus-visible:ring-offset-2`}
>
{attempt.title}
{formatTaskRuntimeTime(attempt.startedAt, locale)}
{attempt.finishedAt ? ` · ${formatAttemptDuration(attempt.startedAt, attempt.finishedAt, locale)}` : ''}
{attempt.tools.length} tools
{attempt.runs.length > 0 ? (
{attempt.runs.map((run, index) => (
{index > 0 ? → : null}
{run.actorName || run.title}
))}
) : null}
{attempt.result && expandedIds.has(attempt.id) ? (
{pickAppText(locale, '本次结果', 'Attempt result')}
{attempt.result.summary || attempt.result.title}
) : null}
))}
);
}
function formatAttemptDuration(startedAt: string, finishedAt: string, locale: string): string {
const startMs = new Date(startedAt).getTime();
const finishMs = new Date(finishedAt).getTime();
if (Number.isNaN(startMs) || Number.isNaN(finishMs) || finishMs < startMs) return '-';
return formatTaskRuntimeDuration(finishMs - startMs, locale);
}
function toolsForAttempt(model: TaskUiModel, selectedAttemptId: string | null): TaskUiAttempt {
return (
model.attempts.find((attempt) => attempt.id === selectedAttemptId) ??
model.attempts.at(-1) ??
{
id: 'all',
index: 1,
title: 'Agent',
status: 'waiting',
startedAt: '',
runs: [],
tools: model.tools,
}
);
}
function ToolCalls({ model, selectedAttemptId }: { model: TaskUiModel; selectedAttemptId: string | null }) {
const { locale } = useAppI18n();
const selectedAttempt = toolsForAttempt(model, selectedAttemptId);
const agents = Array.from(new Set(selectedAttempt.tools.map((tool) => tool.actorName || 'Agent')));
const [selectedAgent, setSelectedAgent] = React.useState(null);
const activeAgent = selectedAgent && agents.includes(selectedAgent) ? selectedAgent : agents[0] ?? 'Agent';
const visibleTools = selectedAttempt.tools.filter((tool) => (tool.actorName || 'Agent') === activeAgent);
return (
{selectedAttempt.title}
}
>
{selectedAttempt.tools.length === 0 ? (
{pickAppText(locale, '暂无工具调用', 'No tool calls yet')}
) : (
工具名称
摘要
状态
运行时间
{visibleTools.map((tool) => (
{tool.toolName}
{tool.summary}
{formatToolDuration(tool, locale)}
))}
)}
);
}
function formatToolDuration(tool: TaskUiModel['tools'][number], locale: string): string {
if (typeof tool.durationMs === 'number') {
return formatTaskRuntimeDuration(tool.durationMs, locale);
}
if (tool.status === 'running' && tool.createdAt) {
const startMs = new Date(tool.createdAt).getTime();
if (!Number.isNaN(startMs)) {
return formatTaskRuntimeDuration(Date.now() - startMs, locale);
}
}
return '-';
}
function iconForArtifact(artifact: TaskUiArtifact) {
if (artifact.type === 'json') return FileJson;
if (artifact.type === 'image') return FileImage;
return FileText;
}
function WorkspaceFiles({ model }: { model: TaskUiModel }) {
const { locale } = useAppI18n();
return (
}
>
{model.artifacts.length === 0 ? (
{pickAppText(locale, '暂无 Workspace 文件', 'No workspace files yet')}
) : (
<>
文件名
类型
大小
状态
操作
{model.artifacts.slice(0, 6).map((artifact) => {
const Icon = iconForArtifact(artifact);
return (
{artifact.title}
{artifact.type.toUpperCase()}
{artifact.sizeLabel || '-'}
);
})}
{model.artifacts.length > 6 ? (
) : null}
>
)}
);
}
function ResultPanel({
model,
resultAcceptance,
reviewTargetId,
}: {
model: TaskUiModel;
resultAcceptance?: TaskResultAcceptance;
reviewTargetId?: string;
}) {
const { locale } = useAppI18n();
const [busyAction, setBusyAction] = React.useState(null);
const submit = async (type: TaskFeedbackType) => {
if (!resultAcceptance || busyAction) return;
setBusyAction(type);
try {
await resultAcceptance.onSubmit(type);
} finally {
setBusyAction(null);
}
};
return (
}>
{model.result.summary ? (
{model.result.summary}
{model.result.bullets.length > 0 ? (
{model.result.bullets.map((item, index) => {
const Icon = [Globe2, Table2, BarChart3, ShieldCheck][index % 4];
return (
{item}
);
})}
) : null}
) : (
{pickAppText(locale, '暂无本轮结果', 'No result for this run yet')}
)}
);
}
export function TaskExecutionWorkspace({ task, process, cards, resultAcceptance, reviewTargetId }: Props) {
const { locale } = useAppI18n();
const model = React.useMemo(
() => buildTaskUiModel({ task, process, cards, locale }),
[cards, locale, process, task],
);
const latestAttemptId = model.attempts.at(-1)?.id ?? null;
const [selectedAttemptState, setSelectedAttemptState] = React.useState(latestAttemptId);
const selectedAttemptId = model.attempts.some((attempt) => attempt.id === selectedAttemptState)
? selectedAttemptState
: latestAttemptId;
return (
);
}