From e623b1c4e91b1b6e6907ab3081b08099273525d1 Mon Sep 17 00:00:00 2001 From: OG T Date: Wed, 1 Apr 2026 20:10:20 +0800 Subject: [PATCH] chore(incident): remove DualStateIncidentCard (Phase 6.5 old style, migrated to IncidentCard) Co-Authored-By: Claude Sonnet 4.6 --- .../incident/dual-state-incident-card.tsx | 420 ------------------ apps/web/src/components/incident/index.ts | 4 - 2 files changed, 424 deletions(-) delete mode 100644 apps/web/src/components/incident/dual-state-incident-card.tsx diff --git a/apps/web/src/components/incident/dual-state-incident-card.tsx b/apps/web/src/components/incident/dual-state-incident-card.tsx deleted file mode 100644 index 19042c94..00000000 --- a/apps/web/src/components/incident/dual-state-incident-card.tsx +++ /dev/null @@ -1,420 +0,0 @@ -'use client' - -/** - * DualStateIncidentCard - Phase 6.5 雙態戰情室卡片 - * ================================================ - * - * Nothing.tech 視覺憲法: - * - 純白極簡 (bg-white/90) - * - 無深色模式 - * - 嚴禁陰影 (shadow-none) - * - 細邊框 (border-[0.5px]) - * - * 雙態設計: - * - normal: 淺灰邊框,靜態 - * - alert: 紅色邊框,脈衝雷達動畫 - * - * Phase 6.5: 決策令牌驅動 - * - 使用 decision.proposal_data 顯示行動方案 - * - decision.state === 'ready' 時解鎖按鈕 - * - 點擊 Y/n 時先建立 Approval 再簽核 - * - * Phase 6.5c: UX 改善 (2026-03-23) - * - 錯誤訊息明顯顯示 (不只是 hover) - * - 執行超時提示 (30秒) - * - 重試按鈕 - * - * 統帥鐵律: 禁止假數據!UI 永不鎖死! - */ - -import React, { useState, useCallback, useEffect, useRef } from 'react' -import { useTranslations } from 'next-intl' -import type { DecisionInfo } from '@/lib/api-client'; -import { apiClient } from '@/lib/api-client' -import { useCSRF } from '@/hooks/useCSRF' - -type ButtonState = 'idle' | 'loading' | 'approved' | 'rejected' | 'error' | 'timeout' - -const EXECUTION_TIMEOUT_MS = 30000 // 30 秒超時警告 - -export interface DualStateIncidentCardProps { - id: string - serviceName: string - status: 'normal' | 'alert' - tier?: 1 | 2 | 3 - message: string - timestamp: string - /** @deprecated 使用 decision 取代 */ - proposalId?: string - /** Phase 6.5: 決策令牌 (取代 proposalId) */ - decision?: DecisionInfo | null - /** Callback when approval status changes */ - onApprovalChange?: (proposalId: string, newStatus: 'approved' | 'rejected') => void -} - -export const DualStateIncidentCard: React.FC = ({ - id, - serviceName, - status, - tier, - message, - timestamp, - proposalId, - decision, - onApprovalChange, -}) => { - const t = useTranslations('incident.card') - const { csrfToken } = useCSRF() - const isAlert = status === 'alert' - const [buttonState, setButtonState] = useState('idle') - const [errorMessage, setErrorMessage] = useState(null) - const [currentProposalId, setCurrentProposalId] = useState(proposalId || null) - const timeoutRef = useRef(null) - - // 清理 timeout - useEffect(() => { - return () => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - } - } - }, []) - - // Phase 6.5: 決策令牌驅動 - // 按鈕可用條件: decision.state === 'ready' 或有 proposalId - const isDecisionReady = decision?.state === 'ready' || !!currentProposalId - const isAnalyzing = decision?.state === 'analyzing' - const decisionAction = decision?.proposal_data?.action || '' - const decisionReasoning = decision?.proposal_data?.reasoning || '' - - /** - * 處理簽核 (Y 按鈕) - * Phase 6.5: 決策令牌驅動 - * 1. 如果沒有 proposalId,先從 incident 生成 proposal - * 2. 然後簽核該 proposal - */ - const handleApprove = useCallback(async () => { - console.log('🚀 指令授權中:', { proposalId: currentProposalId, decision: decision?.token }) - - // Phase 6.5: 決策必須就緒 - if (!isDecisionReady || buttonState === 'loading') { - console.warn('⚠️ 授權阻斷: isDecisionReady=', isDecisionReady, 'buttonState=', buttonState) - return - } - - // 關鍵: 立即進入 loading,消除 300ms 感知延遲 - setButtonState('loading') - setErrorMessage(null) - - // Phase 6.5c: 30 秒超時警告 (如果未被清除則觸發) - if (timeoutRef.current) clearTimeout(timeoutRef.current) - timeoutRef.current = setTimeout(() => { - setButtonState('timeout') - setErrorMessage(t('timeoutMessage')) - }, EXECUTION_TIMEOUT_MS) - - try { - let approvalId = currentProposalId - - // Step 1: 如果沒有 proposalId,先從 incident 生成 - if (!approvalId && decision?.token) { - console.log('📝 生成 Proposal from incident:', id) - const proposalResult = await apiClient.generateProposal(id) - - if (!proposalResult.success || !proposalResult.proposal) { - throw new Error(proposalResult.message || 'Failed to generate proposal') - } - - approvalId = proposalResult.proposal.id - setCurrentProposalId(approvalId) - console.log('✅ Proposal 生成成功:', approvalId) - } - - if (!approvalId) { - throw new Error('No approval ID available') - } - - // Step 2: 簽核 (Phase 22 P0: 帶入 CSRF token) - const result = await apiClient.signApproval(approvalId, 'commander', 'Authorized via WarRoom', csrfToken) - console.log('✅ 簽核回應:', result) - - // 清除超時計時器 - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - - // 🔧 Fix: 正確檢查 result.approval.status (非 result.status) - const approvalStatus = result.approval?.status?.toLowerCase() - if (approvalStatus === 'approved') { - setButtonState('approved') - console.log('🎯 授權成功,觸發 onApprovalChange') - onApprovalChange?.(approvalId, 'approved') - } else { - // Multi-sig: 還需要更多簽核 - const current = result.approval?.current_signatures ?? result.current_signatures ?? 0 - const required = result.approval?.required_signatures ?? result.required_signatures ?? 1 - console.log('🔐 Multi-sig 等待中,目前簽核:', current, '/', required) - setButtonState('idle') - } - } catch (error) { - // 清除超時計時器 - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - - console.error('❌ 授權失敗:', error) - setButtonState('error') - // Phase 6.5c: 更清楚的錯誤訊息 - const errMsg = error instanceof Error ? error.message : String(error) - setErrorMessage(errMsg) - // 不自動恢復,讓用戶看到錯誤並主動點擊重試 - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- t is stable from next-intl - }, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange, csrfToken]) - - /** - * 處理拒絕 (n 按鈕) - * Phase 6.5: 決策令牌驅動 - */ - const handleReject = useCallback(async () => { - console.log('🛑 指令拒絕中:', { proposalId: currentProposalId, decision: decision?.token }) - - if (!isDecisionReady || buttonState === 'loading') { - console.warn('⚠️ 拒絕阻斷: isDecisionReady=', isDecisionReady, 'buttonState=', buttonState) - return - } - - setButtonState('loading') - setErrorMessage(null) - - // 超時計時器 (如果未被清除則觸發) - if (timeoutRef.current) clearTimeout(timeoutRef.current) - timeoutRef.current = setTimeout(() => { - setButtonState('timeout') - setErrorMessage(t('timeoutMessage')) - }, EXECUTION_TIMEOUT_MS) - - try { - let approvalId = currentProposalId - - // 如果沒有 proposalId,先生成再拒絕 - if (!approvalId && decision?.token) { - const proposalResult = await apiClient.generateProposal(id) - if (proposalResult.success && proposalResult.proposal) { - approvalId = proposalResult.proposal.id - setCurrentProposalId(approvalId) - } - } - - if (!approvalId) { - throw new Error('No approval ID available') - } - - await apiClient.rejectApproval(approvalId, 'Rejected via WarRoom', csrfToken) - - // 清除超時計時器 - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - - console.log('✅ 拒絕成功') - setButtonState('rejected') - onApprovalChange?.(approvalId, 'rejected') - } catch (error) { - // 清除超時計時器 - if (timeoutRef.current) { - clearTimeout(timeoutRef.current) - timeoutRef.current = null - } - - console.error('❌ 拒絕失敗:', error) - setButtonState('error') - const errMsg = error instanceof Error ? error.message : String(error) - setErrorMessage(errMsg) - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- t is stable from next-intl - }, [currentProposalId, decision, id, isDecisionReady, buttonState, onApprovalChange, csrfToken]) - - /** - * 渲染決策按鈕區塊 - */ - const renderActionButton = () => { - switch (buttonState) { - case 'loading': - return ( - - - {t('executing')} - - ) - case 'approved': - return ( - - {t('approved')} - - ) - case 'rejected': - return ( - - {t('rejected')} - - ) - case 'error': - return ( -
-
- - {t('error')} - - -
- {errorMessage && ( - - {errorMessage.length > 40 ? errorMessage.slice(0, 40) + '...' : errorMessage} - - )} -
- ) - case 'timeout': - return ( -
-
- - {t('timeout')} - - -
- - {t('checkApiLogs')} - -
- ) - default: - return ( -
- - / - - {isAnalyzing && ( - - )} - {decision?.proposal_data?.source && ( - - [{decision.proposal_data.source.includes('llm') ? 'AI' : 'EXP'}] - - )} -
- ) - } - } - - return ( -
- {/* 異常脈衝雷達 (Ping Animation) */} - {isAlert && ( - - - - - )} - - {/* 標頭資訊 */} -
- {id} - - {serviceName} - -
- - {/* 核心數據與訊息 */} -
- {message} -
-
{timestamp}
- - {/* 大腦決策層 - Phase 6.5 決策令牌驅動 */} - {isAlert && tier && ( -
- {/* 決策狀態 */} -
- - {tier === 1 ? ( - t('aiExecuting') - ) : isAnalyzing ? ( - t('brainAnalyzing') - ) : isDecisionReady ? ( - t('decisionReady', { tier }) - ) : ( - t('waitingCommander', { tier }) - )} - - {tier > 1 && renderActionButton()} -
- - {/* Phase 6.5: 顯示 AI 建議行動 */} - {decisionAction && tier > 1 && ( -
-
{t('suggestedAction')}
-
{decisionAction}
- {decisionReasoning && ( -
- 💡 {decisionReasoning.slice(0, 100)}{decisionReasoning.length > 100 ? '...' : ''} -
- )} -
- )} -
- )} -
- ) -} - -export default DualStateIncidentCard diff --git a/apps/web/src/components/incident/index.ts b/apps/web/src/components/incident/index.ts index 4d9c5f53..61c17e1a 100644 --- a/apps/web/src/components/incident/index.ts +++ b/apps/web/src/components/incident/index.ts @@ -3,10 +3,6 @@ */ export { IncidentCard } from './incident-card' -export { - DualStateIncidentCard, - type DualStateIncidentCardProps, -} from './dual-state-incident-card' export { ThinkingTerminal, DEMO_DECISION_CHAIN,