'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, Loader2, MessageSquare, RefreshCw, ThumbsUp, Trash2, User, XCircle } from 'lucide-react'; import { TaskRuntimeStatusBadge, formatTaskRuntimeDuration, formatTaskRuntimeTime, progressPercent } from '@/components/task-runtime/TaskRuntimeShared'; 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 { deleteBackendTask, getBackendTask, getFileUrl, submitChatFeedback } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { buildTaskRuntimeView, type TaskRuntimeNodeView } from '@/lib/task-runtime'; import { useChatStore } from '@/lib/store'; import type { BackendTask, BackendTaskRun, ProcessArtifact, ProcessEvent } from '@/types'; type TaskFeedbackType = 'satisfied' | 'revise' | 'abandon'; type TaskFeedbackItem = { feedback_type?: unknown; comment?: unknown; created_at?: unknown; run_id?: unknown; }; function taskVisibleStatus(task: TaskRuntimeNodeView, 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( () => buildTaskRuntimeView(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 [runtimeFeedback, setRuntimeFeedback] = useState(null); const [actionError, setActionError] = useState(null); const [actionBusy, setActionBusy] = useState(null); React.useEffect(() => { setSelectedRunId(task?.rootRunId ?? null); setRuntimeFeedback(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'); }); }; const backendFeedbackRunId = backendTask ? pickFeedbackRunId(backendTask) : null; 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')}: {formatTaskRuntimeTime(backendTask.updated_at, locale)}
runAction(`backend-feedback-${feedbackType}`, async () => { await submitChatFeedback({ sessionId: backendTask.session_id, runId: backendFeedbackRunId!, feedbackType, comment, }); const refreshed = await getBackendTask(backendTask.task_id); setBackendTask(refreshed); }) } /> {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 ?

{formatTaskRuntimeTime(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')}: {formatTaskRuntimeTime(task.createdAt, locale)} {pickAppText(locale, '耗时', 'Duration')}: {formatTaskRuntimeDuration(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 || formatTaskRuntimeTime(event.created_at, locale)}
))}
) : (

-

)}
runAction(`runtime-feedback-${feedbackType}`, async () => { updateMessageFeedback(task.rootRunId, feedbackType); await submitChatFeedback({ sessionId: task.sessionId || 'web:default', runId: task.rootRunId, feedbackType, comment, }); setRuntimeFeedback({ feedback_type: feedbackType, comment: comment || '', created_at: new Date().toISOString(), run_id: task.rootRunId, }); setRevision(''); }) } />
{pickAppText(locale, '产物', 'Artifacts')}
{artifacts.length === 0 ? (

{pickAppText(locale, '暂无产物', 'No artifacts yet')}

) : ( artifacts.map((artifact) => (
{artifact.title}
{artifact.actor_name || artifact.actor_id}
{artifact.url || artifact.file_id ? ( ) : ( )}
)) )}
); } function Metric({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function TaskFeedbackPanel({ sessionId, runId, taskStatus, feedbackItems, actionBusy, revision, onRevisionChange, onSubmit, }: { sessionId: string; runId: string | null; taskStatus: string; feedbackItems: TaskFeedbackItem[]; actionBusy: string | null; revision?: string; onRevisionChange?: (value: string) => void; onSubmit: (feedbackType: TaskFeedbackType, comment?: string) => Promise; }) { const { locale } = useAppI18n(); const [localComment, setLocalComment] = React.useState(''); const comment = revision ?? localComment; const setComment = onRevisionChange ?? setLocalComment; const isFinalized = taskStatus === 'closed' || taskStatus === 'abandoned'; const recordedFeedback = feedbackForRun(feedbackItems, runId) ?? (isFinalized ? latestFeedback(feedbackItems) : null); const canSubmit = Boolean(runId) && !recordedFeedback && !isFinalized && !actionBusy; const submit = (feedbackType: TaskFeedbackType, nextComment?: string) => { if (!runId || !canSubmit) return; void onSubmit(feedbackType, nextComment); }; return ( {pickAppText(locale, '任务反馈', 'Task feedback')} {recordedFeedback ? (
{pickAppText(locale, '已提交反馈', 'Feedback submitted')}: {humanFeedback(String(recordedFeedback.feedback_type || ''), locale)}
{recordedFeedback.comment ? (

{String(recordedFeedback.comment)}

) : null} {recordedFeedback.created_at ? (

{formatTaskRuntimeTime(String(recordedFeedback.created_at), locale)}

) : null}
) : isFinalized ? (
{pickAppText(locale, '任务已结束,不能再提交新的反馈。', 'This task is finalized and cannot accept new feedback.')}
) : !runId ? (
{pickAppText(locale, '暂无可反馈的运行记录。', 'No run is available for feedback yet.')}
) : null}
} label={pickAppText(locale, '满意', 'Satisfied')} actionBusy={actionBusy} disabled={!canSubmit} onClick={() => submit('satisfied', comment.trim() || undefined)} /> } label={pickAppText(locale, '需要修改', 'Needs revision')} actionBusy={actionBusy} disabled={!canSubmit || !comment.trim()} onClick={() => submit('revise', comment.trim())} /> } label={pickAppText(locale, '放弃', 'Abandon')} actionBusy={actionBusy} disabled={!canSubmit} onClick={() => submit('abandon', comment.trim() || undefined)} />