diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 0b2ad9c1..add753bb 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -1636,6 +1636,67 @@ "timeout": "Timed out", "waitingApproval": "Waiting approval" } + }, + "approvalDecision": { + "back": "Back to Approval Queue", + "viewTimeline": "View Run Timeline", + "eyebrow": "Human Approval Gate", + "title": "Approval Decision", + "timeout": "Approval Deadline", + "empty": "--", + "errors": { + "title": "Failed to load run data", + "loadFailed": "Load failed", + "missingProject": "Missing project_id; cannot submit approval decision", + "actionFailed": "Action failed" + }, + "success": { + "approve": "Run approved. Returning to Timeline", + "reject": "Run rejected. Returning to Timeline" + }, + "notWaiting": { + "title": "This run is not waiting for human approval", + "detail": "Current state is {state}. This page will not show approve / reject; return to Run Timeline for the latest state." + }, + "details": { + "title": "Run Details", + "runId": "Run ID", + "project": "Project", + "agent": "Agent", + "state": "State", + "traceId": "Trace ID", + "trigger": "Trigger", + "triggerRef": "Trigger Ref", + "cost": "Cost", + "attempts": "Attempts", + "created": "Created", + "timeout": "Timeout", + "error": "Error", + "empty": "Run data was not found." + }, + "actions": { + "approve": "Approve", + "reject": "Reject" + }, + "dialog": { + "close": "Close", + "cancel": "Cancel", + "runId": "Run ID:", + "approve": { + "title": "Confirm Approval", + "body": "After approval, the run resumes from the human gate and continues through Runtime / MCP Gateway.", + "warning": "This decision is written to Run state, approval token, and audit trail.", + "confirm": "Confirm Approval" + }, + "reject": { + "title": "Confirm Rejection", + "body": "After rejection, the run is cancelled and will not continue automatic execution.", + "reason": "Rejection reason", + "placeholder": "Enter rejection reason...", + "warning": "The reason is written to the audit trail for later review in Run Timeline.", + "confirm": "Confirm Rejection" + } + } } } } diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 66e40245..7444873a 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -1637,6 +1637,67 @@ "timeout": "已超時", "waitingApproval": "等待審批" } + }, + "approvalDecision": { + "back": "返回審批佇列", + "viewTimeline": "查看 Run Timeline", + "eyebrow": "人工審批閘門", + "title": "審批決策", + "timeout": "審批期限", + "empty": "--", + "errors": { + "title": "無法載入 Run 資料", + "loadFailed": "載入失敗", + "missingProject": "缺少 project_id,無法送出審批決策", + "actionFailed": "操作失敗" + }, + "success": { + "approve": "Run 已核准,正在回到 Timeline", + "reject": "Run 已拒絕,正在回到 Timeline" + }, + "notWaiting": { + "title": "此 Run 目前不在人工審批狀態", + "detail": "目前狀態為 {state}。此頁不會顯示 approve / reject,請回 Run Timeline 檢查最新狀態。" + }, + "details": { + "title": "Run 詳情", + "runId": "Run ID", + "project": "Project", + "agent": "Agent", + "state": "狀態", + "traceId": "Trace ID", + "trigger": "Trigger", + "triggerRef": "Trigger Ref", + "cost": "Cost", + "attempts": "Attempts", + "created": "Created", + "timeout": "Timeout", + "error": "Error", + "empty": "找不到 Run 資料。" + }, + "actions": { + "approve": "核准", + "reject": "拒絕" + }, + "dialog": { + "close": "關閉", + "cancel": "取消", + "runId": "Run ID:", + "approve": { + "title": "確認核准", + "body": "核准後,Run 會從人工閘門 resume,繼續交由 Runtime / MCP Gateway 執行。", + "warning": "此決策會寫入 Run state、approval token 與 audit trail。", + "confirm": "確認核准" + }, + "reject": { + "title": "確認拒絕", + "body": "拒絕後,Run 會被取消,不會繼續自動執行。", + "reason": "拒絕原因", + "placeholder": "請輸入拒絕原因...", + "warning": "拒絕原因會寫入 audit trail,供後續稽核與 Run Timeline 回看。", + "confirm": "確認拒絕" + } + } } } } diff --git a/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx b/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx index e9165bd4..72e9997d 100644 --- a/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx +++ b/apps/web/src/app/[locale]/awooop/approvals/[run_id]/page.tsx @@ -1,231 +1,190 @@ // ============================================================================= -// WOOO AIOps - AwoooP M8 審批決策頁面 +// WOOO AIOps - AwoooP Approval Decision // ============================================================================= -// 顯示 Run 詳情,Approve / Reject 各需 Dialog 確認 -// Reject 需填寫原因 +// 人工審批必須回到 Run State / Timeline,避免 Approval 成為獨立孤島。 "use client"; -import { useState, useEffect, useCallback } from "react"; -import { useRouter, Link } from "@/i18n/routing"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { useLocale, useTranslations } from "next-intl"; import { - ShieldCheck, AlertCircle, - CheckCircle, - XCircle, ArrowLeft, + ArrowRight, + CheckCircle, + Clock, RefreshCw, + ShieldCheck, X, + XCircle, } from "lucide-react"; -import { cn } from "@/lib/utils"; -// ============================================================================= -// Types -// ============================================================================= +import { Link, useRouter } from "@/i18n/routing"; +import { cn } from "@/lib/utils"; interface RunDetail { run_id: string; project_id: string; agent_id: string; state: string; + trace_id?: string | null; + trigger_type?: string | null; + trigger_ref?: string | null; + cost_usd?: number | string; + attempt_count?: number; + max_attempts?: number; + error_code?: string | null; + error_detail?: string | null; created_at: string; timeout_at?: string | null; } -// ============================================================================= -// 常數 -// ============================================================================= +interface RunDetailResponse { + run: RunDetail; +} const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ""; -// ============================================================================= -// Sub Components -// ============================================================================= +const STATE_STYLE: Record = { + waiting_approval: "border-[#d9b36f] bg-[#fff7e8] text-[#8a5a08]", + running: "border-[#9bb6d9] bg-[#eef5ff] text-[#1f5b9b]", + completed: "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]", + cancelled: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", + failed: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", + timeout: "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]", +}; + +function formatDate(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", + minute: "2-digit", + second: "2-digit", + }); +} + +function runTimelineHref(runId: string, projectId?: string | null) { + const query = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""; + return `/awooop/runs/${runId}${query}` as never; +} function DetailRow({ label, value, + emptyLabel, }: { label: string; - value: string | React.ReactNode; + value?: string | number | null | React.ReactNode; + emptyLabel: string; }) { return ( -
- - {label} - - {value} +
+
{label}
+
+ {value ?? emptyLabel} +
); } -// Approve 確認 Dialog -function ApproveDialog({ +function DecisionDialog({ + decision, runId, - onConfirm, - onCancel, loading, -}: { - runId: string; - onConfirm: () => void; - onCancel: () => void; - loading: boolean; -}) { - return ( -
-
- {/* Header */} -
-
-
-
-

