'use client' /** * ThinkingTerminal - AI 思考流終端機 (Phase 4 升級版) * * 顯示 OpenClaw 的思考過程,Nothing.tech 終端機風格 * 支援 GraphRAG (Blast Radius / Root Cause) 視覺化 * 支援 FinOps 成本分析視覺化 */ import { useEffect, useMemo } from 'react' import { cn } from '@/lib/utils' import { useAgentStore, selectThinkingStream, selectAgentStatus, selectError, type ThinkingStep, } from '@/stores/agent.store' interface ThinkingTerminalProps { className?: string maxHeight?: string } // ==================== GraphRAG 關鍵字偵測 ==================== const GRAPH_RAG_KEYWORDS = { blast_radius: ['分析爆炸半徑', 'blast radius', 'affected services', '影響範圍'], root_cause: ['找到根本原因', 'root cause', 'probable root', '根本原因分析'], } function detectGraphRAGType(content: string): 'blast_radius' | 'root_cause' | null { const lowerContent = content.toLowerCase() for (const keyword of GRAPH_RAG_KEYWORDS.blast_radius) { if (lowerContent.includes(keyword.toLowerCase())) return 'blast_radius' } for (const keyword of GRAPH_RAG_KEYWORDS.root_cause) { if (lowerContent.includes(keyword.toLowerCase())) return 'root_cause' } return null } // ==================== 依賴路徑視覺化 ==================== function DependencyPathVisualizer({ paths, direction, }: { paths: string[] direction: 'upstream' | 'downstream' }) { if (paths.length === 0) return null return (
{direction === 'upstream' ? '[ BLAST RADIUS ]' : '[ ROOT CAUSE CHAIN ]'}
{paths.map((path, i) => (
{i === 0 ? '>' : '|'} {path}
))}
) } function ServiceChainVisualizer({ services, target, type, }: { services: string[] target: string type: 'blast_radius' | 'root_cause' }) { // 建構 ASCII 風格的依賴圖 const isBlastRadius = type === 'blast_radius' return (
{isBlastRadius ? '[ UPSTREAM IMPACT ]' : '[ DOWNSTREAM DEPENDENCIES ]'}
{/* ASCII Art 風格圖形 */}
{isBlastRadius ? ( // Blast Radius: 向上展示誰會受影響 <>
{' ┌─────────────────────┐'}
{' │ '}{services.slice(0, 3).join(', ').padEnd(19)}{'│'}
{' └─────────┬───────────┘'}
{' │ depends on'}
{' ▼'}
{' ┌─────────────────────┐'}
{' │ '}{target.padEnd(19)}{'│ '}X
{' └─────────────────────┘'}
) : ( // Root Cause: 向下展示依賴誰 <>
{' ┌─────────────────────┐'}
{' │ '}{target.padEnd(19)}{'│ '}!
{' └─────────┬───────────┘'}
{' │ calls'}
{' ▼'}
{' ┌─────────────────────┐'}
{' │ '}{services.slice(0, 3).join(', ').padEnd(19)}{'│ '}X
{' └─────────────────────┘'}
)}
{/* 詳細清單 */} {services.length > 0 && (
{services.map((svc, i) => ( {svc} ))}
)}
) } // ==================== FinOps 視覺化 ==================== function FinOpsVisualizer({ data }: { data: NonNullable }) { return (
[ FINOPS ANALYSIS ]
{/* 成本摘要 */}
${data.totalWastedUsd.toFixed(0)}
Wasted/mo
${data.realizableSavingsUsd.toFixed(0)}
Realizable
${data.freedResourcesUsd.toFixed(0)}
Freed
{/* Top Actions */} {data.topActions.length > 0 && (
{data.topActions.slice(0, 3).map((action, i) => (
{action.action} -${action.savings.toFixed(0)}
))}
)}
) } // ==================== Step Renderer ==================== function ThinkingStepRenderer({ step }: { step: ThinkingStep }) { // 偵測 GraphRAG 相關內容 const detectedType = useMemo(() => detectGraphRAGType(step.content), [step.content]) // 基礎渲染 const baseContent = (
[{step.type.toUpperCase()}] {step.content}
) // GraphRAG 結構化資料渲染 if (step.graphData) { const { analysisType, targetService, affectedServices, probableRootCauses, criticalPath } = step.graphData return (
{baseContent} {analysisType === 'blast_radius' && affectedServices && ( )} {analysisType === 'root_cause' && probableRootCauses && ( )} {criticalPath && criticalPath.length > 0 && ( )}
) } // FinOps 結構化資料渲染 if (step.finopsData) { return (
{baseContent}
) } // 關鍵字偵測 (無結構化資料時的 fallback) if (detectedType && !step.graphData) { // 從文字內容嘗試解析服務名稱 const servicePattern = /([a-z]+-[a-z]+(-[a-z]+)?)/gi const matches = step.content.match(servicePattern) || [] if (matches.length > 0) { return (
{baseContent}
) } } return baseContent } export function ThinkingTerminal({ className, maxHeight = '300px', }: ThinkingTerminalProps) { const thinkingStream = useAgentStore(selectThinkingStream) const status = useAgentStore(selectAgentStatus) const error = useAgentStore(selectError) const startThinkingStream = useAgentStore((s) => s.startThinkingStream) const stopThinkingStream = useAgentStore((s) => s.stopThinkingStream) const isStreaming = status === 'thinking' // Cleanup on unmount useEffect(() => { return () => { stopThinkingStream() } }, [stopThinkingStream]) return (
{/* Header */}
{/* Terminal Icon */}

AWOOOI Terminal

v0.1.0 | SSE
{/* Control Button */} {/* Terminal Output */}
{thinkingStream.length === 0 && !isStreaming && !error && (
{'>'} Waiting for command...
)} {thinkingStream.map((step, index) => ( ))} {/* Cursor Animation */} {isStreaming && (
{'>'}
)}
{/* Footer */}

STREAM: /agent/thinking

{thinkingStream.length} events

) }