feat(awooop): add run detail action panel
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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": "已取消",
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<section className="border border-[#e0ddd4] bg-white p-4">
|
||||
<div className="h-5 w-40 animate-pulse bg-[#f2efe6]" />
|
||||
<div className="mt-3 h-4 w-full max-w-xl animate-pulse bg-[#f2efe6]" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<section className="grid gap-px border border-[#e0ddd4] bg-[#e0ddd4] lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div className="bg-white p-4">
|
||||
<div className="flex flex-wrap items-start justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={cn("flex h-8 w-8 items-center justify-center border", toneClass)}>
|
||||
<Icon className="h-4 w-4" aria-hidden="true" />
|
||||
</span>
|
||||
<div>
|
||||
<p className="text-xs font-semibold text-[#77736a]">{t("eyebrow")}</p>
|
||||
<h3 className="text-base font-semibold text-[#141413]">
|
||||
{t(`${actionKey}.title` as never)}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-3 max-w-3xl text-sm leading-6 text-[#5f5b52]">
|
||||
{t(`${actionKey}.detail` as never)}
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href={primaryHref}
|
||||
className={cn(
|
||||
"inline-flex shrink-0 items-center gap-2 border px-3 py-2 text-sm font-semibold",
|
||||
toneClass
|
||||
)}
|
||||
>
|
||||
{t(`${actionKey}.primary` as never)}
|
||||
<ArrowRight className="h-4 w-4" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-px bg-[#e0ddd4] md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-4">
|
||||
{evidence.map((item) => (
|
||||
<div key={item.label} className="bg-white px-4 py-3">
|
||||
<p className="text-xs font-semibold text-[#77736a]">{item.label}</p>
|
||||
<p className="mt-2 font-mono text-2xl font-semibold text-[#141413]">
|
||||
{item.value ?? emptyLabel}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function DetailField({
|
||||
label,
|
||||
value,
|
||||
@@ -322,6 +435,8 @@ export default function RunDetailPage({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<RunActionPanel run={run} counts={detail?.counts} emptyLabel={t("empty")} />
|
||||
|
||||
<section className="grid gap-4 xl:grid-cols-[360px_1fr]">
|
||||
<aside className="border border-[#e0ddd4] bg-white">
|
||||
<div className="border-b border-[#e0ddd4] bg-[#faf9f3] px-4 py-3">
|
||||
|
||||
Reference in New Issue
Block a user