'use client'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import React from 'react'; import { ArrowLeft, ArrowRight, Boxes, FolderOutput, ListTree, MessageSquare, PanelRightOpen, Siren, Users, } from 'lucide-react'; import { OfficeStatusBadge, formatOfficeDuration, formatOfficeTime, progressPercent, } from '@/components/office/OfficeShared'; import { OfficePhaserCanvas } from '@/components/office/OfficePhaserCanvas'; import { TaskManagementTabs } from '@/components/task-management/TaskManagementTabs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { ScrollArea } from '@/components/ui/scroll-area'; import { buildOfficeView, isOfficeTaskTerminal } from '@/lib/office'; import { appEventKindLabel } from '@/lib/i18n/common'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import { useChatStore } from '@/lib/store'; function traceMetadataLabels(locale: 'zh-CN' | 'en-US'): Record { return { stage_label: pickAppText(locale, '阶段', 'Stage'), source: pickAppText(locale, '来源', 'Source'), phase: 'Phase', step: 'Step', selection_mode: pickAppText(locale, '选人方式', 'Selection mode'), selected_mode: pickAppText(locale, '选中模式', 'Selected mode'), execution_mode: pickAppText(locale, '执行模式', 'Execution mode'), selected_targets: pickAppText(locale, '成员', 'Members'), selected_count: pickAppText(locale, '成员数', 'Member count'), requested_targets: pickAppText(locale, '请求成员', 'Requested targets'), planned_targets: pickAppText(locale, '计划成员', 'Planned targets'), matched_procedure_id: pickAppText(locale, '命中 Procedure', 'Matched procedure'), candidate_procedure_id: pickAppText(locale, '候选 Procedure', 'Candidate procedure'), announcement_path: pickAppText(locale, '回流路径', 'Announcement path'), announcement_sender_id: pickAppText(locale, '回流 Sender', 'Announcement sender'), announcement_category: pickAppText(locale, '回流类别', 'Announcement category'), external_fallback_reason: pickAppText(locale, '外部回退原因', 'External fallback reason'), failure_type: pickAppText(locale, '失败分类', 'Failure type'), failure_reason: pickAppText(locale, '失败原因', 'Failure reason'), error: pickAppText(locale, '错误', 'Error'), origin_channel: pickAppText(locale, '来源 Channel', 'Origin channel'), origin_chat_id: pickAppText(locale, '来源 Chat', 'Origin chat'), }; } function formatTraceValue(value: unknown): string | null { if (value === null || value === undefined) return null; if (typeof value === 'string') { const trimmed = value.trim(); return trimmed || null; } if (typeof value === 'number' || typeof value === 'boolean') return String(value); if (Array.isArray(value)) { const parts = value .map((item) => formatTraceValue(item)) .filter((item): item is string => Boolean(item)); return parts.length > 0 ? parts.join(', ') : null; } try { return JSON.stringify(value); } catch { return String(value); } } function traceMetadataEntries( metadata: Record | null | undefined, labels: Record ): Array<{ key: string; label: string; value: string }> { if (!metadata) return []; const entries: Array<{ key: string; label: string; value: string }> = []; const used = new Set(); for (const [key, label] of Object.entries(labels)) { const value = formatTraceValue(metadata[key]); if (!value) continue; used.add(key); entries.push({ key, label, value }); } for (const [key, rawValue] of Object.entries(metadata)) { if (used.has(key)) continue; const value = formatTraceValue(rawValue); if (!value) continue; entries.push({ key, label: key, value }); } return entries; } function PixelPanel({ title, subtitle, children, icon: Icon, }: { title: string; subtitle?: string; children: React.ReactNode; icon?: React.ComponentType<{ className?: string }>; }) { return (
{Icon ? : null} {title}
{subtitle ? (
{subtitle}
) : null}
{children}
); } function BoardPanel({ icon: Icon, title, description, children, }: { icon: React.ComponentType<{ className?: string }>; title: string; description?: string; children: React.ReactNode; }) { return ( {title} {description ? {description} : null} {children} ); } export default function OfficeDetailPage() { const { locale } = useAppI18n(); 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 office = React.useMemo( () => buildOfficeView(taskId, { sessions, processRuns, processEvents, processArtifacts }, locale), [locale, processArtifacts, processEvents, processRuns, sessions, taskId] ); const metadataLabels = React.useMemo(() => traceMetadataLabels(locale), [locale]); const [selectedRunId, setSelectedRunId] = React.useState(null); const [detailOpen, setDetailOpen] = React.useState(false); React.useEffect(() => { setSelectedRunId(office?.rootRunId ?? null); setDetailOpen(false); }, [office?.rootRunId]); const selectedTask = React.useMemo( () => office?.tasks.find((task) => task.runId === selectedRunId) ?? office?.tasks[0] ?? null, [office?.tasks, selectedRunId] ); const selectedRun = React.useMemo( () => processRuns.find((run) => run.run_id === selectedTask?.runId) ?? null, [processRuns, selectedTask?.runId] ); const selectedRunMetadata = React.useMemo( () => traceMetadataEntries(selectedRun?.metadata, metadataLabels), [metadataLabels, selectedRun?.metadata] ); const selectedEvents = React.useMemo( () => processEvents .filter((event) => event.run_id === selectedTask?.runId) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) .slice(0, 16), [processEvents, selectedTask?.runId] ); const selectedArtifacts = React.useMemo( () => processArtifacts .filter((artifact) => artifact.run_id === selectedTask?.runId) .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()), [processArtifacts, selectedTask?.runId] ); const openRunDetail = React.useCallback((runId: string) => { setSelectedRunId(runId); setDetailOpen(true); }, []); if (!office) { return (

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

{pickAppText( locale, '当前 store 中没有这个 task 的运行数据。先从对话页发起任务,或者回到 Office 列表查看当前可用任务。', 'The current store does not contain runtime data for this task yet. Start it from chat first, or return to the office list to inspect available tasks.' )}

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

{office.title}

{pickAppText(locale, '负责人', 'Lead')}: {office.rootActorName} {pickAppText(locale, '会话', 'Session')}: {office.sourceSessionLabel} {pickAppText(locale, '开始', 'Started')}: {formatOfficeTime(office.createdAt, locale)} {pickAppText(locale, '耗时', 'Duration')}: {formatOfficeDuration(office.durationMs, locale)}
{selectedTask?.summary || pickAppText(locale, '当前选中任务没有摘要,先从右侧任务看板切一个具体 run 看现场。', 'The selected task has no summary yet. Pick a specific run from the board on the right to inspect the floor.')}
{office.alerts.slice(0, 2).map((alert) => ( ))}
{office.progress.label} {progressValue}%
{selectedTask ? (
{pickAppText(locale, '当前聚焦', 'Current focus')}
{selectedTask.title}
{selectedTask.actorName} · {selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}
) : null}
{isOfficeTaskTerminal(office.status) ? (
{pickAppText(locale, '任务已结束,办公室已解散,但现场记录仍可回看。', 'The task has ended and the office has dissolved, but the floor record is still available for review.')}
) : null}
{office.members.map((member) => ( ))}
{office.tasks.map((task) => ( ))}
{office.assignments.length === 0 ? (
{pickAppText(locale, '当前没有可见的子任务分工。', 'No visible subtask assignments yet.')}
) : ( office.assignments.map((assignment) => ( )) )}
{office.alerts.length === 0 ? (
{pickAppText(locale, '当前没有高优先级告警。', 'There are no high-priority alerts right now.')}
) : ( office.alerts.map((alert) => ( )) )}
{selectedTask?.title ?? pickAppText(locale, '任务详情', 'Task details')} {selectedTask ? `${selectedTask.actorName} · ${selectedTask.stageLabel ?? pickAppText(locale, '无阶段标签', 'No stage label')}` : pickAppText(locale, '当前没有选中的任务实例。', 'No task run is currently selected.')} {!selectedTask ? (
{pickAppText(locale, '当前没有可展示的任务详情。', 'There are no task details to display right now.')}
) : (
{selectedTask.title}
{selectedTask.actorName}
{pickAppText(locale, '开始时间', 'Started')} {formatOfficeTime(selectedTask.startedAt, locale)}
{pickAppText(locale, '最近更新', 'Last update')} {formatOfficeTime(selectedTask.updatedAt, locale)}
{pickAppText(locale, '阶段', 'Stage')} {selectedTask.stageLabel ?? '-'}
{selectedRunMetadata.length > 0 ? (
{pickAppText(locale, '链路上下文', 'Trace context')}
{selectedRunMetadata.map((item) => (
{item.label} {item.value}
))}
) : null} {selectedTask.summary ? (
{selectedTask.summary}
) : null}
{pickAppText(locale, '产物', 'Artifacts')}
{selectedArtifacts.length === 0 ? (
{pickAppText(locale, '当前没有产物。', 'There are no artifacts for this task.')}
) : ( selectedArtifacts.map((artifact) => (
{artifact.title}
{artifact.artifact_type} · {formatOfficeTime(artifact.created_at, locale)}
)) )}
{pickAppText(locale, '最近事件', 'Recent events')}
{selectedEvents.length === 0 ? (
{pickAppText(locale, '当前没有事件。', 'There are no events for this task.')}
) : ( selectedEvents.map((event) => { const metadataEntries = traceMetadataEntries(event.metadata, metadataLabels); return (
{appEventKindLabel(event.kind, locale)}
{formatOfficeTime(event.created_at, locale)}
{event.status ? (
{pickAppText(locale, '状态', 'Status')}: {event.status}
) : null}
{event.text || pickAppText(locale, '结构化更新', 'Structured update')}
{metadataEntries.length > 0 ? (
{pickAppText(locale, '事件上下文', 'Event context')}
{metadataEntries.map((item) => (
{item.label} {item.value}
))}
) : null}
)}) )}
)}
); } function MetricTile({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); } function MiniMetric({ label, value }: { label: string; value: string }) { return (
{label}
{value}
); }