import { useState, useRef, useEffect, useMemo } from 'react'; import { useDataRequest, useTaskProgress, useAnalysisResults } from '@/hooks/useApi'; interface AnalysisState { content: string; loading: boolean; error: string | null; elapsed_ms?: number; } interface AnalysisRecord { type: string; name: string; status: 'pending' | 'running' | 'done' | 'error'; start_ts?: string; end_ts?: string; duration_ms?: number; tokens?: { prompt_tokens: number; completion_tokens: number; total_tokens: number; }; error?: string; } export function useAnalysisRunner( financials: any, financialConfig: any, normalizedMarket: string, unifiedSymbol: string, isLoading: boolean, error: any, templateSets: any // Added templateSets ) { // --- Template Logic --- const [selectedTemplateId, setSelectedTemplateId] = useState(''); const reportTemplateId = financials?.meta?.template_id; // Sync selected template with report template when report loads useEffect(() => { if (reportTemplateId) { setSelectedTemplateId(reportTemplateId); } }, [reportTemplateId]); // Set default template if nothing selected and no report template useEffect(() => { if (!selectedTemplateId && !reportTemplateId && templateSets && Object.keys(templateSets).length > 0) { const defaultId = Object.keys(templateSets).find(k => k.includes('standard') || k === 'default') || Object.keys(templateSets)[0]; setSelectedTemplateId(defaultId); } }, [templateSets, selectedTemplateId, reportTemplateId]); // Determine active template set const activeTemplateId = selectedTemplateId; const activeTemplateSet = useMemo(() => { if (!activeTemplateId || !templateSets) return null; return templateSets[activeTemplateId] || null; }, [activeTemplateId, templateSets]); // Derive effective analysis config from template set, falling back to global config if needed const activeAnalysisConfig = useMemo(() => { if (activeTemplateSet) { return { ...financialConfig, analysis_modules: activeTemplateSet.modules, }; } return financialConfig; // Fallback to global config (legacy behavior) }, [activeTemplateSet, financialConfig]); // 分析类型列表 const analysisTypes = useMemo(() => { if (!activeAnalysisConfig?.analysis_modules) return []; return Object.keys(activeAnalysisConfig.analysis_modules); }, [activeAnalysisConfig]); // 分析状态管理 const [analysisStates, setAnalysisStates] = useState>({}); const fullAnalysisTriggeredRef = useRef(false); const isAnalysisRunningRef = useRef(false); const analysisFetchedRefs = useRef>({}); const stopRequestedRef = useRef(false); const abortControllerRef = useRef(null); const currentAnalysisTypeRef = useRef(null); const [manualRunKey, setManualRunKey] = useState(0); // 当前正在执行的分析任务 const [currentAnalysisTask, setCurrentAnalysisTask] = useState(null); // 计时器状态 const [startTime, setStartTime] = useState(null); const [elapsedSeconds, setElapsedSeconds] = useState(0); // 分析执行记录 const [analysisRecords, setAnalysisRecords] = useState([]); // 新架构:触发分析与查看任务进度 const { trigger: triggerAnalysisRequest, isMutating: triggering } = useDataRequest(); const [requestId, setRequestId] = useState(null); const { progress: taskProgress } = useTaskProgress(requestId); // 引入 Analysis Results 轮询 const { data: newAnalysisResults } = useAnalysisResults(unifiedSymbol); // 1. Determine the Active Request ID (The one we want to display) const activeRequestId = useMemo(() => { // If the user manually triggered a task in this session, prioritize that if (requestId) return requestId; // Otherwise, default to the most recent result's request_id from the backend // Assuming newAnalysisResults is sorted by created_at DESC if (newAnalysisResults && newAnalysisResults.length > 0) { return newAnalysisResults[0].request_id; } return null; }, [requestId, newAnalysisResults]); // 2. Filter results for the current batch const currentBatchResults = useMemo(() => { if (!newAnalysisResults || !activeRequestId) return []; return newAnalysisResults.filter(r => r.request_id === activeRequestId); }, [newAnalysisResults, activeRequestId]); // 3. Sync analysisStates (Content) from current batch // We only update if we have a result for that module in the current batch. // If not, we leave it as is (or could clear it if we wanted strict mode). // For now, we'll update based on what we find. useEffect(() => { if (!currentBatchResults) return; setAnalysisStates(prev => { const next = { ...prev }; let hasChanges = false; currentBatchResults.forEach(result => { const type = result.module_id; const status = result.meta_data?.status || 'success'; const content = result.content; const currentState = next[type]; // Only update if content changed or status changed if ( !currentState || currentState.content !== content || (status === 'processing' && !currentState.loading) || (status === 'success' && currentState.loading) || (status === 'error' && !currentState.error) ) { next[type] = { content: content, loading: status === 'processing', error: status === 'error' ? result.meta_data?.error || 'Unknown error' : null, }; hasChanges = true; } }); return hasChanges ? next : prev; }); }, [currentBatchResults]); // 4. Sync analysisRecords (Execution Details) from current batch // This ensures Execution Details only shows the relevant modules for the current run. useEffect(() => { if (!currentBatchResults) return; // If we are starting a new run (triggered), we might want to reset records initially? // But currentBatchResults will eventually populate. const records: AnalysisRecord[] = currentBatchResults.map(r => { const statusStr = r.meta_data?.status; let status: 'pending' | 'running' | 'done' | 'error' = 'done'; if (statusStr === 'processing') status = 'running'; else if (statusStr === 'error') status = 'error'; return { type: r.module_id, name: activeAnalysisConfig?.analysis_modules?.[r.module_id]?.name || r.module_id, status: status, duration_ms: r.meta_data?.elapsed_ms, // Backend needs to provide this in meta_data error: r.meta_data?.error, tokens: r.meta_data?.tokens // Backend needs to provide this }; }); // Sort records to match the defined order in activeAnalysisConfig if possible const definedOrder = Object.keys(activeAnalysisConfig?.analysis_modules || {}); records.sort((a, b) => { const idxA = definedOrder.indexOf(a.type); const idxB = definedOrder.indexOf(b.type); if (idxA === -1) return 1; if (idxB === -1) return -1; return idxA - idxB; }); setAnalysisRecords(records); }, [currentBatchResults, activeAnalysisConfig]); // 计算完成比例 const completionProgress = useMemo(() => { const totalTasks = analysisRecords.length; if (totalTasks === 0) return 0; const completedTasks = analysisRecords.filter(r => r.status === 'done' || r.status === 'error').length; return (completedTasks / totalTasks) * 100; }, [analysisRecords]); // 总耗时(ms) const totalElapsedMs = useMemo(() => { const finMs = financials?.meta?.elapsed_ms || 0; const analysesMs = analysisRecords.reduce((sum, r) => sum + (r.duration_ms || 0), 0); return finMs + analysesMs; }, [financials?.meta?.elapsed_ms, analysisRecords]); const hasRunningTask = useMemo(() => { if (currentAnalysisTask !== null) return true; // Also check analysisRecords derived from backend if (analysisRecords.some(r => r.status === 'running')) return true; return false; }, [currentAnalysisTask, analysisRecords]); // 全部任务是否完成 const allTasksCompleted = useMemo(() => { if (analysisRecords.length === 0) return false; const allDoneOrErrored = analysisRecords.every(r => r.status === 'done' || r.status === 'error'); return allDoneOrErrored && !hasRunningTask && currentAnalysisTask === null; }, [analysisRecords, hasRunningTask, currentAnalysisTask]); // 所有任务完成时,停止计时器 useEffect(() => { if (allTasksCompleted) { setStartTime(null); } }, [allTasksCompleted]); useEffect(() => { if (!startTime) return; const interval = setInterval(() => { const now = Date.now(); const elapsed = Math.floor((now - startTime) / 1000); setElapsedSeconds(elapsed); }, 1000); return () => clearInterval(interval); }, [startTime]); const retryAnalysis = async (analysisType: string) => { // Retry logic is complicated with the new backend-driven approach. // Ideally, we should send a backend command to retry a specific module. // For now, we can just re-trigger the whole template or alert the user. // Or implementation TODO: Single module retry endpoint. alert("单个模块重试功能在新架构中尚未就绪,请重新触发完整分析。"); }; const stopAll = () => { // Clean up client-side state stopRequestedRef.current = true; isAnalysisRunningRef.current = false; setStartTime(null); // Ideally call backend to cancel job }; const continuePending = () => { // No-op in new architecture basically }; const triggerAnalysis = async () => { const reqId = await triggerAnalysisRequest(unifiedSymbol, normalizedMarket || '', selectedTemplateId); if (reqId) { setRequestId(reqId); setStartTime(Date.now()); // Start timer // Reset records to empty or wait for poll? // Waiting for poll is safer to avoid flashing old data setAnalysisRecords([]); } }; return { activeAnalysisConfig, // Exported analysisTypes, analysisStates, analysisRecords, currentAnalysisTask, triggerAnalysis, triggering, requestId, setRequestId, taskProgress, startTime, elapsedSeconds, completionProgress, totalElapsedMs, stopAll, continuePending, retryAnalysis, hasRunningTask, isAnalysisRunning: hasRunningTask, // Simplified selectedTemplateId, // Exported setSelectedTemplateId, // Exported }; }