import { useState, useEffect } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; import { Badge } from '@/components/ui/badge'; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { WorkflowVisualizer } from '@/components/workflow/WorkflowVisualizer'; import { ContextExplorer } from '@/components/workflow/ContextExplorer'; import { useWorkflowStore } from '@/stores/useWorkflowStore'; import { TaskStatus, schemas } from '@/api/schema.gen'; import { Loader2, CheckCircle2, AlertCircle, Clock, PanelLeftClose, PanelLeftOpen, TerminalSquare, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { useAnalysisTemplates } from "@/hooks/useConfig" import { WorkflowStatus, ConnectionStatus, TaskState } from '@/types/workflow'; import { Progress } from "@/components/ui/progress" import { cn, formatNodeName } from '@/lib/utils'; import { RealtimeLogs } from '@/components/workflow/RealtimeLogs'; import { PDFExportButton } from '@/components/report/PDFExportButton'; export function ReportPage() { const { id } = useParams(); const [searchParams] = useSearchParams(); const symbol = searchParams.get('symbol'); const market = searchParams.get('market'); const templateId = searchParams.get('templateId'); const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false); const [isWorkflowSticky, setIsWorkflowSticky] = useState(false); useEffect(() => { const handleScroll = () => { setIsWorkflowSticky(window.scrollY > 10); }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); }, []); const { initialize, handleEvent, status, mode, loadFromSnapshot, tasks, dag, activeTab, setActiveTab, logs: globalLogs } = useWorkflowStore(); const { data: templates } = useAnalysisTemplates(); const templateName = templates?.find(t => t.id === templateId)?.name || templateId; // Initialization & Connection Logic useEffect(() => { if (!id) return; initialize(id); let eventSource: EventSource | null = null; // 1. Attempt to load snapshot (Parallel / Fallback) // If the workflow is already finished, SSE might close immediately or 404. const loadSnapshot = async () => { try { console.log(`[ReportPage] Fetching snapshot for ${id}...`); const res = await fetch(`/api/v1/workflow/snapshot/${id}`); if (res.ok) { const snapshot = await res.json(); console.log(`[ReportPage] Snapshot loaded successfully for ${id}`, snapshot); // Handle tagged enum wrapper (type/payload) if present let rawPayload = snapshot.data_payload; if (rawPayload && typeof rawPayload === 'object' && 'payload' in rawPayload && 'type' in rawPayload) { rawPayload = rawPayload.payload; } loadFromSnapshot(rawPayload); } else { console.warn(`[ReportPage] Snapshot fetch failed: ${res.status} ${res.statusText}`); } } catch (e) { console.warn("[ReportPage] Snapshot load exception (normal for new tasks):", e); } }; loadSnapshot(); // 2. Connect to Real-time Stream try { console.log(`[ReportPage] Initializing EventSource for ${id}...`); eventSource = new EventSource(`/api/v1/workflow/events/${id}`); eventSource.onopen = () => { console.log(`[ReportPage] SSE Connection Opened for ${id}`); }; eventSource.onmessage = (event) => { try { // console.log(`[ReportPage] SSE Message received:`, event.data); const parsedEvent = JSON.parse(event.data); if (parsedEvent.type === schemas.WorkflowEventType.enum.WorkflowStateSnapshot) { console.log(`[ReportPage] !!! Received WorkflowStateSnapshot !!!`, parsedEvent); } else if (parsedEvent.type !== schemas.WorkflowEventType.enum.TaskStreamUpdate && parsedEvent.type !== schemas.WorkflowEventType.enum.TaskLog) { // Suppress high-frequency logs to prevent browser lag console.log(`[ReportPage] SSE Event: ${parsedEvent.type}`, parsedEvent); } handleEvent(parsedEvent); } catch (e) { console.error("[ReportPage] Failed to parse SSE event:", e); } }; eventSource.onerror = (err) => { // Standard behavior: if connection closes, it might be finished or failed. // We rely on Snapshot for history if SSE fails. console.warn("[ReportPage] SSE Connection Closed/Error", err); eventSource?.close(); }; } catch (e) { console.error("[ReportPage] Failed to init SSE:", e); } return () => { eventSource?.close(); }; }, [id, initialize, handleEvent, loadFromSnapshot]); // Include ALL nodes in tabs to allow debugging context for DataFetch tasks const tabNodes = dag?.nodes || []; // Use global raw logs directly // const { tasks, logs: globalLogs } = useWorkflowStore(); return (
{/* Realtime Logs - Only in realtime mode */} {mode === 'realtime' && ( )} {/* Header Area */}

{symbol} {market}

