Files
awoooi/apps/web/src/components/charts/ai-process-stepper.tsx
OG T 246587a401 fix(web): Sprint F 前端打假行動 — 29處假數據全面清除 (首席架構師 98/100)
P0: Neural Command 三個子組件移除所有 MOCK 常數,接上真實 API props
- NeuralLiveCenter: 假歷史/假KPI/假雷達 → 從 stats/history/incidents 即時計算
- NeuralStats: MOCK_HISTORY/SCHEME_STATS/PLAYBOOK_RANKINGS → useMemo 聚合
- NeuralApprovalPanel: MOCK_PENDING → 真實 /api/v1/approvals 簽核操作

P1: 10+處假用戶身份 (demo-user/user-001/War Room User) → CURRENT_USER 常數統一
P2: 刪除 6 個 Demo 匯出 (GlobalPulseChartDemo/MOCK_APPROVAL/DEMO_DECISION_CHAIN)
P3: /demo 頁面加 NEXT_PUBLIC_ENABLE_DEMO 環境變數保護
i18n: 新增 22 個翻譯鍵 (zh-TW + en)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 12:53:52 +08:00

232 lines
6.2 KiB
TypeScript

'use client'
/**
* AIProcessStepper - AI 決策流程步進器
* =====================================
* Phase 7: 視覺主權重構
*
* Nothing.tech 設計語言:
* - 極簡步進設計
* - 點陣式進度指示
* - 白玻璃層次感
*
* Features:
* - 5 步 AI 決策流程可視化
* - 當前步驟動畫高亮
* - 狀態顏色區分 (thinking/success/error)
*/
import { cn } from '@/lib/utils'
import { useTranslations } from 'next-intl'
import {
Search,
Brain,
Shield,
Play,
CheckCircle2,
Loader2,
XCircle,
type LucideIcon,
} from 'lucide-react'
// =============================================================================
// Types
// =============================================================================
export type StepStatus = 'pending' | 'active' | 'complete' | 'error'
export interface ProcessStep {
id: string
label: string
description?: string
status: StepStatus
duration?: string
}
export interface AIProcessStepperProps {
steps: ProcessStep[]
currentStep?: number
className?: string
}
// =============================================================================
// Step Icons
// =============================================================================
const stepIcons: Record<string, LucideIcon> = {
gather: Search,
analyze: Brain,
approve: Shield,
execute: Play,
verify: CheckCircle2,
}
// =============================================================================
// Status Config (Nothing.tech palette)
// =============================================================================
const statusConfig = {
pending: {
bg: 'bg-nothing-gray-100',
border: 'border-nothing-gray-200',
text: 'text-nothing-gray-400',
icon: 'text-nothing-gray-400',
line: 'bg-nothing-gray-200',
},
active: {
bg: 'bg-status-thinking/10',
border: 'border-status-thinking/30',
text: 'text-status-thinking',
icon: 'text-status-thinking',
line: 'bg-status-thinking/30',
},
complete: {
bg: 'bg-green-50',
border: 'border-green-200',
text: 'text-green-700',
icon: 'text-green-600',
line: 'bg-green-400',
},
error: {
bg: 'bg-red-50',
border: 'border-red-200',
text: 'text-red-700',
icon: 'text-red-600',
line: 'bg-red-400',
},
}
// =============================================================================
// Step Component
// =============================================================================
interface StepProps {
step: ProcessStep
index: number
isLast: boolean
}
function Step({ step, index, isLast }: StepProps) {
const config = statusConfig[step.status]
const Icon = stepIcons[step.id] || CheckCircle2
return (
<div className="flex items-start gap-4">
{/* Step Circle & Connector */}
<div className="flex flex-col items-center">
{/* Circle */}
<div
className={cn(
'relative flex items-center justify-center',
'w-10 h-10 rounded-full border-2 transition-all duration-300',
config.bg,
config.border
)}
>
{step.status === 'active' ? (
<Loader2 className={cn('w-5 h-5 animate-spin', config.icon)} />
) : step.status === 'error' ? (
<XCircle className={cn('w-5 h-5', config.icon)} />
) : step.status === 'complete' ? (
<CheckCircle2 className={cn('w-5 h-5', config.icon)} />
) : (
<Icon className={cn('w-5 h-5', config.icon)} />
)}
{/* Pulse Animation for Active */}
{step.status === 'active' && (
<div className="absolute inset-0 rounded-full border-2 border-status-thinking animate-ping opacity-20" />
)}
</div>
{/* Connector Line */}
{!isLast && (
<div
className={cn(
'w-0.5 h-12 mt-2 transition-colors duration-300',
step.status === 'complete' ? config.line : 'bg-nothing-gray-200'
)}
/>
)}
</div>
{/* Step Content */}
<div className="flex-1 pt-1.5 pb-6">
<div className="flex items-center gap-2">
<span
className={cn(
'text-xs font-body uppercase tracking-wider',
config.text
)}
>
Step {index + 1}
</span>
{step.duration && (
<span className="text-xs text-nothing-gray-400">
{step.duration}
</span>
)}
</div>
<h3
className={cn(
'text-base font-medium mt-0.5 transition-colors duration-300',
step.status === 'active'
? 'text-nothing-gray-900'
: step.status === 'pending'
? 'text-nothing-gray-400'
: 'text-nothing-gray-700'
)}
>
{step.label}
</h3>
{step.description && (
<p className="text-sm text-nothing-gray-500 mt-1">{step.description}</p>
)}
</div>
</div>
)
}
// =============================================================================
// Main Component
// =============================================================================
export function AIProcessStepper({ steps, className }: AIProcessStepperProps) {
const t = useTranslations('ai')
return (
<div
className={cn(
'p-6 rounded-xl border',
'bg-white/70 backdrop-blur-[16px]',
'border-nothing-gray-200/50',
className
)}
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-heading font-semibold text-nothing-gray-900">
{t('processFlow')}
</h2>
<div className="flex items-center gap-1.5">
<div className="w-1.5 h-1.5 rounded-full bg-status-thinking animate-pulse" />
<span className="text-xs text-nothing-gray-500">{t('processing')}</span>
</div>
</div>
{/* Steps */}
<div className="space-y-0">
{steps.map((step, index) => (
<Step
key={step.id}
step={step}
index={index}
isLast={index === steps.length - 1}
/>
))}
</div>
</div>
)
}