import React, { useEffect, useRef, useState, useMemo } from "react"; import { ChevronLeft, ChevronRight, ChevronUp, ChevronDown, Info, Database, BarChart3, Cpu, GitBranch, Clock, Layers, } from "lucide-react"; import { Button } from "../ui/button"; import { Card, CardContent } from "../ui/card"; import Editor from "@monaco-editor/react"; import type { editor } from "monaco-editor"; import { KnowledgeGraph } from "@/types"; interface Range { start: number; // 0-based inclusive end: number; // 0-based inclusive } interface TraceViewerSidebarProps { numberedLines: string[]; // each already prefixed with highlightRanges: Range[]; widthClass?: string; // tailwind width override (default w-80) knowledgeGraph?: KnowledgeGraph; // optional knowledge graph data for system tab showTraceTab?: boolean; // whether to show the trace tab (default true) } interface LineMapping { originalLineNumber: number; startChar: number; endChar: number; content: string; } export const TraceViewerSidebar: React.FC = ({ numberedLines, highlightRanges, widthClass = "w-80", knowledgeGraph, showTraceTab = true, }) => { const containerRef = useRef(null); const editorRef = useRef(null); const [isCollapsed, setIsCollapsed] = useState(false); const [currentHighlightIndex, setCurrentHighlightIndex] = useState(0); const [decorations, setDecorations] = useState([]); const [activeTab, setActiveTab] = useState<"system" | "trace">("system"); // Parse numbered lines and reconstruct original content const { reconstructedContent, characterRanges } = useMemo(() => { const mapping: LineMapping[] = []; // Parse each numbered line and extract line numbers const parsedLines: { lineNumber: number; content: string; index: number; }[] = []; for (let i = 0; i < numberedLines.length; i++) { const line = numberedLines[i]; if (!line) continue; const match = line.match(/^\s*(.*)$/); if (!match) continue; const [, lineNumber, content] = match; if (!lineNumber || content === undefined) continue; parsedLines.push({ lineNumber: parseInt(lineNumber), content, index: i, }); } // Sort by line number to ensure proper order parsedLines.sort((a, b) => a.lineNumber - b.lineNumber); // Simple concatenation - just join all content together let rawContent = ""; for (let i = 0; i < parsedLines.length; i++) { const currentLine = parsedLines[i]; if (!currentLine) continue; // Track character mapping for highlighting const startChar = rawContent.length; rawContent += currentLine.content; const endChar = rawContent.length; mapping.push({ originalLineNumber: currentLine.lineNumber, startChar, endChar, content: currentLine.content, }); } // Process the raw content for better formatting let processedContent = rawContent; // Convert literal \\n to actual line breaks processedContent = processedContent.replace(/\\n/g, "\n"); // Convert literal \\t to actual tabs processedContent = processedContent.replace(/\\t/g, "\t"); // Try to detect and format JSON content (handle multiple JSON objects) const jsonRegex = /\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}/g; const jsonMatches = processedContent.match(jsonRegex); if (jsonMatches) { jsonMatches.forEach((jsonStr) => { try { const parsed = JSON.parse(jsonStr); const formatted = JSON.stringify(parsed, null, 2); processedContent = processedContent.replace(jsonStr, formatted); } catch (e) { // If JSON parsing fails, keep original content } }); } // Clean up excessive whitespace but preserve intentional formatting processedContent = processedContent .replace(/[ \t]+/g, " ") // Multiple spaces/tabs to single space .replace(/\n\s*\n\s*\n/g, "\n\n") // Multiple newlines to double newline max .trim(); // Remove leading/trailing whitespace // Update character mapping based on content transformation const contentLengthDiff = processedContent.length - rawContent.length; // Convert line-based ranges to character-based ranges const charRanges = highlightRanges .map((range) => { const startMapping = mapping.find( (m) => m.originalLineNumber === range.start + 1 ); const endMapping = mapping.find( (m) => m.originalLineNumber === range.end + 1 ); if (!startMapping || !endMapping) { return { startChar: 0, endChar: 0 }; } // Adjust for content transformation let adjustedStartChar = startMapping.startChar; let adjustedEndChar = endMapping.endChar; // Simple adjustment - this might need refinement for complex cases if (contentLengthDiff > 0) { const adjustmentRatio = processedContent.length / rawContent.length; adjustedStartChar = Math.floor( startMapping.startChar * adjustmentRatio ); adjustedEndChar = Math.floor(endMapping.endChar * adjustmentRatio); } return { startChar: adjustedStartChar, endChar: adjustedEndChar, }; }) .filter((range) => range.startChar !== range.endChar); return { reconstructedContent: processedContent, characterRanges: charRanges, }; }, [numberedLines, highlightRanges]); // Sort ranges by start position for navigation const sortedRanges = [...characterRanges].sort( (a, b) => a.startChar - b.startChar ); // Scroll to specific highlighted range in Monaco Editor const scrollToHighlight = (rangeIndex: number) => { if (!editorRef.current || sortedRanges.length === 0) return; const range = sortedRanges[rangeIndex]; if (!range) return; const startPosition = editorRef.current .getModel()! .getPositionAt(range.startChar); // Scroll to the position editorRef.current.revealPositionInCenter(startPosition); // Update decorations to highlight current range updateHighlightDecorations(); }; // Navigation handlers const goToNextHighlight = () => { if (sortedRanges.length === 0) return; const nextIndex = (currentHighlightIndex + 1) % sortedRanges.length; setCurrentHighlightIndex(nextIndex); scrollToHighlight(nextIndex); }; const goToPreviousHighlight = () => { if (sortedRanges.length === 0) return; const prevIndex = currentHighlightIndex === 0 ? sortedRanges.length - 1 : currentHighlightIndex - 1; setCurrentHighlightIndex(prevIndex); scrollToHighlight(prevIndex); }; // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) { return; // Don't interfere with input fields } if (e.key === ">" || (e.key === "ArrowDown" && e.ctrlKey)) { e.preventDefault(); goToNextHighlight(); } else if (e.key === "<" || (e.key === "ArrowUp" && e.ctrlKey)) { e.preventDefault(); goToPreviousHighlight(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [currentHighlightIndex, sortedRanges.length]); // Scroll first highlighted range into view on initial load useEffect(() => { if (sortedRanges.length > 0) { setCurrentHighlightIndex(0); scrollToHighlight(0); } }, [highlightRanges]); // Update decorations when current highlight changes useEffect(() => { updateHighlightDecorations(); }, [currentHighlightIndex, characterRanges]); // Handle Monaco Editor mount const handleEditorDidMount = (editor: editor.IStandaloneCodeEditor) => { editorRef.current = editor; // Apply initial decorations for highlights updateHighlightDecorations(); // Configure editor options editor.updateOptions({ readOnly: true, minimap: { enabled: false }, scrollBeyondLastLine: false, wordWrap: "on", lineNumbers: "on", folding: false, renderWhitespace: "selection", fontSize: 12, fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace", lineHeight: 18, letterSpacing: 0.5, fontWeight: "400", renderLineHighlight: "none", cursorStyle: "line", cursorBlinking: "blink", smoothScrolling: true, }); }; // Update highlight decorations in Monaco Editor const updateHighlightDecorations = () => { if (!editorRef.current) return; // Clear all existing decorations first to prevent old highlights from persisting if (decorations.length > 0) { editorRef.current.deltaDecorations(decorations, []); setDecorations([]); } const newDecorations: editor.IModelDeltaDecoration[] = []; characterRanges.forEach((range, index) => { const startPosition = editorRef .current!.getModel()! .getPositionAt(range.startChar); const endPosition = editorRef .current!.getModel()! .getPositionAt(range.endChar); const isCurrentHighlight = index === currentHighlightIndex; newDecorations.push({ range: { startLineNumber: startPosition.lineNumber, startColumn: startPosition.column, endLineNumber: endPosition.lineNumber, endColumn: endPosition.column, }, options: { className: isCurrentHighlight ? "current-highlight-decoration" : "highlight-decoration", isWholeLine: false, }, }); }); // Apply new decorations const decorationIds = editorRef.current.deltaDecorations( [], newDecorations ); setDecorations(decorationIds); }; // Detect language for syntax highlighting const detectLanguage = (content: string): string => { // Check for JSON content if (content.trim().startsWith("{") && content.trim().endsWith("}")) { try { JSON.parse(content); return "json"; } catch { // Not valid JSON, continue checking } } // Check for Python code patterns if (/^(import|from|def|class|if __name__|print\()/m.test(content)) { return "python"; } // Check for JavaScript/TypeScript patterns if (/^(import|export|function|const|let|var|console\.log)/m.test(content)) { return "javascript"; } // Check for shell/bash patterns if ( /^(#!\/bin\/|cd |ls |mkdir |rm |pip install|npm install)/m.test(content) ) { return "shell"; } // Check for SQL patterns if (/^(SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER)/im.test(content)) { return "sql"; } // Default to plain text return "text"; }; const detectedLanguage = useMemo( () => detectLanguage(reconstructedContent), [reconstructedContent] ); if (isCollapsed) { return (
); } return (
{/* Header */}
{showTraceTab && ( )}
{/* Content */}
{activeTab === "system" ? (
{knowledgeGraph ? ( <> {/* System Hero Section */}
{knowledgeGraph.system_name && (

{knowledgeGraph.system_name}

)} {knowledgeGraph.system_summary && (

{knowledgeGraph.system_summary}

)}
{/* Subtle background pattern */}
{/* Stats Grid */}
{/* Entities Card */}

Entities

{knowledgeGraph.entity_count || 0}

{/* Relations Card */}

Relations

{knowledgeGraph.relation_count || 0}

{/* Processing Details */} {/* Processing Information */} {knowledgeGraph.processing_metadata && (
Processing Details
{knowledgeGraph.processing_metadata.method_name && (
Method
{knowledgeGraph.processing_metadata.method_name}
)} {knowledgeGraph.processing_metadata.splitter_type && (
Splitter
{knowledgeGraph.processing_metadata.splitter_type}
)} {knowledgeGraph.window_total && (
Windows
{knowledgeGraph.window_total}
)}
)}
) : (

System Information

No system data available for this view

)}
) : activeTab === "trace" ? (
{/* Navigation controls for trace */} {sortedRanges.length > 0 && (
{currentHighlightIndex + 1} / {sortedRanges.length}
)}
) : null}
{/* Footer with navigation hint */} {activeTab === "trace" && sortedRanges.length > 0 && (
Press {"<"} {">"} to navigate
)}
); };