Request ID: {id} {templateName && Template: {templateName}}
{/* Main Content Grid */}
{/* Left Col: Visualizer */}
setIsSidebarCollapsed(!isSidebarCollapsed)} > {!isSidebarCollapsed ? ( Workflow Status ) : (
Workflow Status
)}
{/* Right Col: Detail Tabs */}
Overview {tabNodes.map(node => ( {node.display_name || formatNodeName(node.name)} ))}
{/* Content Area */}
t.status === schemas.TaskStatus.enum.Completed).length} /> {tabNodes.map(node => ( ))}
); } function OverviewTabContent({ status, tasks, totalTasks, completedTasks }: { status: WorkflowStatus, tasks: Record, totalTasks: number, completedTasks: number }) { // Count ALL tasks that have reached a terminal state (Completed, Skipped, Failed) // This is more accurate for "progress" than just successful completions. const processedCount = Object.values(tasks).filter(t => t.status === schemas.TaskStatus.enum.Completed || t.status === schemas.TaskStatus.enum.Skipped || t.status === schemas.TaskStatus.enum.Failed ).length; const progress = totalTasks > 0 ? (processedCount / totalTasks) * 100 : 0; // Find errors const failedTasks = Object.entries(tasks).filter(([_, t]) => t.status === schemas.TaskStatus.enum.Failed); return (
{/* Hero Status */}
{status === schemas.TaskStatus.enum.Completed ? ( ) : status === schemas.TaskStatus.enum.Failed ? ( ) : ( )}
{status === schemas.TaskStatus.enum.Completed ? "Analysis Completed" : status === schemas.TaskStatus.enum.Failed ? "Analysis Failed" : "Analysis In Progress"}
Overall Progress {Math.round(progress)}% ({processedCount}/{totalTasks} tasks)
{/* Failed Tasks Warning */} {failedTasks.length > 0 && (

Some tasks failed:

    {failedTasks.map(([id, t]) => (
  • {id}: {t.message || "Unknown error"}
  • ))}
)}
{/* Stats Grid */}
Total Tasks
{totalTasks}
Completed
{completedTasks}
Duration
--:--
) } function TaskDetailView({ taskId, task, requestId, mode }: { taskId: string, task?: TaskState, requestId?: string, mode: 'realtime' | 'historical' }) { const [isInspectorOpen, setIsInspectorOpen] = useState(false); const { setTaskContent } = useWorkflowStore(); // Fetch content for historical tasks if missing useEffect(() => { if (!requestId || !task || !task.outputCommit) return; // Only proceed if content is missing and task is finished (or has output commit) if (task.content || (task.status !== schemas.TaskStatus.enum.Completed && task.status !== schemas.TaskStatus.enum.Failed)) { return; } const fetchContent = async () => { try { let targetFile = null; // Strategy 1: Use Metadata (Preferred) // Now task.metadata is strongly typed as TaskMetadata from generated schema if (task.metadata && task.metadata.output_path) { targetFile = task.metadata.output_path; } // Strategy 2: Infer from Diff (Fallback) else if (task.inputCommit) { const diffRes = await fetch(`/api/context/${requestId}/diff/${task.inputCommit}/${task.outputCommit}`); if (diffRes.ok) { const changes = await diffRes.json(); const files: string[] = changes.map((c: any) => c.Added || c.Modified).filter(Boolean); // Heuristic to find the "Main Report" or "Output" const reportFile = files.find((f: string) => f.endsWith('.md') && !f.endsWith('_execution.md') && !f.endsWith('_trace.md')); const execFile = files.find((f: string) => f.endsWith('_execution.md')); const anyMd = files.find((f: string) => f.endsWith('.md')); const anyJson = files.find((f: string) => f.endsWith('.json')); targetFile = reportFile || execFile || anyMd || anyJson || files[0]; } } if (targetFile) { const contentRes = await fetch(`/api/context/${requestId}/blob/${task.outputCommit}/${encodeURIComponent(targetFile)}`); if (contentRes.ok) { const text = await contentRes.text(); if (targetFile.endsWith('.json')) { try { const obj = JSON.parse(text); setTaskContent(taskId, JSON.stringify(obj, null, 2)); } catch { setTaskContent(taskId, text); } } else { setTaskContent(taskId, text); } } } } catch (e) { console.error("Auto-fetch content failed", e); } }; fetchContent(); }, [requestId, taskId, task?.outputCommit, task?.inputCommit, task?.status, task?.content, task?.metadata, setTaskContent]); // Only show context if we have commits const hasContext = task?.inputCommit || task?.outputCommit; return (
{/* Main Report View */}
{task?.content ? ( {task.content || ''} ) : (
{task?.status === schemas.TaskStatus.enum.Pending &&

Waiting to start...

} {task?.status === schemas.TaskStatus.enum.Running && !task?.content && } {task?.status === schemas.TaskStatus.enum.Failed && (

Task Failed

{task.message}

)}
)} {task?.status === schemas.TaskStatus.enum.Running && ( )}
{/* Inspector Toggle (Floating) */}
{/* Inspector Panel (Overlaid on Content) */}

Task Inspector

{hasContext && requestId && (task?.inputCommit || task?.outputCommit) ? ( ) : (
No context available
)}
); } function WorkflowStatusBadge({ status, mode }: { status: WorkflowStatus, mode: 'realtime' | 'historical' }) { // Map local store status to TaskStatus enum for consistency where possible const content = (
{status === schemas.TaskStatus.enum.Running && } {status} {mode === 'historical' && ( Historical )}
); if (status === schemas.TaskStatus.enum.Running) { return {content}; } if (status === schemas.TaskStatus.enum.Completed) { return {content}; } if (status === schemas.TaskStatus.enum.Failed) { return {content}; } if (status === ConnectionStatus.Connecting) { return CONNECTING...; } return {content}; } function TaskStatusIndicator({ status }: { status: TaskStatus }) { switch (status) { case schemas.TaskStatus.enum.Running: return ; case schemas.TaskStatus.enum.Completed: return ; case schemas.TaskStatus.enum.Failed: return
; default: return null; } }