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
)}
{
e.stopPropagation(); // Prevent double toggle if button is clicked
setIsSidebarCollapsed(!isSidebarCollapsed);
}}
>
{isSidebarCollapsed ? : }
{/* 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) */}
setIsInspectorOpen(!isInspectorOpen)}>
Inspector
{/* Inspector Panel (Overlaid on Content) */}
Task Inspector
setIsInspectorOpen(false)}>
{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;
}
}