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>
232 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|