diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 6c3e6119..cdb7de63 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1551,6 +1551,57 @@ "item3": "Review contract lifecycle", "item4": "Open the AwoooP work map" } + }, + "runDetail": { + "back": "Back to Run Monitor", + "title": "Run Disposition Timeline", + "refresh": "Refresh", + "empty": "--", + "durationSeconds": "{seconds}s", + "errors": { + "title": "Failed to load run details", + "loadFailed": "Load failed" + }, + "stats": { + "state": "Current State", + "timeline": "Timeline", + "mcpSteps": "MCP / Steps", + "duration": "Duration" + }, + "summary": { + "title": "Run Summary", + "project": "Project", + "agent": "Agent", + "traceId": "Trace ID", + "trigger": "Trigger", + "triggerRef": "Trigger Ref", + "cost": "Cost", + "attempts": "Attempts", + "created": "Created", + "completed": "Completed", + "error": "Error" + }, + "timeline": { + "title": "Disposition Timeline", + "lastUpdated": "Last updated {time}", + "count": "{count} items", + "empty": "No timeline records yet." + }, + "statuses": { + "blocked": "Blocked", + "cancelled": "Cancelled", + "completed": "Completed", + "error": "Error", + "failed": "Failed", + "pending": "Pending", + "received": "Received", + "running": "Running", + "sent": "Sent", + "shadow": "Shadow", + "success": "Success", + "timeout": "Timed out", + "waitingApproval": "Waiting approval" + } } } } diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 34a59f88..94924a9d 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1552,6 +1552,57 @@ "item3": "審查 Contract lifecycle", "item4": "查看 AwoooP 工作鏈路地圖" } + }, + "runDetail": { + "back": "返回 Run 監控", + "title": "Run 處置脈絡", + "refresh": "重新整理", + "empty": "--", + "durationSeconds": "{seconds}s", + "errors": { + "title": "無法載入 Run 詳情", + "loadFailed": "載入失敗" + }, + "stats": { + "state": "目前狀態", + "timeline": "Timeline", + "mcpSteps": "MCP / Steps", + "duration": "執行時間" + }, + "summary": { + "title": "Run 摘要", + "project": "Project", + "agent": "Agent", + "traceId": "Trace ID", + "trigger": "Trigger", + "triggerRef": "Trigger Ref", + "cost": "Cost", + "attempts": "Attempts", + "created": "Created", + "completed": "Completed", + "error": "Error" + }, + "timeline": { + "title": "處置時間線", + "lastUpdated": "上次更新 {time}", + "count": "{count} 筆", + "empty": "尚無時間線資料。" + }, + "statuses": { + "blocked": "已阻擋", + "cancelled": "已取消", + "completed": "已完成", + "error": "錯誤", + "failed": "失敗", + "pending": "待執行", + "received": "已接收", + "running": "執行中", + "sent": "已送出", + "shadow": "Shadow", + "success": "成功", + "timeout": "已超時", + "waitingApproval": "等待審批" + } } } } diff --git a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx index a15c55ac..f6eeef10 100644 --- a/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx +++ b/apps/web/src/app/[locale]/awooop/runs/[run_id]/page.tsx @@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useSearchParams } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { Activity, AlertCircle, @@ -85,9 +86,25 @@ const STATUS_STYLE: Record = { timeout: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", }; -function formatTime(value?: string | null) { - if (!value) return "--"; - return new Date(value).toLocaleString("zh-TW", { +const STATUS_TRANSLATION_KEYS: Record = { + blocked: "statuses.blocked", + cancelled: "statuses.cancelled", + completed: "statuses.completed", + error: "statuses.error", + failed: "statuses.failed", + pending: "statuses.pending", + received: "statuses.received", + running: "statuses.running", + sent: "statuses.sent", + shadow: "statuses.shadow", + success: "statuses.success", + timeout: "statuses.timeout", + waiting_approval: "statuses.waitingApproval", +}; + +function formatTime(value: string | null | undefined, locale: string, emptyLabel: string) { + if (!value) return emptyLabel; + return new Date(value).toLocaleString(locale === "zh-TW" ? "zh-TW" : "en-US", { month: "2-digit", day: "2-digit", hour: "2-digit", @@ -108,20 +125,42 @@ function itemIcon(kind: string) { return Route; } -function DetailField({ label, value }: { label: string; value?: string | number | null }) { +function DetailField({ + label, + value, + emptyLabel, +}: { + label: string; + value?: string | number | null; + emptyLabel: string; +}) { return (
{label}
-
{value ?? "--"}
+
+ {value ?? emptyLabel} +
); } -function TimelineRow({ item }: { item: TimelineItem }) { +function TimelineRow({ + item, + locale, + emptyLabel, + statusLabel, +}: { + item: TimelineItem; + locale: string; + emptyLabel: string; + statusLabel: (status: string) => string; +}) { const Icon = itemIcon(item.kind); return (
-
{formatTime(item.ts)}
+
+ {formatTime(item.ts, locale, emptyLabel)} +
@@ -129,7 +168,7 @@ function TimelineRow({ item }: { item: TimelineItem }) {

{item.title}

- {item.status} + {statusLabel(item.status)}
{item.summary && ( @@ -143,7 +182,7 @@ function TimelineRow({ item }: { item: TimelineItem }) {
{key}
- {value === null || value === undefined ? "--" : String(value)} + {value === null || value === undefined ? emptyLabel : String(value)}
))} @@ -157,9 +196,11 @@ function TimelineRow({ item }: { item: TimelineItem }) { export default function RunDetailPage({ params, }: { - params: { run_id: string }; + params: { locale: string; run_id: string }; }) { const { run_id } = params; + const locale = useLocale(); + const t = useTranslations("awooop.runDetail"); const searchParams = useSearchParams(); const projectId = searchParams.get("project_id") ?? ""; @@ -180,11 +221,11 @@ export default function RunDetailPage({ setDetail(data); setLastRefresh(new Date()); } catch (err) { - setError(err instanceof Error ? err.message : "載入失敗"); + setError(err instanceof Error ? err.message : t("errors.loadFailed")); } finally { setLoading(false); } - }, [projectId, run_id]); + }, [projectId, run_id, t]); useEffect(() => { setLoading(true); @@ -198,11 +239,19 @@ export default function RunDetailPage({ const run = detail?.run; const durationText = useMemo(() => { - if (!run?.created_at) return "--"; + if (!run?.created_at) return t("empty"); const end = run.completed_at || run.heartbeat_at || new Date().toISOString(); const ms = Math.max(0, new Date(end).getTime() - new Date(run.created_at).getTime()); - return `${Math.round(ms / 1000)}s`; - }, [run]); + return t("durationSeconds", { seconds: Math.round(ms / 1000) }); + }, [run, t]); + + const statusLabel = useCallback( + (status: string) => { + const key = STATUS_TRANSLATION_KEYS[status]; + return key ? t(key as never) : status; + }, + [t] + ); return (
@@ -211,14 +260,14 @@ export default function RunDetailPage({ className="inline-flex items-center gap-2 text-sm text-[#77736a] hover:text-[#141413]" >