- 確認核准 -

-
- -
- - {/* Body */} -
-

確定要核准此 Run?

-
- - Run ID:{" "} - {runId.slice(0, 8)} - -
-

- 核准後,Run 將繼續執行後續步驟。此操作無法撤銷。 -

-
- - {/* Footer */} -
- - -
-
-
- ); -} - -// Reject 確認 Dialog(需填原因) -function RejectDialog({ - runId, - onConfirm, onCancel, - loading, + onConfirm, }: { + decision: "approve" | "reject"; runId: string; - onConfirm: (reason: string) => void; - onCancel: () => void; loading: boolean; + onCancel: () => void; + onConfirm: (reason?: string) => void; }) { + const t = useTranslations("awooop.approvalDecision.dialog"); const [reason, setReason] = useState(""); + const isReject = decision === "reject"; + const titleId = `${decision}-dialog-title`; + const tone = isReject + ? "border-[#e2a29b] bg-[#fff0ef] text-[#9f2f25]" + : "border-[#9bc7a4] bg-[#f0faf2] text-[#17602a]"; + const Icon = isReject ? XCircle : CheckCircle; return (
-
- {/* Header */} -
+
+
-
-
-

- 確認拒絕 + + +

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

- {/* Body */} -
-
- - Run ID:{" "} - {runId.slice(0, 8)} +
+

+ {t(`${decision}.body` as never)} +

+
+ + {t("runId")} {runId.slice(0, 8)}
-
-