'use client'; import Link from 'next/link'; import { useParams, useRouter } from 'next/navigation'; import React, { useMemo, useState } from 'react'; import { AlertCircle, ArrowLeft, Loader2, Trash2 } from 'lucide-react'; import { TaskLiveHeader, TaskSideRail, TaskTimeline, type TaskFeedbackItem, type TaskFeedbackType, } from '@/components/task-detail'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { deleteBackendTask, getBackendTask, submitChatFeedback } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { useChatStore } from '@/lib/store'; import { shouldPollTaskDetail, taskDetailDurationMs } from '@/lib/task-detail-refresh'; import { buildTaskTimelineView } from '@/lib/task-timeline-view'; import type { BackendTask } from '@/types'; const TERMINAL_TASK_STATUSES = new Set(['closed', 'abandoned', 'cancelled', 'error']); const TASK_RESULT_REVIEW_ID = 'task-result-review'; 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 processRuns = useChatStore((state) => state.processRuns); const processEvents = useChatStore((state) => state.processEvents); const processArtifacts = useChatStore((state) => state.processArtifacts); const setSessionProcess = useChatStore((state) => state.setSessionProcess); const updateMessageFeedback = useChatStore((state) => state.updateMessageFeedback); const wsStatus = useChatStore((state) => state.wsStatus); const [backendTask, setBackendTask] = useState(null); const [backendTaskLoading, setBackendTaskLoading] = useState(true); const [revision, setRevision] = useState(''); const [actionError, setActionError] = useState(null); const [actionBusy, setActionBusy] = useState(null); const mountedRef = React.useRef(true); React.useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; }; }, []); const loadBackendTask = React.useCallback(async () => { if (!taskId) return null; setBackendTaskLoading(true); try { const item = await getBackendTask(taskId); if (!mountedRef.current) return item; setBackendTask(item); setSessionProcess(item.session_id, { runs: item.process_runs ?? [], events: item.process_events ?? [], artifacts: item.process_artifacts ?? [], }); return item; } catch { if (mountedRef.current) { setBackendTask(null); } return null; } finally { if (mountedRef.current) { setBackendTaskLoading(false); } } }, [setSessionProcess, taskId]); React.useEffect(() => { void loadBackendTask(); }, [loadBackendTask]); const isTaskLive = backendTask ? !TERMINAL_TASK_STATUSES.has(backendTask.status) : false; React.useEffect(() => { if (!shouldPollTaskDetail(backendTask)) return; const id = window.setInterval(() => { void loadBackendTask(); }, 4000); return () => window.clearInterval(id); }, [backendTask, loadBackendTask]); const timelineView = useMemo( () => buildTaskTimelineView({ task: backendTask, liveRuns: processRuns, liveEvents: processEvents, liveArtifacts: processArtifacts, locale, }), [backendTask, locale, processArtifacts, processEvents, processRuns] ); const timelineCards = timelineView?.cards ?? []; const activeLabel = [...timelineCards].reverse().find((card) => !['acceptance', 'task_created'].includes(card.type))?.title ?? '-'; const durationMs = backendTask ? taskDetailDurationMs(backendTask) : null; const feedbackRunId = backendTask ? pickFeedbackRunId(backendTask) : 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 (backendTask) { const feedbackItems = backendTask.feedback || []; return (
{actionError ? ( {actionError} ) : null} runAction(`backend-feedback-${feedbackType}`, async () => { if (!feedbackRunId) throw new Error(pickAppText(locale, '暂无可验收的运行记录。', 'No run is available for acceptance yet.')); await submitChatFeedback({ sessionId: backendTask.session_id, runId: feedbackRunId, feedbackType, comment, }); updateMessageFeedback(feedbackRunId, feedbackType); setRevision(''); await loadBackendTask(); }), }} />
); } return (
{backendTaskLoading ? : null}

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

{backendTaskLoading ? pickAppText(locale, '正在从后端任务库加载任务。', 'Loading the task from the backend task store.') : pickAppText(locale, '后端任务库里没有这个任务。', 'The backend task store does not contain this task.')}

); } function pickFeedbackRunId(task: BackendTask): string | null { const runIds = task.run_ids.filter(Boolean); if (runIds.length > 0) return runIds[runIds.length - 1]; const runs = task.runs ?? []; if (runs.length > 0) return runs[runs.length - 1].run_id; return null; }