From 93e3aa68117149437810b3aeefd16fcd77416505 Mon Sep 17 00:00:00 2001 From: OG T Date: Thu, 2 Apr 2026 21:18:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=9B=9B=E7=A8=AE=E5=9A=B4?= =?UTF-8?q?=E9=87=8D=E5=BA=A6=E7=AE=A1=E7=B7=9A=E5=8B=95=E7=95=AB=20+=20Wo?= =?UTF-8?q?ooClaw=20=E5=91=BD=E5=90=8D=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - flow-pipeline.tsx: 新增 severity prop,四種管線樣式 - P0 → Style A: 脈衝光波 + 流動光效 (#cc2200) - P1 → Style B: 進度條,龍蝦站在進度端點 (#F59E0B) - P2 → Style C: 卡片步驟,龍蝦浮在 active 卡片上方 (#4A90D9) - P3 → Style D: 時間軸,虛線流動動畫 (#22C55E) - incident-card.tsx: FlowPipeline 傳入 severity={sev} - openclaw-panel.tsx: NemoClaw→WoooClaw, OpenClaw Pipeline→WoooClaw Pipeline Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/components/ai/openclaw-panel.tsx | 4 +- .../src/components/incident/flow-pipeline.tsx | 397 +++++++++++++----- .../src/components/incident/incident-card.tsx | 2 +- 3 files changed, 305 insertions(+), 98 deletions(-) diff --git a/apps/web/src/components/ai/openclaw-panel.tsx b/apps/web/src/components/ai/openclaw-panel.tsx index 6e186dcd7..3531e40e5 100644 --- a/apps/web/src/components/ai/openclaw-panel.tsx +++ b/apps/web/src/components/ai/openclaw-panel.tsx @@ -229,7 +229,7 @@ export function OpenClawPanel({ {/* 右側文字 */}
- NemoClaw + WoooClaw
{tPanel(status)} @@ -241,7 +241,7 @@ export function OpenClawPanel({ borderRadius: 10, color: '#4A90D9', }}> - OpenClaw Pipeline + WoooClaw Pipeline
diff --git a/apps/web/src/components/incident/flow-pipeline.tsx b/apps/web/src/components/incident/flow-pipeline.tsx index 00c4bd201..01c040d66 100644 --- a/apps/web/src/components/incident/flow-pipeline.tsx +++ b/apps/web/src/components/incident/flow-pipeline.tsx @@ -4,8 +4,12 @@ * FlowPipeline - AI中心 v6 事件流程管線 * ======================================== * 7節點水平管線:告警→AI偵測→AI分析→提案生成→等待授權→執行→完成 - * Q版龍蝦 SVG:進行中=橘紅 #E85530,完成=綠色 #22C55E - * lobster-bob CSS 動畫:上下浮動 4px,1.5s loop + * 4種嚴重度對應不同管線樣式: + * P0 → Style A: 脈衝光波 (紅 #cc2200) + * P1 → Style B: 進度條 (橘 #F59E0B) + * P2 → Style C: 卡片步驟 (藍 #4A90D9) + * P3 → Style D: 時間軸 (綠 #22C55E) + * 2026-04-02 Claude Code: 新增 severity prop + 四種管線渲染 */ export type FlowStage = @@ -22,167 +26,278 @@ export type NodeStatus = 'done' | 'active' | 'pending' | 'resolved' interface FlowNode { id: FlowStage label: string - labelKey: string } export interface FlowPipelineProps { - /** 目前進行到哪個 stage */ activeStage: FlowStage - /** 是否已全部完成(resolved 狀態,綠色龍蝦靜止)*/ isResolved?: boolean - /** 各節點 tooltip 資訊(可選)*/ + severity?: 'P0' | 'P1' | 'P2' | 'P3' tooltips?: Partial> } const FLOW_NODES: FlowNode[] = [ - { id: 'alert', label: '告警', labelKey: 'flow.alert' }, - { id: 'detection', label: 'AI偵測', labelKey: 'flow.detection' }, - { id: 'analysis', label: 'AI分析', labelKey: 'flow.analysis' }, - { id: 'proposal', label: '提案生成', labelKey: 'flow.proposal' }, - { id: 'approval', label: '等待授權', labelKey: 'flow.approval' }, - { id: 'execution', label: '執行', labelKey: 'flow.execution' }, - { id: 'resolved', label: '完成', labelKey: 'flow.resolved' }, + { id: 'alert', label: '告警' }, + { id: 'detection', label: 'AI偵測' }, + { id: 'analysis', label: 'AI分析' }, + { id: 'proposal', label: '提案生成' }, + { id: 'approval', label: '等待授權' }, + { id: 'execution', label: '執行' }, + { id: 'resolved', label: '完成' }, ] -/** 根據 activeStage 計算每個節點的狀態 */ +const SEV_COLOR: Record = { + P0: '#cc2200', + P1: '#F59E0B', + P2: '#4A90D9', + P3: '#22C55E', +} + +function getActiveIndex(activeStage: FlowStage): number { + return FLOW_NODES.findIndex(n => n.id === activeStage) +} + function getNodeStatus(nodeId: FlowStage, activeStage: FlowStage, isResolved: boolean): NodeStatus { if (isResolved) return 'resolved' - const stageOrder = FLOW_NODES.map(n => n.id) - const nodeIndex = stageOrder.indexOf(nodeId) - const activeIndex = stageOrder.indexOf(activeStage) + const nodeIndex = FLOW_NODES.findIndex(n => n.id === nodeId) + const activeIndex = getActiveIndex(activeStage) if (nodeIndex < activeIndex) return 'done' if (nodeIndex === activeIndex) return 'active' return 'pending' } -/** Q版龍蝦 SVG (18×20) */ -function LobsterSVG({ color }: { color: string }) { +// ── Q版龍蝦 SVG ────────────────────────────────────────────────────────────── + +function LobsterSVG({ color, size = 18 }: { color: string; size?: number }) { + const h = Math.round(size * 20 / 18) return ( - - {/* 身體 */} + - {/* 頭部 */} - {/* 眼睛 */} - {/* 左鉗 */} - {/* 右鉗 */} - {/* 觸鬚 */} - {/* 尾部 */} ) } -export function FlowPipeline({ activeStage, isResolved = false, tooltips }: FlowPipelineProps) { - const lobsterColor = isResolved ? '#22C55E' : '#E85530' +// ── Style A: P0 脈衝光波 ────────────────────────────────────────────────────── + +function PipelineStyleA({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) { + const color = isResolved ? '#22C55E' : SEV_COLOR.P0 + const activeIdx = getActiveIndex(activeStage) return ( -
- {/* CSS keyframes via style tag */} +
-
{FLOW_NODES.map((node, idx) => { const status = getNodeStatus(node.id, activeStage, isResolved) const isActive = status === 'active' - const isDone = status === 'done' - const isResNode = status === 'resolved' + const isDone = status === 'done' || status === 'resolved' const isLast = idx === FLOW_NODES.length - 1 - // 節點顏色 - const dotBg = isDone || isResNode - ? (isResNode ? '#22C55E' : '#E85530') - : isActive ? '#141413' : '#f8f9fc' - const dotBorder = isActive - ? '1.5px solid #E85530' - : isDone || isResNode - ? 'none' - : '1.5px solid #e0ddd4' - const dotShadow = isActive ? '0 0 0 3px rgba(232,85,48,0.2)' : 'none' - - // 連接線顏色 - const connColor = isDone || isResNode ? 'rgba(232,85,48,0.5)' : '#e0ddd4' - return (
-
- {/* Q版龍蝦(僅 active 節點顯示)*/} +
{isActive && ( -
- +
+
)}
- - {/* 節點圓點 */}
- - {/* 節點標籤 */} {node.label} - - {/* Tooltip */} - {tooltips?.[node.id] && ( -
{tooltips[node.id]}
- )}
+ {!isLast && ( +
+ )} +
+ ) + })} +
+
+ ) +} - {/* 連接線(最後一個節點後不需要)*/} +// ── Style B: P1 進度條 ──────────────────────────────────────────────────────── + +function PipelineStyleB({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) { + const color = isResolved ? '#22C55E' : SEV_COLOR.P1 + const activeIdx = getActiveIndex(activeStage) + const totalNodes = FLOW_NODES.length + const progressPct = isResolved ? 100 : (activeIdx / (totalNodes - 1)) * 100 + + return ( +
+ + {/* 進度條軌道 */} +
+ {/* 底部軌道 */} +
+ {/* 已完成進度 */} +
+ {/* 節點圓點 */} + {FLOW_NODES.map((node, idx) => { + const pct = (idx / (totalNodes - 1)) * 100 + const status = getNodeStatus(node.id, activeStage, isResolved) + const isActive = status === 'active' + const isDone = status === 'done' || status === 'resolved' + + return ( +
+ {/* 龍蝦 (active 節點,站在進度條終點上方) */} + {isActive && ( +
+ +
+ )} + {!isActive &&
} + {/* 圓點 */} +
+ {/* 標籤 */} + + {node.label} + +
+ ) + })} +
+
+ ) +} + +// ── Style C: P2 卡片步驟 ────────────────────────────────────────────────────── + +function PipelineStyleC({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) { + const color = isResolved ? '#22C55E' : SEV_COLOR.P2 + const activeIdx = getActiveIndex(activeStage) + + return ( +
+ +
+ {FLOW_NODES.map((node, idx) => { + const status = getNodeStatus(node.id, activeStage, isResolved) + const isActive = status === 'active' + const isDone = status === 'done' || status === 'resolved' + const isLast = idx === FLOW_NODES.length - 1 + + return ( +
+
+ {/* 龍蝦 */} +
+ {isActive && ( +
+ +
+ )} +
+ {/* 卡片 */} +
+ + {node.label} + +
+
{!isLast && (
)}
@@ -193,4 +308,96 @@ export function FlowPipeline({ activeStage, isResolved = false, tooltips }: Flow ) } +// ── Style D: P3 時間軸 ──────────────────────────────────────────────────────── + +function PipelineStyleD({ activeStage, isResolved }: { activeStage: FlowStage; isResolved: boolean }) { + const color = isResolved ? '#22C55E' : SEV_COLOR.P3 + const activeIdx = getActiveIndex(activeStage) + + return ( +
+ +
+ {FLOW_NODES.map((node, idx) => { + const status = getNodeStatus(node.id, activeStage, isResolved) + const isActive = status === 'active' + const isDone = status === 'done' || status === 'resolved' + const isLast = idx === FLOW_NODES.length - 1 + const isNextActive = idx === activeIdx - 1 + + return ( +
+
+
+ {isActive && ( +
+ +
+ )} +
+
+ + {node.label} + +
+ {!isLast && ( +
+ {/* Active 前的虛線用 SVG 做流動動畫 */} + {isNextActive ? ( + + + + ) : ( +
+ )} +
+ )} +
+ ) + })} +
+
+ ) +} + +// ── Main Component ──────────────────────────────────────────────────────────── + +export function FlowPipeline({ activeStage, isResolved = false, severity = 'P3', tooltips: _tooltips }: FlowPipelineProps) { + // 2026-04-02 Claude Code: severity → pipeline style mapping + // P0=StyleA(脈衝光波) P1=StyleB(進度條) P2=StyleC(卡片步驟) P3=StyleD(時間軸) + if (isResolved || severity === 'P3') { + return + } + if (severity === 'P0') { + return + } + if (severity === 'P1') { + return + } + // P2 + return +} + export default FlowPipeline diff --git a/apps/web/src/components/incident/incident-card.tsx b/apps/web/src/components/incident/incident-card.tsx index e4441ac6a..c04551f87 100644 --- a/apps/web/src/components/incident/incident-card.tsx +++ b/apps/web/src/components/incident/incident-card.tsx @@ -292,7 +292,7 @@ export function IncidentCard({ incident, decision, onApprovalChange }: IncidentC
{/* 流程狀態圖 */} - + {/* Impact 指標列 */}