import React, { useState, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { FileText, Database, Download, GitBranch, HardDrive, Hash, Activity, Trash2, AlertCircle, Plus, } from "lucide-react"; import { Trace, KnowledgeGraph } from "@/types"; import { formatFileSize, formatRelativeTime } from "@/lib/utils"; import { getTraceStatusColor } from "@/lib/status-utils"; import { api } from "@/lib/api"; import { useModal } from "@/context/ModalContext"; import { useNotification } from "@/context/NotificationContext"; import { useNavigation } from "@/context/NavigationContext"; import { useAgentGraph } from "@/context/AgentGraphContext"; import { KnowledgeGraphTree } from "@/components/features/traces/KnowledgeGraphTree"; import { SplitterSelectionModal, SplitterType } from "./SplitterSelectionModal"; import { useTaskPolling } from "@/hooks/useTaskPolling"; import { ContextDocumentsSection } from "@/components/features/context/ContextDocumentsSection"; interface TraceDetailsModalProps { data: { trace: Trace; knowledgeGraphs?: KnowledgeGraph[]; }; onClose: () => void; } export function TraceDetailsModal({ data, onClose: _onClose, }: TraceDetailsModalProps) { const { trace, knowledgeGraphs = [] } = data; const { openModal } = useModal(); const { showNotification } = useNotification(); const { actions: navigationActions } = useNavigation(); // Helper to show both toast and persistent notifications const showSystemNotification = useCallback( (notification: { type: "success" | "error" | "warning" | "info"; title: string; message: string; }) => { // Show toast notification showNotification(notification); // Add to persistent notifications for all types navigationActions.addNotification({ type: notification.type, title: notification.title, message: notification.message, }); }, [showNotification, navigationActions] ); const { actions } = useAgentGraph(); const [isSplitterModalOpen, setIsSplitterModalOpen] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(0); const formatTraceType = (type?: string) => { if (!type) return "Unknown"; return type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); }; const handleViewKnowledgeGraph = (kgId: string) => { const kg = knowledgeGraphs.find((k) => k.kg_id === kgId || k.id === kgId); if (!kg) return; openModal( "knowledge-graph", `Knowledge Graph - ${ kg.filename || `Agent Graph ${kg.kg_id?.slice(0, 8) || "unknown"}` }`, { knowledgeGraph: kg, windowGraphs: kg.window_knowledge_graphs || [], }, { size: "xl", closable: true, } ); }; const handleViewSegment = async ( traceId: string, startChar: number, endChar: number, windowIndex: number ) => { try { const segmentData = await api.traces.extractSegment( traceId, startChar, endChar ); openModal( "trace-segment", `Trace Segment - Window ${windowIndex}`, { trace: trace, segment: { content: segmentData.content, startChar, endChar, windowIndex, }, }, { size: "xl", closable: true, } ); } catch (error) { console.error("Error loading trace segment:", error); showSystemNotification({ type: "error", title: "Failed to Load Segment", message: "Could not load the trace segment. Please try again.", }); } }; const handleDeleteKnowledgeGraph = async (kgId: string, name: string) => { const confirmMessage = `Are you sure you want to delete the knowledge graph "${name}"? This will permanently remove: • The knowledge graph and all entities/relationships • All prompt reconstruction data • All perturbation test results • All causal analysis results This action cannot be undone.`; if (!confirm(confirmMessage)) { return; } try { const response = await fetch(`/api/knowledge-graphs/${kgId}`, { method: "DELETE", headers: { "Content-Type": "application/json", }, }); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } const data = await response.json(); console.log("Knowledge graph deleted:", data); // TODO: Add success notification and refresh knowledge graphs list alert("Knowledge graph deleted successfully"); } catch (error) { console.error("Error deleting knowledge graph:", error); alert( `Error deleting knowledge graph: ${ error instanceof Error ? error.message : String(error) }` ); } }; const handleReplayKg = ( kgId: string, traceId: string, processingRunId?: string ) => { // Handle replay functionality console.log("Replay KG:", { kgId, traceId, processingRunId }); // Implement actual replay logic }; const handleGenerateKG = () => { setIsSplitterModalOpen(true); }; // Task polling setup for knowledge graph generation const { pollTaskStatus, isPolling } = useTaskPolling({ onSuccess: (taskId) => { console.log("Knowledge graph generation completed:", taskId); showSystemNotification({ type: "success", title: "Agent Graph Generation Complete", message: "Your agent graph has been generated successfully!", }); // Refresh the trace data to get the updated knowledge graphs actions.setTraces([]); actions.setKnowledgeGraphs([]); setIsGenerating(false); setGenerationProgress(0); }, onError: (error, taskId) => { console.error("Knowledge graph generation failed:", error, taskId); // Check if this is a timeout vs a real failure const isTimeout = error.includes("timeout") || error.includes("timed out"); showSystemNotification({ type: isTimeout ? "warning" : "error", title: isTimeout ? "Generation Taking Longer Than Expected" : "Agent Graph Generation Failed", message: isTimeout ? `${error} You can refresh the page to check if it completed.` : error, }); // Only reset state if it's not a timeout (user might want to keep waiting) if (!isTimeout) { setIsGenerating(false); setGenerationProgress(0); } }, onProgress: (progress, message) => { setGenerationProgress(progress); if (message) { console.log("Generation progress:", progress, message); } }, maxAttempts: 180, // 15 minutes with exponential backoff interval: 5000, // Start with 5 seconds enableExponentialBackoff: true, }); const handleSplitterConfirm = async ( splitterType: SplitterType, methodName?: string, modelName?: string, chunkingConfig?: { min_chunk_size?: number; max_chunk_size?: number } ) => { const finalMethodName = methodName || "production"; console.log("TraceDetailsModal: Using method name:", finalMethodName); console.log("TraceDetailsModal: Using chunking config:", chunkingConfig); setIsGenerating(true); setIsSplitterModalOpen(false); // Safety fallback - modal should already be closed setGenerationProgress(0); try { const response = await api.traces.generateKnowledgeGraph( trace.trace_id, splitterType, true, finalMethodName, modelName || "gpt-5-mini", chunkingConfig ); showSystemNotification({ type: "info", title: "Agent Graph Generation Started", message: `Using ${getSplitterDisplayName( splitterType )} splitter with ${finalMethodName} method. This may take several minutes.`, }); // Start polling for task status if we got a task_id if (response.task_id) { pollTaskStatus(response.task_id); } else { // If no task_id, just refresh immediately and mark as complete const updatedTraces = await api.traces.list(); actions.setTraces(updatedTraces); const updatedKGs = await api.knowledgeGraphs.list(); actions.setKnowledgeGraphs(updatedKGs); setIsGenerating(false); showSystemNotification({ type: "success", title: "Agent Graph Generation Complete", message: "Your agent graph has been generated successfully!", }); } } catch (error) { console.error("Failed to generate knowledge graph:", error); showSystemNotification({ type: "error", title: "Agent Graph Generation Failed", message: "Failed to start agent graph generation. Please try again.", }); setIsGenerating(false); setGenerationProgress(0); } }; // Helper function to get display name for splitter type const getSplitterDisplayName = (splitterType: SplitterType) => { switch (splitterType) { case "agent_semantic": return "Agent Semantic"; case "json": return "JSON"; case "prompt_interaction": return "Prompt Interaction"; default: return splitterType; } }; const handleViewContent = async () => { try { // Get numbered content for ContentReference compatibility const numberedContent = await api.traces.getNumberedContent( trace.trace_id ); openModal( "trace-content", `Trace Content - ${trace.filename}`, { trace: trace, content: { content: numberedContent, type: "numbered" }, }, { size: "xl", closable: true, } ); } catch (error) { console.error("Failed to load numbered content:", error); // Fallback to existing content if numbered content fails openModal( "trace-content", `Trace Content - ${trace.filename}`, { trace: trace, content: { content: trace.content || "", type: "raw" }, }, { size: "xl", closable: true, } ); } }; const handleDownloadTrace = async () => { try { // Get the full trace content instead of just metadata const traceContent = await api.traces.getContent(trace.trace_id); // Create blob with the actual trace content const blob = new Blob([traceContent], { type: "application/json", }); const url = URL.createObjectURL(blob); const exportFileDefaultName = `${trace.filename || "trace"}.json`; const linkElement = document.createElement("a"); linkElement.setAttribute("href", url); linkElement.setAttribute("download", exportFileDefaultName); linkElement.click(); // Clean up the object URL URL.revokeObjectURL(url); console.log("Trace downloaded successfully:", trace.trace_id); } catch (error) { console.error("Failed to download trace:", error); alert("Failed to download trace. Please try again."); } }; const handleDeleteTrace = async () => { const kgCount = knowledgeGraphs.length; const displayFilename = trace.filename.length > 50 ? `${trace.filename.substring(0, 47)}...` : trace.filename; const confirmMessage = `Are you sure you want to delete the trace "${displayFilename}"? This will permanently remove: • The trace and all its content • All associated knowledge graphs (${kgCount} found) • All entities and relationships • All prompt reconstruction data • All perturbation test results • All causal analysis results This action cannot be undone.`; if (!confirm(confirmMessage)) { return; } try { await api.traces.delete(trace.trace_id); // Close modal and refresh page window.location.reload(); } catch (error) { console.error("Error deleting trace:", error); alert( `Error deleting trace: ${ error instanceof Error ? error.message : String(error) }` ); } }; // Calculate summary statistics - use consistent counting const finalKGsArray = knowledgeGraphs.filter( (kg) => kg.is_final === true || (kg.window_index === null && kg.window_total !== null) ); const finalKGs = finalKGsArray.length; // Count of final KGs const totalKGs = finalKGs; // Use final KG count for consistency const completedPipelines = knowledgeGraphs.filter( (kg) => kg.is_analyzed ).length; return (
{/* Trace Header */}

{trace.filename}

Uploaded{" "} {formatRelativeTime( trace.upload_timestamp || trace.timestamp || "" )}

{trace.status}
{/* Quick Stats */}

{formatFileSize(trace.character_count || trace.size || 0)}

File Size

{trace.turn_count || 0}

Turns

{finalKGs}

Final Agent Graphs

{completedPipelines}

Completed

{/* Trace Information */} Trace Information {/* Core Information Grid */}
{/* Characters */}

Content Size

{trace.character_count?.toLocaleString() || "N/A"} chars

{trace.turn_count && (

{trace.turn_count} turns

)}
{/* Type & Upload Info */}

Type & Date

{formatTraceType(trace.trace_type)}

{new Date( trace.upload_timestamp || trace.timestamp || "" ).toLocaleDateString()}

{/* Description */} {trace.description && (

Description

{trace.description}

)} {/* Error Message */} {trace.error_message && (

Error Message

{trace.error_message}

)} {/* Tags */} {trace.tags && trace.tags.length > 0 && (

Tags

{trace.tags.map((tag) => ( {tag} ))}
)}
{/* Context Documents Section */} {/* Knowledge Graphs using KnowledgeGraphTree */} {knowledgeGraphs.length > 0 ? (
Agent Graphs ({totalKGs})
{finalKGs} Final {completedPipelines} Completed
) : (

Ready to analyze this trace

Transform your trace data into an interactive agent graph.

Choose from different chunking strategies for optimal analysis.

)} {/* Metadata */} {trace.metadata && Object.keys(trace.metadata).length > 0 && ( Metadata
{Object.entries(trace.metadata).map(([key, value]) => (
{key}: {typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)}
))}
)} {/* Actions */}
); }