From beba668a4c9723aa9a80e8e2d9679eaa8ae72e5e Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 7 May 2026 09:25:49 +0800 Subject: [PATCH] feat(awooop): add run detail action panel --- apps/web/messages/en.json | 34 ++++++ apps/web/messages/zh-TW.json | 34 ++++++ .../[locale]/awooop/runs/[run_id]/page.tsx | 115 ++++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index cdb7de63..0b2ad9c1 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1587,6 +1587,40 @@ "count": "{count} items", "empty": "No timeline records yet." }, + "action": { + "eyebrow": "Next Decision", + "approval": { + "title": "Waiting for human approval", + "detail": "AI is stopped at the human gate and has not resumed. Approve or reject from the approval page; every decision is written back to Run state and audit.", + "primary": "Open approval decision" + }, + "manual": { + "title": "Manual handoff required", + "detail": "AI cannot safely close the loop, or execution has failed / timed out. Return to Run Monitor to compare same-project work and hand off to the SRE war room when needed.", + "primary": "Back to Run Monitor" + }, + "completed": { + "title": "Completed, ready for audit review", + "detail": "The run has converged. Use the timeline to verify MCP calls, outbound messages, and cost records before writing back to KM / Playbook.", + "primary": "Back to Run Monitor" + }, + "running": { + "title": "AI is processing", + "detail": "The run is still active and this page refreshes periodically. If it stays running for too long, check heartbeat, MCP latency, and worker state.", + "primary": "Back to Run Monitor" + }, + "observe": { + "title": "Observing", + "detail": "The run has not reached a human gate or terminal state. Follow the timeline to verify inbound events, tool calls, and outbound messages.", + "primary": "Back to Run Monitor" + }, + "evidence": { + "inbound": "Inbound", + "outbound": "Outbound", + "mcp": "MCP Calls", + "steps": "Steps" + } + }, "statuses": { "blocked": "Blocked", "cancelled": "Cancelled", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 94924a9d..66e40245 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1588,6 +1588,40 @@ "count": "{count} 筆", "empty": "尚無時間線資料。" }, + "action": { + "eyebrow": "下一步判斷", + "approval": { + "title": "等待人工審批", + "detail": "AI 已停在人工閘門,尚未 resume。請從審批頁 approve 或 reject,所有決策都會回寫 Run state 與 audit。", + "primary": "前往審批決策" + }, + "manual": { + "title": "需人工接手", + "detail": "AI 無法安全閉環,或執行已失敗 / 超時。請回 Run 監控比對同專案任務,必要時交由 SRE 戰情室處置。", + "primary": "回 Run 監控" + }, + "completed": { + "title": "已完成,等待稽核回看", + "detail": "Run 已收斂。請以時間線檢查 MCP、出站訊息與成本紀錄是否完整,必要時再回寫 KM / Playbook。", + "primary": "回 Run 監控" + }, + "running": { + "title": "AI 正在處理", + "detail": "Run 尚未結束,頁面會定期刷新。若長時間停留在 running,請檢查 heartbeat、MCP latency 與 worker 狀態。", + "primary": "回 Run 監控" + }, + "observe": { + "title": "觀察中", + "detail": "目前尚未進入人工閘門或終止狀態。請沿時間線確認入站事件、工具呼叫與出站訊息是否有缺口。", + "primary": "回 Run 監控" + }, + "evidence": { + "inbound": "入站事件", + "outbound": "出站訊息", + "mcp": "MCP 呼叫", + "steps": "Steps" + } + }, "statuses": { "blocked": "已阻擋", "cancelled": "已取消", 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 f6eeef10..082724a1 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 @@ -12,12 +12,17 @@ import { Activity, AlertCircle, ArrowLeft, + ArrowRight, + CheckCircle2, Clock, MessageSquareText, RefreshCw, Route, + SearchCheck, Send, ShieldAlert, + ShieldCheck, + TriangleAlert, Wrench, } from "lucide-react"; @@ -102,6 +107,8 @@ const STATUS_TRANSLATION_KEYS: Record = { waiting_approval: "statuses.waitingApproval", }; +const MANUAL_STATES = new Set(["blocked", "cancelled", "error", "failed", "timeout"]); + 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", { @@ -125,6 +132,112 @@ function itemIcon(kind: string) { return Route; } +function RunActionPanel({ + run, + counts, + emptyLabel, +}: { + run?: RunDetail; + counts?: RunDetailResponse["counts"]; + emptyLabel: string; +}) { + const t = useTranslations("awooop.runDetail.action"); + + if (!run) { + return ( +
+
+
+
+ ); + } + + const isApproval = run.state === "waiting_approval"; + const isManual = MANUAL_STATES.has(run.state); + const isCompleted = run.state === "completed" || run.state === "success"; + const actionKey = isApproval + ? "approval" + : isManual + ? "manual" + : isCompleted + ? "completed" + : run.state === "running" + ? "running" + : "observe"; + const Icon = isApproval + ? ShieldCheck + : isManual + ? TriangleAlert + : isCompleted + ? CheckCircle2 + : run.state === "running" + ? Activity + : SearchCheck; + const toneClass = isApproval + ? "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]" + : isManual + ? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]" + : isCompleted + ? "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]" + : "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]"; + const projectParam = encodeURIComponent(run.project_id); + const primaryHref = isApproval + ? (`/awooop/approvals/${run.run_id}?project_id=${projectParam}` as never) + : (`/awooop/runs?project_id=${projectParam}` as never); + + const evidence = [ + { label: t("evidence.inbound"), value: counts?.inbound_events ?? 0 }, + { label: t("evidence.outbound"), value: counts?.outbound_messages ?? 0 }, + { label: t("evidence.mcp"), value: counts?.mcp_calls ?? 0 }, + { label: t("evidence.steps"), value: counts?.steps ?? 0 }, + ]; + + return ( +
+
+
+
+
+ + +
+

{t("eyebrow")}

+

+ {t(`${actionKey}.title` as never)} +

+
+
+

+ {t(`${actionKey}.detail` as never)} +

+
+ + {t(`${actionKey}.primary` as never)} +
+
+
+ {evidence.map((item) => ( +
+

{item.label}

+

+ {item.value ?? emptyLabel} +

+
+ ))} +
+
+ ); +} + function DetailField({ label, value, @@ -322,6 +435,8 @@ export default function RunDetailPage({ + +