From 9e97bdb9587d1801bda22c255e123d097e5cbc0f Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 18 Jun 2026 11:22:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E8=A6=96=E8=A6=BA=E5=8C=96=20Awoo?= =?UTF-8?q?oP=20=E6=93=8D=E4=BD=9C=E6=B1=BA=E7=AD=96=E5=9C=96=E8=AD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/messages/en.json | 48 +++++ apps/web/messages/zh-TW.json | 48 +++++ apps/web/src/app/[locale]/awooop/page.tsx | 211 ++++++++++++++++++++++ 3 files changed, 307 insertions(+) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 5de75a7ff..0e4c7eb04 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -6937,6 +6937,54 @@ "sourceReviewsDetail": "{count} 個 來源 review 待決策" } }, + "visualOps": { + "title": "操作決策圖譜", + "subtitle": "先看來源、證據、Gate、驗證與學習是否連成閉環,再往下鑽完整證據。", + "status": { + "ready": "資料可判讀", + "degraded": "資料不完整" + }, + "flow": { + "ingest": { + "label": "告警 / 來源", + "detail": "Run 連結 {linked}" + }, + "evidence": { + "label": "證據鏈", + "detail": "來源事件是否回到 Run / Incident" + }, + "gate": { + "label": "人工 Gate", + "detail": "待處理工作 {work}" + }, + "verify": { + "label": "修復驗證", + "detail": "失敗修復 {failed}" + }, + "learn": { + "label": "學習回寫", + "detail": "綠燈品質占比" + } + }, + "matrix": { + "source": { + "label": "來源到 Run", + "detail": "來源證據是否已能追蹤" + }, + "work": { + "label": "工作項清理", + "detail": "尚有 {count} 組待處理" + }, + "repair": { + "label": "修復閉環", + "detail": "已驗證自動修復占比" + }, + "quality": { + "label": "品質分布", + "detail": "紅 {red} / 黃 {yellow}" + } + } + }, "quality": { "title": "自動化品質", "subtitle": "最近 24 小時告警是否真正走到 AI 自動修復、驗證與學習回寫。", diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json index 5de75a7ff..0e4c7eb04 100644 --- a/apps/web/messages/zh-TW.json +++ b/apps/web/messages/zh-TW.json @@ -6937,6 +6937,54 @@ "sourceReviewsDetail": "{count} 個 來源 review 待決策" } }, + "visualOps": { + "title": "操作決策圖譜", + "subtitle": "先看來源、證據、Gate、驗證與學習是否連成閉環,再往下鑽完整證據。", + "status": { + "ready": "資料可判讀", + "degraded": "資料不完整" + }, + "flow": { + "ingest": { + "label": "告警 / 來源", + "detail": "Run 連結 {linked}" + }, + "evidence": { + "label": "證據鏈", + "detail": "來源事件是否回到 Run / Incident" + }, + "gate": { + "label": "人工 Gate", + "detail": "待處理工作 {work}" + }, + "verify": { + "label": "修復驗證", + "detail": "失敗修復 {failed}" + }, + "learn": { + "label": "學習回寫", + "detail": "綠燈品質占比" + } + }, + "matrix": { + "source": { + "label": "來源到 Run", + "detail": "來源證據是否已能追蹤" + }, + "work": { + "label": "工作項清理", + "detail": "尚有 {count} 組待處理" + }, + "repair": { + "label": "修復閉環", + "detail": "已驗證自動修復占比" + }, + "quality": { + "label": "品質分布", + "detail": "紅 {red} / 黃 {yellow}" + } + } + }, "quality": { "title": "自動化品質", "subtitle": "最近 24 小時告警是否真正走到 AI 自動修復、驗證與學習回寫。", diff --git a/apps/web/src/app/[locale]/awooop/page.tsx b/apps/web/src/app/[locale]/awooop/page.tsx index 60e486dc6..1624759d3 100644 --- a/apps/web/src/app/[locale]/awooop/page.tsx +++ b/apps/web/src/app/[locale]/awooop/page.tsx @@ -614,6 +614,209 @@ function ProgressRow({ ); } +function clampPercent(value: number): number { + if (!Number.isFinite(value)) return 0; + return Math.max(0, Math.min(100, value)); +} + +function VisualFlowNode({ + label, + value, + detail, + icon: Icon, + tone, +}: { + label: string; + value: string | number; + detail: string; + icon: typeof Activity; + tone: "good" | "warn" | "neutral"; +}) { + return ( +
+
+
+

{label}

+

+ {value} +

+
+ + +
+

{detail}

+
+ ); +} + +function VisualGateCell({ + label, + value, + detail, + percent, + tone, +}: { + label: string; + value: string; + detail: string; + percent: number; + tone: "good" | "warn" | "neutral"; +}) { + return ( +
+
+
+

{label}

+

{detail}

+
+ {value} +
+
+
+
+
+ ); +} + +function OperationsDecisionMap({ + snapshot, + sourceFlow, + sourceError, + quality, + qualityError, +}: { + snapshot: Snapshot; + sourceFlow: SourceFlowSummary | null; + sourceError: boolean; + quality: AutomationQualitySummary | null; + qualityError: boolean; +}) { + const t = useTranslations("awooop.home.visualOps"); + const sourceTotal = sourceFlow?.source_event_total ?? 0; + const linkedRuns = sourceFlow?.linked_run_total ?? 0; + const openWork = sourceFlow?.open_work_item_group_total ?? 0; + const manualGates = sourceFlow?.manual_gate_group_total ?? 0; + const failedRepairs = sourceFlow?.failed_repair_group_total ?? 0; + const evaluated = quality?.evaluated_total ?? 0; + const verified = quality?.verified_auto_repair_total ?? 0; + const red = quality?.score_buckets.red ?? 0; + const yellow = quality?.score_buckets.yellow ?? 0; + const green = quality?.score_buckets.green ?? 0; + const linkedPercent = percentValue(linkedRuns, sourceTotal); + const workClearPercent = percentValue(Math.max(0, (sourceFlow?.recurrence_group_total ?? 0) - openWork), sourceFlow?.recurrence_group_total ?? 0); + const verifiedPercent = percentValue(verified, evaluated); + const qualityHealthyPercent = percentValue(green, green + yellow + red); + const isDegraded = sourceError || qualityError; + + return ( +
+
+
+
+ + {isDegraded ? t("status.degraded") : t("status.ready")} + +
+ +
+ 0 ? "good" : "neutral"} + /> + = 80 ? "good" : linkedPercent > 0 ? "warn" : "neutral"} + /> + 0 ? "warn" : "good"} + /> + = 80 ? "good" : evaluated > 0 ? "warn" : "neutral"} + /> + = 80 ? "good" : green + yellow + red > 0 ? "warn" : "neutral"} + /> +
+ +
+ = 80 ? "good" : "warn"} + /> + + = 80 ? "good" : "warn"} + /> + +
+
+ ); +} + function SourceFlowActionLink({ href, label, @@ -1539,6 +1742,14 @@ export default function AwoooPPage() { return (
+ +