import React, { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Save, FileText, RotateCcw, AlertCircle, CheckCircle, Search, Hash, } from "lucide-react"; import { Trace } from "@/types"; interface TraceEditorProps { trace: Trace; onBack?: () => void; } interface LineNumberMapping { [lineNumber: number]: number; // line number to character position mapping } export function TraceEditor({ trace, onBack: _onBack }: TraceEditorProps) { const [content, setContent] = useState(""); const [originalContent, setOriginalContent] = useState(""); const [isLoading, setIsLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const [error, setError] = useState(null); const [saveSuccess, setSaveSuccess] = useState(false); const [validationError, setValidationError] = useState(null); const [lineMapping, setLineMapping] = useState({}); const [gotoLineNumber, setGotoLineNumber] = useState(""); const textareaRef = useRef(null); // JSON validation function const validateJSON = ( content: string ): { isValid: boolean; error?: string } => { if (!content.trim()) { return { isValid: false, error: "Content cannot be empty" }; } try { JSON.parse(content); return { isValid: true }; } catch (e) { if (e instanceof SyntaxError) { return { isValid: false, error: `Invalid JSON: ${e.message}`, }; } return { isValid: false, error: "Invalid JSON format", }; } }; // Validate content on change useEffect(() => { if (content !== originalContent) { const validation = validateJSON(content); setValidationError(validation.isValid ? null : validation.error || null); } else { setValidationError(null); } }, [content, originalContent]); // Create line number mapping (similar to TraceLineNumberProcessor) const createLineMapping = (content: string): LineNumberMapping => { const lines = content.split("\n"); const mapping: LineNumberMapping = {}; let currentCharPos = 0; for (let i = 0; i < lines.length; i++) { const lineNumber = i + 1; const line = lines[i]; mapping[lineNumber] = currentCharPos; currentCharPos += (line?.length || 0) + 1; // +1 for newline character } return mapping; }; // Get line mapping for external use (e.g., ContentReference integration) // eslint-disable-next-line @typescript-eslint/no-unused-vars const getLineMapping = (): LineNumberMapping => { return lineMapping; }; // Load trace content with line numbers for ContentReference compatibility useEffect(() => { const loadTraceContent = async () => { try { setIsLoading(true); setError(null); const response = await fetch( `/api/traces/${trace.trace_id}/content-numbered` ); if (!response.ok) { throw new Error( `Failed to load trace content: ${response.statusText}` ); } const data = await response.json(); const traceContent = data.content || ""; setContent(traceContent); setOriginalContent(traceContent); // Create line number mapping const mapping = createLineMapping(traceContent); setLineMapping(mapping); } catch (err) { setError( err instanceof Error ? err.message : "Failed to load trace content" ); } finally { setIsLoading(false); } }; loadTraceContent(); }, [trace.trace_id]); // Calculate statistics const characterCount = content.length; const lineCount = content.split("\n").length; const wordCount = content.trim() === "" ? 0 : content.trim().split(/\s+/).length; const hasChanges = content !== originalContent; const canSave = hasChanges && !validationError; // Handle save const handleSave = async () => { // Prevent saving if there are validation errors if (validationError) { setError("Cannot save: " + validationError); return; } try { setIsSaving(true); setError(null); setSaveSuccess(false); const response = await fetch(`/api/traces/${trace.trace_id}/content`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ content }), }); if (!response.ok) { throw new Error(`Failed to save trace: ${response.statusText}`); } setOriginalContent(content); setSaveSuccess(true); // Update line mapping for the new content const mapping = createLineMapping(content); setLineMapping(mapping); // Auto-hide success message after 3 seconds setTimeout(() => setSaveSuccess(false), 3000); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save trace"); } finally { setIsSaving(false); } }; // Handle reset const handleReset = () => { if (window.confirm("Are you sure you want to discard all changes?")) { setContent(originalContent); // Reset line mapping const mapping = createLineMapping(originalContent); setLineMapping(mapping); } }; // Handle goto line const handleGotoLine = () => { const lineNumber = parseInt(gotoLineNumber); if ( lineNumber && lineNumber > 0 && lineNumber <= lineCount && textareaRef.current ) { const lines = content.split("\n"); let position = 0; for (let i = 0; i < lineNumber - 1; i++) { position += (lines[i]?.length || 0) + 1; // +1 for newline } textareaRef.current.focus(); textareaRef.current.setSelectionRange(position, position); textareaRef.current.scrollTop = (lineNumber - 1) * 20; // Approximate line height } }; // Handle content change const handleContentChange = (e: React.ChangeEvent) => { setContent(e.target.value); }; // Handle scroll synchronization between line numbers and textarea const handleScroll = (e: React.UIEvent) => { const lineNumbersElement = document.getElementById("line-numbers"); if (lineNumbersElement && e.currentTarget) { const scrollTop = e.currentTarget.scrollTop; lineNumbersElement.style.transform = `translateY(-${scrollTop}px)`; } }; // Generate line numbers based on content const generateLineNumbers = () => { const lines = content.split("\n"); return lines.map((_, index) => `${index + 1}`).join("\n"); }; // Expose line mapping functionality for future ContentReference integration React.useEffect(() => { if (Object.keys(lineMapping).length > 0) { console.debug( "Line mapping updated:", Object.keys(lineMapping).length, "lines" ); } }, [lineMapping]); return (
{/* Action Buttons */}
{hasChanges && ( )}
{/* Status Messages */} {error && (
{error}
)} {validationError && (
Validation Error: {validationError}
)} {saveSuccess && (
Trace content saved successfully!

Note: Trace statistics may need to be regenerated to reflect your changes.

)} {/* File Info & Statistics */} File Information
Characters
{characterCount.toLocaleString()}
Lines
{lineCount.toLocaleString()}
Words
{wordCount.toLocaleString()}
Status
{hasChanges ? ( Modified ) : ( Saved )}
{/* Editor Controls */} Editor Controls
setGotoLineNumber(e.target.value)} className="w-32" min="1" max={lineCount} />
Language: plaintext
{/* Simple Text Editor */} Content Editor {isLoading ? (
) : (
{/* Line Numbers Container */}
{generateLineNumbers()}
{/* Text Editor */}