wu981526092's picture
add
7bc750c
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 (
<div className="space-y-6 p-6 max-h-[90vh] overflow-y-auto">
{/* Trace Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<FileText className="h-6 w-6 text-primary" />
</div>
<div>
<h2 className="text-xl font-semibold">{trace.filename}</h2>
<p className="text-sm text-muted-foreground">
Uploaded{" "}
{formatRelativeTime(
trace.upload_timestamp || trace.timestamp || ""
)}
</p>
</div>
</div>
<Badge variant="outline" className={getTraceStatusColor(trace.status)}>
{trace.status}
</Badge>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="p-4 quick-stats-card">
<div className="flex items-center gap-2">
<HardDrive className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-2xl font-bold">
{formatFileSize(trace.character_count || trace.size || 0)}
</p>
<p className="text-xs text-muted-foreground">File Size</p>
</div>
</div>
</Card>
<Card className="p-4 quick-stats-card">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-2xl font-bold">{trace.turn_count || 0}</p>
<p className="text-xs text-muted-foreground">Turns</p>
</div>
</div>
</Card>
<Card className="p-4 quick-stats-card">
<div className="flex items-center gap-2">
<Activity className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-2xl font-bold">{finalKGs}</p>
<p className="text-xs text-muted-foreground">
Final Agent Graphs
</p>
</div>
</div>
</Card>
<Card className="p-4 quick-stats-card">
<div className="flex items-center gap-2">
<Database className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-2xl font-bold">{completedPipelines}</p>
<p className="text-xs text-muted-foreground">Completed</p>
</div>
</div>
</Card>
</div>
{/* Trace Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Trace Information
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Core Information Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Characters */}
<Card className="p-4 bg-muted/30">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<Hash className="h-4 w-4 text-primary" />
</div>
<div className="flex-1">
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">
Content Size
</p>
<p className="text-lg font-semibold">
{trace.character_count?.toLocaleString() || "N/A"} chars
</p>
{trace.turn_count && (
<p className="text-sm text-muted-foreground">
{trace.turn_count} turns
</p>
)}
</div>
</div>
</Card>
{/* Type & Upload Info */}
<Card className="p-4 bg-muted/30">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<FileText className="h-4 w-4 text-primary" />
</div>
<div className="flex-1">
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium">
Type & Date
</p>
<p className="text-lg font-semibold">
{formatTraceType(trace.trace_type)}
</p>
<p className="text-sm text-muted-foreground">
{new Date(
trace.upload_timestamp || trace.timestamp || ""
).toLocaleDateString()}
</p>
</div>
</div>
</Card>
</div>
{/* Description */}
{trace.description && (
<div className="space-y-2">
<p className="text-sm font-medium flex items-center gap-2">
<FileText className="h-4 w-4 text-muted-foreground" />
Description
</p>
<div className="p-4 rounded-lg bg-muted/30 border-l-4 border-primary">
<p className="text-sm text-muted-foreground leading-relaxed">
{trace.description}
</p>
</div>
</div>
)}
{/* Error Message */}
{trace.error_message && (
<div className="space-y-2">
<p className="text-sm font-medium text-destructive flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
Error Message
</p>
<div className="p-4 rounded-lg bg-destructive/10 border border-destructive/20">
<p className="text-sm text-destructive font-mono">
{trace.error_message}
</p>
</div>
</div>
)}
{/* Tags */}
{trace.tags && trace.tags.length > 0 && (
<div className="space-y-3">
<p className="text-sm font-medium flex items-center gap-2">
<Hash className="h-4 w-4 text-muted-foreground" />
Tags
</p>
<div className="flex flex-wrap gap-2">
{trace.tags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="text-xs px-3 py-1"
>
{tag}
</Badge>
))}
</div>
</div>
)}
</CardContent>
</Card>
{/* Context Documents Section */}
<ContextDocumentsSection traceId={trace.trace_id} />
{/* Knowledge Graphs using KnowledgeGraphTree */}
{knowledgeGraphs.length > 0 ? (
<Card>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Agent Graphs ({totalKGs})
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleGenerateKG}
disabled={isGenerating || isPolling}
size="sm"
className="bg-primary hover:bg-primary/90"
>
{isGenerating || isPolling ? (
<>
<div className="w-3 h-3 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
{generationProgress > 0
? `${Math.round(generationProgress)}%`
: "Generating..."}
</>
) : (
<>
<Plus className="h-3 w-3 mr-2" />
Generate
</>
)}
</Button>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{finalKGs} Final</span>
<span></span>
<span>{completedPipelines} Completed</span>
</div>
</div>
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<KnowledgeGraphTree
knowledgeGraphs={knowledgeGraphs}
_onViewKg={handleViewKnowledgeGraph}
onDeleteKg={handleDeleteKnowledgeGraph}
onReplayKg={handleReplayKg}
onViewSegment={handleViewSegment}
currentTraceId={trace.trace_id}
/>
</CardContent>
</Card>
) : (
<Card>
<CardContent className="p-6 text-center empty-state">
<div className="text-muted-foreground">
<Activity className="h-8 w-8 mx-auto mb-3 empty-state-icon" />
<p className="font-medium mb-2">Ready to analyze this trace</p>
<p className="text-sm text-muted-foreground mb-1">
Transform your trace data into an interactive agent graph.
</p>
<p className="text-xs text-muted-foreground mb-4">
Choose from different chunking strategies for optimal analysis.
</p>
</div>
<Button
onClick={handleGenerateKG}
className="mt-2 bg-primary hover:bg-primary/90"
size="sm"
>
<Plus className="h-4 w-4 mr-2" />
Generate Agent Graph
</Button>
</CardContent>
</Card>
)}
{/* Metadata */}
{trace.metadata && Object.keys(trace.metadata).length > 0 && (
<Card>
<CardHeader>
<CardTitle>Metadata</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{Object.entries(trace.metadata).map(([key, value]) => (
<div key={key} className="flex justify-between">
<span className="text-sm font-medium">{key}:</span>
<span className="text-sm text-muted-foreground">
{typeof value === "object"
? JSON.stringify(value, null, 2)
: String(value)}
</span>
</div>
))}
</div>
</CardContent>
</Card>
)}
{/* Actions */}
<div className="flex gap-2 pt-4 border-t action-button-group">
<Button onClick={handleViewContent}>
<FileText className="h-4 w-4 mr-2" />
View Content
</Button>
<Button variant="outline" onClick={handleGenerateKG}>
<Activity className="h-4 w-4 mr-2" />
Generate Knowledge Graph
</Button>
<Button variant="outline" onClick={handleDownloadTrace}>
<Download className="h-4 w-4 mr-2" />
Download Trace
</Button>
<Button variant="destructive" onClick={handleDeleteTrace}>
<Trash2 className="h-4 w-4 mr-2" />
Delete Trace
</Button>
</div>
<SplitterSelectionModal
open={isSplitterModalOpen}
onOpenChange={setIsSplitterModalOpen}
onConfirm={handleSplitterConfirm}
isLoading={isGenerating}
/>
</div>
);
}