'use client'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import React, { useMemo, useState } from 'react'; import { AlertCircle, ArrowLeft, Bot, CheckCircle2, Download, FileText, HelpCircle, MessageSquare, RefreshCw, RotateCcw, Trash2, User, XCircle } from 'lucide-react'; import { OfficeStatusBadge, formatOfficeDuration, formatOfficeTime, progressPercent } from '@/components/office/OfficeShared'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Textarea } from '@/components/ui/textarea'; import { cancelDelegation, deleteBackendTask, getBackendTask, getFileUrl, retryDelegation, submitChatFeedback } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { buildOfficeView, isOfficeTaskTerminal, type OfficeTaskView } from '@/lib/office'; import { useChatStore } from '@/lib/store'; import type { BackendTask, BackendTaskRun, ProcessArtifact, ProcessEvent } from '@/types'; function taskVisibleStatus(task: OfficeTaskView, locale: 'zh-CN' | 'en-US') { if (task.status === 'error') return pickAppText(locale, '任务失败', 'Task failed'); if (task.status === 'cancelled') return pickAppText(locale, '已取消', 'Cancelled'); return task.stageLabel || task.status; } function downloadText(filename: string, content: string) { const url = URL.createObjectURL(new Blob([content], { type: 'text/plain;charset=utf-8' })); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } export default function TaskDetailPage() { const { locale } = useAppI18n(); const router = useRouter(); const params = useParams<{ taskId: string }>(); const taskId = decodeURIComponent(Array.isArray(params?.taskId) ? params.taskId[0] : params?.taskId ?? ''); const sessions = useChatStore((state) => state.sessions); const processRuns = useChatStore((state) => state.processRuns); const processEvents = useChatStore((state) => state.processEvents); const processArtifacts = useChatStore((state) => state.processArtifacts); const updateMessageFeedback = useChatStore((state) => state.updateMessageFeedback); const task = useMemo( () => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale), [locale, processArtifacts, processEvents, processRuns, sessions, taskId] ); const [backendTask, setBackendTask] = useState(null); const [backendTaskLoading, setBackendTaskLoading] = useState(false); const [selectedRunId, setSelectedRunId] = useState(task?.rootRunId ?? null); const [revision, setRevision] = useState(''); const [actionError, setActionError] = useState(null); const [actionBusy, setActionBusy] = useState(null); React.useEffect(() => { setSelectedRunId(task?.rootRunId ?? null); }, [task?.rootRunId]); React.useEffect(() => { let cancelled = false; if (task || !taskId) { setBackendTask(null); return () => { cancelled = true; }; } setBackendTaskLoading(true); getBackendTask(taskId) .then((item) => { if (!cancelled) setBackendTask(item); }) .catch(() => { if (!cancelled) setBackendTask(null); }) .finally(() => { if (!cancelled) setBackendTaskLoading(false); }); return () => { cancelled = true; }; }, [task, taskId]); const runIds = useMemo(() => new Set(task?.tasks.map((item) => item.runId) ?? []), [task?.tasks]); const artifacts = useMemo( () => processArtifacts.filter((artifact) => runIds.has(artifact.run_id)), [processArtifacts, runIds] ); const eventsByRun = useMemo(() => { const map = new Map(); for (const event of processEvents) { if (!runIds.has(event.run_id)) continue; map.set(event.run_id, [...(map.get(event.run_id) ?? []), event]); } return map; }, [processEvents, runIds]); const artifactsByRun = useMemo(() => { const map = new Map(); for (const artifact of artifacts) { map.set(artifact.run_id, [...(map.get(artifact.run_id) ?? []), artifact]); } return map; }, [artifacts]); const phaseGroups = useMemo(() => { const groups = new Map(); for (const item of task?.tasks ?? []) { const label = item.stageLabel || taskVisibleStatus(item, locale); groups.set(label, [...(groups.get(label) ?? []), item]); } return Array.from(groups.entries()).map(([label, nodes]) => ({ label, nodes })); }, [locale, task?.tasks]); const selectedNode = task?.tasks.find((item) => item.runId === selectedRunId) ?? task?.tasks[0] ?? null; const runAction = async (key: string, action: () => Promise) => { setActionBusy(key); setActionError(null); try { await action(); } catch (err: any) { setActionError(err.message || pickAppText(locale, '操作失败', 'Action failed')); } finally { setActionBusy(null); } }; const deleteCurrentBackendTask = async () => { if (!backendTask) return; const title = backendTask.short_title || backendTask.description || backendTask.goal || backendTask.task_id; if (!window.confirm(pickAppText(locale, `删除任务“${title}”?`, `Delete task "${title}"?`))) { return; } await runAction('delete-backend-task', async () => { await deleteBackendTask(backendTask.task_id); router.push('/tasks'); }); }; if (!task && backendTask) { const validation = backendTask.validation_result; const accepted = Boolean(validation?.accepted); const validationIssues = [ ...arrayOfStrings(validation?.issues), ...arrayOfStrings(validation?.missing_requirements), ]; const feedbackItems = backendTask.feedback || []; return (
{backendTask.is_open ? {pickAppText(locale, '进行中', 'Active')} : null} {humanTaskStatus(backendTask.status, locale)}

{backendTask.short_title || String(backendTask.metadata?.short_title || '') || backendTask.description || backendTask.goal || backendTask.task_id}

{backendTask.description ? (

{backendTask.description}

) : null}
{pickAppText(locale, '来源会话', 'Session')}: {backendTask.session_id} {pickAppText(locale, '创建者', 'Creator')}: {backendTask.creator} {pickAppText(locale, '更新', 'Updated')}: {formatOfficeTime(backendTask.updated_at, locale)}
{pickAppText(locale, 'Agent 执行过程', 'Agent conversation process')} {(backendTask.runs ?? []).length === 0 ? (
{pickAppText(locale, '暂无可展示的问答过程', 'No readable conversation process yet')}
) : ( (backendTask.runs ?? []).map((run, index) => ) )}
{pickAppText(locale, '验证和反馈', 'Validation and feedback')}
{validation ? ( accepted ? : ) : ( )}
{validation ? accepted ? pickAppText(locale, '验证通过', 'Validation passed') : pickAppText(locale, '需要继续修改', 'Needs revision') : pickAppText(locale, '尚未验证', 'Not validated yet')}
{validation ? (
{pickAppText(locale, '评分', 'Score')}: {String(validation.score ?? '-')} · {pickAppText(locale, '验证器', 'Validator')}: {String(validation.validator ?? '-')}
) : null} {validationIssues.length > 0 && (
    {validationIssues.map((item, index) =>
  • {item}
  • )}
)} {typeof validation?.recommended_revision_prompt === 'string' && validation.recommended_revision_prompt && (

{validation.recommended_revision_prompt}

)}
{pickAppText(locale, '用户反馈', 'User feedback')}
{feedbackItems.length === 0 ? (

{pickAppText(locale, '还没有用户反馈。', 'No user feedback yet.')}

) : ( feedbackItems.map((item, index) => (
{humanFeedback(String(item.feedback_type || ''), locale)}
{item.comment ?

{String(item.comment)}

: null} {item.created_at ?

{formatOfficeTime(String(item.created_at), locale)}

: null}
)) )}
); } if (!task) { return (

{pickAppText(locale, '任务不存在', 'Task not found')}

{backendTaskLoading ? pickAppText(locale, '正在从后端任务库加载任务。', 'Loading the task from the backend task store.') : pickAppText(locale, '当前前端状态和后端任务库里都没有这个任务。', 'Neither frontend state nor backend task store contains this task.')}

); } const progressValue = progressPercent(task.progress.value, task.progress.max); return (

{task.title}

{pickAppText(locale, '来源会话', 'Session')}: {task.sourceSessionLabel} {pickAppText(locale, '主 Agent', 'Lead agent')}: {task.rootActorName} {pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(task.createdAt, locale)} {pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(task.durationMs, locale)}
{task.progress.label} {progressValue}%
{actionError && ( {actionError} )}
{pickAppText(locale, '阶段链', 'Phase chain')}
{phaseGroups.map((phase, index) => (
{phase.label}
{phase.nodes.length} nodes
{index < phaseGroups.length - 1 ? / : null}
))}
{phaseGroups.map((phase) => ( {phase.label} {phase.nodes.map((node) => ( ))} ))}
{pickAppText(locale, '节点详情', 'Node detail')} {selectedNode ? (
{selectedNode.title}
{selectedNode.runId}

{selectedNode.summary || '-'}

{(eventsByRun.get(selectedNode.runId) ?? []).slice(-5).map((event) => (
{event.kind}
{event.text || formatOfficeTime(event.created_at, locale)}
))}
) : (

-

)}
{pickAppText(locale, '修订意见', 'Revision')}