feat(incident): add FlowPipeline 7-node pipeline with lobster animation

This commit is contained in:
OG T
2026-04-01 19:47:19 +08:00
parent c1c7564e41
commit 16ca133955

View File

@@ -0,0 +1,196 @@
'use client'
/**
* FlowPipeline - AI中心 v6 事件流程管線
* ========================================
* 7節點水平管線告警→AI偵測→AI分析→提案生成→等待授權→執行→完成
* Q版龍蝦 SVG進行中=橘紅 #E85530完成=綠色 #22C55E
* lobster-bob CSS 動畫:上下浮動 4px1.5s loop
*/
export type FlowStage =
| 'alert'
| 'detection'
| 'analysis'
| 'proposal'
| 'approval'
| 'execution'
| 'resolved'
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 資訊(可選)*/
tooltips?: Partial<Record<FlowStage, string>>
}
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' },
]
/** 根據 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)
if (nodeIndex < activeIndex) return 'done'
if (nodeIndex === activeIndex) return 'active'
return 'pending'
}
/** Q版龍蝦 SVG (18×20) */
function LobsterSVG({ color }: { color: string }) {
return (
<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
{/* 身體 */}
<ellipse cx="9" cy="13" rx="5.5" ry="6.5" fill={color} />
{/* 頭部 */}
<circle cx="9" cy="7.5" r="4.5" fill={color} />
{/* 眼睛 */}
<circle cx="7" cy="6.5" r="1" fill="#b03a1a" />
<circle cx="11" cy="6.5" r="1" fill="#b03a1a" />
{/* 左鉗 */}
<path d="M3.5 10 Q1 9 1.5 12 Q2 14 4 13" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
<ellipse cx="1.5" cy="12" rx="1.2" ry="1.5" fill={color} transform="rotate(-10 1.5 12)"/>
{/* 右鉗 */}
<path d="M14.5 10 Q17 9 16.5 12 Q16 14 14 13" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
<ellipse cx="16.5" cy="12" rx="1.2" ry="1.5" fill={color} transform="rotate(10 16.5 12)"/>
{/* 觸鬚 */}
<path d="M7 3 Q5 1 3 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
<path d="M11 3 Q13 1 15 2" stroke="#b03a1a" strokeWidth="0.8" fill="none" strokeLinecap="round"/>
{/* 尾部 */}
<path d="M6 19 Q9 21 12 19" stroke={color} strokeWidth="1.2" fill="none" strokeLinecap="round"/>
</svg>
)
}
export function FlowPipeline({ activeStage, isResolved = false, tooltips }: FlowPipelineProps) {
const lobsterColor = isResolved ? '#22C55E' : '#E85530'
return (
<div className="flow-pipeline-wrapper" style={{ padding: '4px 10px 8px', overflowX: 'auto' }}>
{/* CSS keyframes via style tag */}
<style>{`
@keyframes lobster-bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.lobster-bob { animation: lobster-bob 1.5s ease-in-out infinite; }
.flow-tooltip {
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: #141413;
color: #f5f4ed;
font-size: 9px;
padding: 3px 7px;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 10;
}
.flow-node-wrap:hover .flow-tooltip { opacity: 1; }
`}</style>
<div style={{ display: 'flex', alignItems: 'center', gap: 0, minWidth: 'max-content' }}>
{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 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 (
<div key={node.id} style={{ display: 'flex', alignItems: 'center' }}>
<div
className="flow-node-wrap"
style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2 }}
>
{/* Q版龍蝦僅 active 節點顯示)*/}
<div style={{ height: 22, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
{isActive && (
<div className={isResolved ? '' : 'lobster-bob'}>
<LobsterSVG color={lobsterColor} />
</div>
)}
</div>
{/* 節點圓點 */}
<div style={{
width: 8, height: 8,
borderRadius: '50%',
background: dotBg,
border: dotBorder,
boxShadow: dotShadow,
flexShrink: 0,
}} />
{/* 節點標籤 */}
<span style={{
fontSize: 8,
color: isActive ? '#141413' : isDone || isResNode ? '#E85530' : '#b0ad9f',
fontWeight: isActive ? 700 : 400,
marginTop: 2,
whiteSpace: 'nowrap',
}}>
{node.label}
</span>
{/* Tooltip */}
{tooltips?.[node.id] && (
<div className="flow-tooltip">{tooltips[node.id]}</div>
)}
</div>
{/* 連接線(最後一個節點後不需要)*/}
{!isLast && (
<div style={{
width: 16, height: 1,
background: connColor,
flexShrink: 0,
marginBottom: 18, // 對齊圓點位置(跳過 lobster 空間 + 標籤)
}} />
)}
</div>
)
})}
</div>
</div>
)
}
export default FlowPipeline