import React, { useState, useEffect, useCallback } from "react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { Trace, KnowledgeGraph } from "@/types"; import { useAgentGraph } from "@/context/AgentGraphContext"; import { useKGDisplayMode } from "@/context/KGDisplayModeContext"; import { filterKnowledgeGraphsByMode } from "@/lib/kg-selection"; import { AgentGraphsSection } from "@/components/features/traces/AgentGraphsSection"; import { TraceOverviewSection } from "@/components/features/traces/TraceOverviewSection"; import { ContextDocumentsModal } from "@/components/shared/modals/ContextDocumentsModal"; import { TraceContentModal } from "@/components/shared/modals/TraceContentModal"; import { api } from "@/lib/api"; import { useModal } from "@/context/ModalContext"; import { useNotification } from "@/context/NotificationContext"; import { useNavigation } from "@/context/NavigationContext"; import { SplitterSelectionModal, SplitterType, // ChunkingConfig, } from "@/components/shared/modals/SplitterSelectionModal"; import { useTaskPolling } from "@/hooks/useTaskPolling"; import { MetadataCardConfig } from "@/components/shared/CompactMetadataCard"; import { MetadataCardSelector, AVAILABLE_CARDS, DEFAULT_CARDS, } from "@/components/shared/modals/MetadataCardSelector"; interface TraceKnowledgeGraphViewProps { trace: Trace; knowledgeGraphs: KnowledgeGraph[]; } export function TraceKnowledgeGraphView({ trace, knowledgeGraphs: _knowledgeGraphs, }: TraceKnowledgeGraphViewProps) { const { actions } = useAgentGraph(); const { setActiveView, // Used in TraceContentModal onEditTrace } = actions; const { mode: kgDisplayMode } = useKGDisplayMode(); const [kgData, setKgData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useModal(); // Keep for potential future use const { showNotification } = useNotification(); const { actions: navigationActions } = useNavigation(); // Add state for trace content const [traceContent, setTraceContent] = useState(""); const [traceContentLoading, setTraceContentLoading] = useState(false); const [traceContentError, setTraceContentError] = useState( null ); // State for metadata regeneration const [metadataOutdated, setMetadataOutdated] = useState(false); const [updatedMetadata, setUpdatedMetadata] = useState(null); const [updatedTraceData, setUpdatedTraceData] = useState(null); // State for enhanced statistics const [enhancedStats, setEnhancedStats] = useState(null); const [enhancedStatsLoading, setEnhancedStatsLoading] = useState(false); // Get current metadata (use updated if available, otherwise original) const currentMetadata = updatedMetadata || trace.metadata; // 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] ); // Function to check if metadata is outdated const checkMetadataFreshness = useCallback( (trace: Trace, content: string, metadata: any, updatedTraceData?: any) => { if (!metadata?.schema_analytics || !content) { return false; // No metadata to check } // Check 1: JSON validity - if current content is invalid JSON, metadata is definitely outdated try { JSON.parse(content); } catch (e) { return true; // Invalid JSON means metadata is outdated } // Check 2: Content length difference (>5% threshold) const currentLength = content.length; // Use updated trace data if available, otherwise use original trace const metadataLength = updatedTraceData?.character_count || trace.character_count || 0; if (metadataLength > 0) { const lengthDifference = Math.abs(currentLength - metadataLength) / metadataLength; if (lengthDifference > 0.05) { return true; } } // Check 3: Character count mismatch (exact) if (currentLength !== metadataLength) { return true; } return false; }, [] ); // Check metadata freshness when trace content is loaded useEffect(() => { if (traceContent && trace) { const outdated = checkMetadataFreshness( trace, traceContent, currentMetadata, updatedTraceData ); setMetadataOutdated(outdated); } }, [ traceContent, trace, currentMetadata, updatedTraceData, checkMetadataFreshness, ]); // Auto-refresh metadata when outdated useEffect(() => { const autoRefreshMetadata = async () => { if (!metadataOutdated || !trace.trace_id) return; try { // Call the universal parser endpoint to regenerate metadata const response = await fetch( `/api/traces/${trace.trace_id}/regenerate-metadata`, { method: "POST", headers: { "Content-Type": "application/json", }, } ); if (!response.ok) { throw new Error("Failed to regenerate metadata"); } // Fetch updated trace data const traceResponse = await fetch(`/api/traces/${trace.trace_id}`); if (traceResponse.ok) { const response = await traceResponse.json(); const updatedTrace = response.trace; // Extract trace from response wrapper // Store the updated metadata and trace data locally setUpdatedMetadata(updatedTrace.metadata); setUpdatedTraceData(updatedTrace); // Reset metadata outdated flag setMetadataOutdated(false); } } catch (error) { console.error("Auto-refresh metadata failed:", error); } }; // Auto-refresh metadata every 90 seconds if outdated (reduced frequency for HF Spaces) const interval = setInterval(() => { autoRefreshMetadata(); }, 90000); // 90 seconds (increased from 30s to avoid 429 errors) return () => clearInterval(interval); }, [metadataOutdated, trace.trace_id]); const [isSplitterModalOpen, setIsSplitterModalOpen] = useState(false); const [isContextDocumentsModalOpen, setIsContextDocumentsModalOpen] = useState(false); const [isTraceContentModalOpen, setIsTraceContentModalOpen] = useState(false); const [isGenerating, setIsGenerating] = useState(false); const [generationProgress, setGenerationProgress] = useState(0); // Load trace content (clean content for overview display) const fetchTraceContent = useCallback(async () => { if (!trace.trace_id) return; setTraceContentLoading(true); setTraceContentError(null); try { // Use clean content for overview display, not numbered content const content = await api.traces.getContent(trace.trace_id); setTraceContent(content); } catch (err) { setTraceContentError( err instanceof Error ? err.message : "Failed to load trace content" ); } finally { setTraceContentLoading(false); } }, [trace.trace_id]); const fetchKnowledgeGraphs = useCallback(async () => { if (!trace.trace_id) return; setLoading(true); setError(null); try { // Also check if trace already has knowledge graphs console.log("Trace object:", trace); console.log("Trace knowledge_graphs:", trace.knowledge_graphs); const response = await fetch( `/api/traces/${trace.trace_id}/knowledge-graphs` ); if (!response.ok) { throw new Error( `Failed to fetch knowledge graphs: ${response.statusText}` ); } const data = await response.json(); console.log("Raw API response:", data); if (data?.knowledge_graphs?.length) { console.log( "Total knowledge graphs received:", data.knowledge_graphs.length ); // Log each KG to see their structure data.knowledge_graphs.forEach((kg: any, index: number) => { console.log(`KG ${index}:`, { id: kg.id, kg_id: kg.kg_id, is_final: kg.is_final, window_index: kg.window_index, window_total: kg.window_total, filename: kg.filename, }); }); // Filter for final graphs using the exact logic from stage_processor.js const finalKGs = data.knowledge_graphs.filter((kg: any) => { const hasValidId = Boolean(kg.kg_id || kg.id); const isFinal = kg.is_final === true || (kg.window_index === null && kg.window_total !== null); if (!hasValidId) { console.warn( "Found knowledge graph with missing ID - skipping:", kg ); return false; } if (!isFinal) { console.log( "Skipping non-final KG:", kg.kg_id, "window_index:", kg.window_index, "is_final:", kg.is_final ); } else { console.log( "Including final KG:", kg.kg_id, "is_final:", kg.is_final, "window_index:", kg.window_index, "window_total:", kg.window_total ); } return isFinal; }); console.log("Filtered final KGs:", finalKGs.length, finalKGs); // Apply display mode filtering const filteredByMode = filterKnowledgeGraphsByMode( finalKGs, kgDisplayMode ); console.log( `Applied ${kgDisplayMode} mode filtering:`, filteredByMode.length, "KGs" ); setKgData(filteredByMode); } else { console.log( "No knowledge graphs in API response, checking trace object" ); // Fallback to trace.knowledge_graphs if API returns empty if (trace.knowledge_graphs && trace.knowledge_graphs.length > 0) { console.log("Using trace.knowledge_graphs:", trace.knowledge_graphs); setKgData(trace.knowledge_graphs); } else { setKgData([]); } } } catch (err) { console.log("API call failed, trying trace.knowledge_graphs fallback"); setError( err instanceof Error ? err.message : "Failed to fetch knowledge graphs" ); // Fallback to trace data if API fails if (trace.knowledge_graphs && trace.knowledge_graphs.length > 0) { console.log( "Using trace.knowledge_graphs as fallback:", trace.knowledge_graphs ); setKgData(trace.knowledge_graphs); setError(null); // Clear error since we found data } else { setKgData([]); } } finally { setLoading(false); } }, [trace, kgDisplayMode]); // Fetch enhanced statistics const fetchEnhancedStatistics = useCallback(async () => { if (!trace.trace_id) return; setEnhancedStatsLoading(true); try { const response = await api.traces.getEnhancedStatistics(trace.trace_id); setEnhancedStats(response.enhanced_statistics); } catch (err) { console.error("Failed to fetch enhanced statistics:", err); setEnhancedStats(null); } finally { setEnhancedStatsLoading(false); } }, [trace.trace_id]); useEffect(() => { fetchKnowledgeGraphs(); fetchTraceContent(); fetchEnhancedStatistics(); }, [fetchKnowledgeGraphs, fetchTraceContent, fetchEnhancedStatistics]); // Auto-refresh knowledge graphs every 10 seconds useEffect(() => { const interval = setInterval(() => { if (!loading && !isGenerating) { fetchKnowledgeGraphs(); } }, 10000); // 10 seconds return () => clearInterval(interval); }, [fetchKnowledgeGraphs, loading, isGenerating]); 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 knowledge graphs list fetchKnowledgeGraphs(); 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, model?: string, chunkingConfig?: { min_chunk_size?: number; max_chunk_size?: number } ) => { const finalMethodName = methodName || "production"; const finalModel = model || "gpt-5-mini"; console.log("TraceKnowledgeGraphView: Using method name:", finalMethodName); console.log("TraceKnowledgeGraphView: Using model:", finalModel); console.log( "TraceKnowledgeGraphView: 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, // force_regenerate = true to allow generating new graphs even if existing ones exist finalMethodName, finalModel, 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 await fetchKnowledgeGraphs(); 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); showNotification({ 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 (matching stage_processor.js) 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; } }; // Metadata cards state management const [selectedMetadataCards, setSelectedMetadataCards] = useState( () => { const saved = localStorage.getItem("metadata-cards-selection"); return saved ? JSON.parse(saved) : DEFAULT_CARDS; } ); const [isMetadataCardSelectorOpen, setIsMetadataCardSelectorOpen] = useState(false); // Save metadata card selection to localStorage const handleSaveMetadataCards = (cards: string[]) => { setSelectedMetadataCards(cards); localStorage.setItem("metadata-cards-selection", JSON.stringify(cards)); }; // Generate metadata card configs const generateMetadataCards = (): MetadataCardConfig[] => { const formatDate = (timestamp?: string) => { if (!timestamp) return "N/A"; return new Date(timestamp).toLocaleDateString(); }; const formatCurrency = (amount: number | string): string => { const num = typeof amount === "string" ? parseFloat(amount) : amount; if (isNaN(num) || num === 0) return "$0.00"; if (num < 0.01) return `$${num.toFixed(6)}`; return `$${num.toFixed(4)}`; }; const getCardValue = (cardId: string): string | number => { // Get schema analytics from trace metadata const schemaAnalytics = currentMetadata?.schema_analytics; // Use enhanced stats if available and not loading const stats = !enhancedStatsLoading && enhancedStats ? enhancedStats : null; switch (cardId) { case "size": return trace.character_count || 0; case "type": return trace.trace_type || "Unknown"; case "uploaded": return formatDate(trace.upload_timestamp); case "graphs": return kgData.length; case "modified": return formatDate(trace.upload_timestamp); case "source": return "Manual Upload"; case "method": return "Production"; // Schema analytics cards case "depth": return ( stats?.components?.max_depth || schemaAnalytics?.numerical_overview?.component_stats?.max_depth || "N/A" ); case "execution_time": { const totalTimeMs = stats?.performance?.total_execution_time_ms || schemaAnalytics?.numerical_overview?.timing_analytics ?.total_execution_time_ms; if (totalTimeMs && totalTimeMs > 0) { if (totalTimeMs >= 1000) { return `${(totalTimeMs / 1000).toFixed(1)}s`; } else { return `${totalTimeMs}ms`; } } return "N/A"; } case "total_tokens": return ( stats?.tokens?.total_tokens || schemaAnalytics?.numerical_overview?.token_analytics ?.total_tokens || "N/A" ); case "prompt_calls": return ( stats?.prompt_calls?.total_calls || schemaAnalytics?.prompt_analytics?.prompt_calls_detected || "N/A" ); case "components": return ( stats?.components?.total_components || schemaAnalytics?.numerical_overview?.component_stats ?.total_components || "N/A" ); case "success_rate": { const successRate = stats?.components?.success_rate || schemaAnalytics?.numerical_overview?.component_stats?.success_rate; if (successRate !== undefined && successRate !== null) { return `${successRate.toFixed(1)}%`; } return "N/A"; } // Cost cards case "total_cost": return enhancedStatsLoading ? "Loading..." : stats?.cost?.total_cost_usd ? formatCurrency(stats.cost.total_cost_usd) : "N/A"; case "cost_per_call": return enhancedStatsLoading ? "Loading..." : stats?.cost?.avg_cost_per_call_usd ? formatCurrency(stats.cost.avg_cost_per_call_usd) : "N/A"; case "input_cost": return enhancedStatsLoading ? "Loading..." : stats?.cost?.input_cost_usd ? formatCurrency(stats.cost.input_cost_usd) : "N/A"; case "output_cost": return enhancedStatsLoading ? "Loading..." : stats?.cost?.output_cost_usd ? formatCurrency(stats.cost.output_cost_usd) : "N/A"; // Token analytics cards case "avg_input_tokens": return enhancedStatsLoading ? "Loading..." : stats?.prompt_calls?.avg_prompt_tokens_per_call ? Math.round(stats.prompt_calls.avg_prompt_tokens_per_call) : "N/A"; case "avg_output_tokens": return enhancedStatsLoading ? "Loading..." : stats?.prompt_calls?.avg_completion_tokens_per_call ? Math.round(stats.prompt_calls.avg_completion_tokens_per_call) : "N/A"; default: return "N/A"; } }; return selectedMetadataCards .map((cardId) => { const cardConfig = AVAILABLE_CARDS.find((card) => card.id === cardId); if (!cardConfig) return null; return { ...cardConfig, value: getCardValue(cardId), }; }) .filter((card): card is MetadataCardConfig => card !== null); }; return (
{/* Main Content */}
{/* Main Side-by-Side Section - Agent Graphs (Left) and Trace Overview (Right) */}
{/* Left Column - Agent Graphs */}
setIsContextDocumentsModalOpen(true) } />
{/* Right Column - Trace Overview */}
setIsTraceContentModalOpen(true)} metadataCards={generateMetadataCards()} onOpenMetadataCardSelector={() => setIsMetadataCardSelectorOpen(true) } />
{/* Splitter Selection Modal */} {/* Metadata Card Selector Modal */} {/* Context Documents Modal */} setIsContextDocumentsModalOpen(false)} traceId={trace.trace_id} /> {/* Trace Content Modal */} setIsTraceContentModalOpen(false)} trace={trace} traceContent={traceContent} traceContentLoading={traceContentLoading} traceContentError={traceContentError} onEditTrace={() => setActiveView("trace-editor")} />
); }