'use client'; import { AlertTriangle, Bot, Download, ExternalLink, FileText, Users } from 'lucide-react'; import { TaskRuntimeStatusBadge, formatTaskRuntimeTime } 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 { getFileUrl } from '@/lib/api'; import { pickAppText } from '@/lib/i18n/core'; import { useAppI18n } from '@/lib/i18n/provider'; import type { TaskRuntimeStatus } from '@/lib/task-runtime'; import type { BackendTask, ProcessArtifact, ProcessRun, TaskTimelineCard } from '@/types'; type Props = { task: BackendTask; runs: ProcessRun[]; artifacts: ProcessArtifact[]; cards: TaskTimelineCard[]; }; const ACTIVE_RUN_STATUSES = new Set(['queued', 'running', 'waiting']); const RUNTIME_STATUSES = new Set(['queued', 'running', 'waiting', 'blocked', 'done', 'error', 'cancelled']); function isRuntimeStatus(status: string): status is TaskRuntimeStatus { return RUNTIME_STATUSES.has(status); } function humanTaskStatus(status: string, locale: 'zh-CN' | 'en-US') { const map: Record = { open: ['已创建', 'Open'], running: ['执行中', 'Running'], awaiting_acceptance: ['等待验收', 'Awaiting acceptance'], needs_revision: ['需要修改', 'Needs revision'], closed: ['已完成', 'Closed'], abandoned: ['已放弃', 'Abandoned'], accept: ['已接受', 'Accepted'], satisfied: ['已接受', 'Accepted'], revise: ['已请求修改', 'Revision requested'], abandon: ['已放弃', 'Abandoned'], }; const item = map[status]; return item ? pickAppText(locale, item[0], item[1]) : status; } function latestFeedback(task: BackendTask): Record | null { return [...(task.feedback ?? [])].reverse()[0] ?? null; } function acceptanceState(task: BackendTask, locale: 'zh-CN' | 'en-US'): string { const feedback = latestFeedback(task); const kind = String(feedback?.acceptance_type || feedback?.feedback_type || ''); if (kind) return humanTaskStatus(kind, locale); if (task.status === 'awaiting_acceptance') return pickAppText(locale, '等待验收', 'Awaiting acceptance'); if (task.status === 'needs_revision') return pickAppText(locale, '等待修改', 'Awaiting revision'); return pickAppText(locale, '未验收', 'No acceptance yet'); } function toTime(value: string): number { const parsed = new Date(value).getTime(); return Number.isFinite(parsed) ? parsed : 0; } function isWarningOrError(card: TaskTimelineCard): boolean { const severity = String(card.details?.severity || card.details?.level || '').toLowerCase(); return card.type === 'error' || card.status === 'error' || severity === 'warning' || severity === 'error'; } function artifactHref(artifact: ProcessArtifact): string | null { if (artifact.url) return artifact.url; if (artifact.file_id) return getFileUrl(artifact.file_id); return null; } function inlineArtifactPayload(artifact: ProcessArtifact): { content: string; filename: string; mimeType: string } | null { const baseName = (artifact.title || artifact.artifact_id || 'artifact').replace(/[\\/:*?"<>|]+/g, '-'); if (artifact.content !== undefined) { const isMarkdown = artifact.artifact_type === 'markdown'; return { content: artifact.content, filename: `${baseName}.${isMarkdown ? 'md' : 'txt'}`, mimeType: isMarkdown ? 'text/markdown;charset=utf-8' : 'text/plain;charset=utf-8', }; } if (artifact.data !== undefined) { return { content: JSON.stringify(artifact.data, null, 2), filename: `${baseName}.json`, mimeType: 'application/json;charset=utf-8', }; } return null; } function downloadInlineArtifact(artifact: ProcessArtifact): void { const payload = inlineArtifactPayload(artifact); if (!payload) return; const url = URL.createObjectURL(new Blob([payload.content], { type: payload.mimeType })); const anchor = document.createElement('a'); anchor.href = url; anchor.download = payload.filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } function RunRow({ run }: { run: ProcessRun }) { const { locale } = useAppI18n(); return (
{run.title || run.actor_name}
{run.actor_name}
{formatTaskRuntimeTime(run.started_at, locale)}
{run.summary ?

{run.summary}

: null}
); } export function TaskSideRail({ task, runs, artifacts, cards }: Props) { const { locale } = useAppI18n(); const activeRuns = runs.filter((run) => ACTIVE_RUN_STATUSES.has(run.status)); const childRuns = runs.filter((run) => Boolean(run.parent_run_id)); const latestAlert = cards.filter(isWarningOrError).sort((a, b) => toTime(b.createdAt) - toTime(a.createdAt))[0] ?? null; return ( ); }