import React, { useState, useEffect } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Link, Download, Eye, Upload, Hash, FileText, Database, Calendar, } from "lucide-react"; import { useModal } from "@/context/ModalContext"; import { useToast } from "@/hooks/use-toast"; import { useNavigation } from "@/context/NavigationContext"; import { useAgentGraph } from "@/context/AgentGraphContext"; import { api } from "@/lib/api"; import langfuseIcon from "@/static/langfuse.png"; import langsmithIcon from "@/static/langsmith.png"; import { PreprocessingMethodModal } from "@/components/shared/modals/PreprocessingMethodModal"; interface Connection { id: string; platform: string; status: string; connected_at: string; last_sync: string | null; projects: Array<{ id: string; name: string; description?: string; created_at?: string; }>; } interface FetchedTrace { id: string; name: string; platform: string; fetched_at: string; generated_timestamp?: string; imported: boolean; data?: any; connection_id?: string; } export function ConnectionsView() { const [connections, setConnections] = useState([]); const [fetchedTraces, setFetchedTraces] = useState([]); const [selectedTraces, setSelectedTraces] = useState>(new Set()); const [fetchingConnections, setFetchingConnections] = useState>( new Set() ); const [selectedProjects, setSelectedProjects] = useState>( new Map() ); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [connectionToDelete, setConnectionToDelete] = useState(null); const [previewDialogOpen, setPreviewDialogOpen] = useState(false); const [previewTrace, setPreviewTrace] = useState(null); const [preprocessingModalOpen, setPreprocessingModalOpen] = useState(false); const [tracesToImport, setTracesToImport] = useState([]); const [isImporting, setIsImporting] = useState(false); const { openModal } = useModal(); const { toast } = useToast(); const { actions: navigationActions } = useNavigation(); const { actions: agentGraphActions } = useAgentGraph(); const loadConnections = async () => { try { const response = await api.observability.getConnections(); setConnections( response.connections.map((conn: any) => ({ ...conn, last_sync: null, // Add missing field projects: conn.projects || [], // Add projects field })) || [] ); // Load fetched traces for each connection using connection-specific API const allFetchedTraces: FetchedTrace[] = []; for (const conn of response.connections) { try { const fetchedResponse = await api.observability.getFetchedTracesByConnection( conn.id ); const tracesWithConnectionId = fetchedResponse.traces.map((trace: any) => ({ ...trace, connection_id: conn.id })); allFetchedTraces.push(...tracesWithConnectionId); } catch (error) { console.warn( `Failed to load fetched traces for connection ${conn.id}:`, error ); } } setFetchedTraces(allFetchedTraces); } catch (error) { console.warn("Failed to load connections:", error); setConnections([]); } }; useEffect(() => { loadConnections(); }, []); useEffect(() => { const handleConnectionUpdate = () => { loadConnections(); }; window.addEventListener( "observability-connection-updated", handleConnectionUpdate ); return () => { window.removeEventListener( "observability-connection-updated", handleConnectionUpdate ); }; }, []); // Auto-select first project for connections that don't have a project selected useEffect(() => { if (connections.length > 0) { const newSelected = new Map(selectedProjects); let hasChanges = false; connections.forEach(connection => { if (!selectedProjects.has(connection.id) && connection.projects && connection.projects.length > 0) { const firstProject = connection.projects[0]; if (firstProject?.name) { newSelected.set(connection.id, firstProject.name); hasChanges = true; } } }); if (hasChanges) { setSelectedProjects(newSelected); } } }, [connections, selectedProjects]); const handleAddConnection = () => { openModal("observability-connection", "Connect to AI Observability"); }; const handleEditConnection = (connection: Connection) => { openModal( "observability-connection", `Edit ${connection.platform} Connection`, { editConnection: connection, } ); }; const handleDeleteConnection = async (connection: Connection) => { setConnectionToDelete(connection); setDeleteDialogOpen(true); }; const confirmDelete = async () => { if (!connectionToDelete) return; try { await api.observability.deleteConnection(connectionToDelete.id); loadConnections(); setDeleteDialogOpen(false); setConnectionToDelete(null); } catch (error) { console.error("Failed to delete connection:", error); } }; const cancelDelete = () => { setDeleteDialogOpen(false); setConnectionToDelete(null); }; const handleFetchTraces = async (connectionId: string, projectName?: string) => { setFetchingConnections((prev) => new Set(prev).add(connectionId)); try { // Get the connection to find project info const connection = connections.find(conn => conn.id === connectionId); const selectedProjectName = selectedProjects.get(connectionId) || projectName; let projectId: string | undefined; if (connection && selectedProjectName) { const selectedProject = connection.projects.find(p => p.name === selectedProjectName); projectId = selectedProject?.id; } // Trigger fetch from specific connection with project info await api.observability.fetchTracesByConnection(connectionId, 50, projectId, selectedProjectName); // Reload all traces to ensure consistency loadConnections(); } catch (error) { console.error("Failed to fetch traces:", error); } finally { setFetchingConnections((prev) => { const newSet = new Set(prev); newSet.delete(connectionId); return newSet; }); } }; const handleDownloadTrace = async (trace: FetchedTrace) => { try { // Much simpler! Just need the trace ID const response = await api.observability.downloadTrace(trace.id); const jsonContent = JSON.stringify(response.data, null, 2); // Create blob and download const blob = new Blob([jsonContent], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${trace.name}_${trace.platform}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); toast({ title: "Download Complete", description: `${trace.name} has been downloaded successfully.`, }); } catch (error) { console.error("Error downloading trace:", error); toast({ title: "Download Failed", description: "Failed to download trace. Please try again.", variant: "destructive", }); } }; const handleBulkImport = async () => { const selectedTracesToImport = fetchedTraces.filter( (t) => selectedTraces.has(t.id) && !t.imported ); if (selectedTracesToImport.length === 0) return; // Store traces to import and show preprocessing method modal setTracesToImport(selectedTracesToImport); setPreprocessingModalOpen(true); }; const handlePreprocessingMethodConfirm = async (preprocessingOptions: { max_char: number | null; topk: number; raw: boolean; hierarchy: boolean; replace: boolean; }) => { setIsImporting(true); setPreprocessingModalOpen(false); const successfulImports: string[] = []; const failedImports: string[] = []; try { // Process each trace by uploading the preprocessed data for (const trace of tracesToImport) { try { console.log( `Processing trace ${trace.id} from ${trace.platform} with preprocessing options:`, preprocessingOptions ); // Find the connection for this trace const connection = connections.find(conn => conn.id === trace.connection_id); if (!connection) { throw new Error(`No connection found for trace ${trace.id}`); } // Call backend import API using connection-specific endpoint const result = await api.observability.importTracesByConnection( connection.id, [trace.id], preprocessingOptions ); if (result.imported > 0) { successfulImports.push(trace.id); console.log(`Successfully processed trace ${trace.id}`); } else { failedImports.push(trace.id); console.log(`Failed to process trace ${trace.id}:`, result.errors); } } catch (error) { console.error(`Failed to process trace ${trace.id}:`, error); failedImports.push(trace.id); } } // Update UI state for successfully imported traces if (successfulImports.length > 0) { setFetchedTraces((prev) => prev.map((t) => successfulImports.includes(t.id) ? { ...t, imported: true } : t ) ); } // Clear selection setSelectedTraces(new Set()); // Refresh traces list after successful import if (successfulImports.length > 0) { try { const tracesData = await api.traces.list(); // Update traces in AgentGraphContext agentGraphActions.setTraces( Array.isArray(tracesData) ? tracesData : [] ); } catch (error) { console.error("Failed to refresh traces list:", error); } } // Show appropriate toast based on results if (successfulImports.length > 0 && failedImports.length === 0) { toast({ title: "Traces Imported Successfully", description: `${successfulImports.length} trace${ successfulImports.length > 1 ? "s" : "" } imported to your traces.`, variant: "default", }); // Navigate to main page (traces section) only if all imports succeeded navigationActions.setCurrentSection("traces"); } else if (successfulImports.length > 0 && failedImports.length > 0) { toast({ title: "Partial Import Success", description: `${successfulImports.length} traces imported successfully, ${failedImports.length} failed.`, variant: "default", }); } else { toast({ title: "Import Failed", description: "Failed to import all selected traces. Please try again.", variant: "destructive", }); } } catch (error) { console.error("Failed to import traces:", error); toast({ title: "Import Failed", description: "Failed to import traces. Please try again.", variant: "destructive", }); } finally { setIsImporting(false); setTracesToImport([]); } }; const handlePreprocessingMethodCancel = () => { setPreprocessingModalOpen(false); setTracesToImport([]); }; const handlePreviewTrace = (trace: FetchedTrace) => { setPreviewTrace(trace); setPreviewDialogOpen(true); }; const getPlatformIcon = (platform: string) => { switch (platform.toLowerCase()) { case "langfuse": return ( Langfuse ); case "langsmith": return ( LangSmith ); default: return "AI"; } }; if (connections.length === 0) { return (

Connect AI Observability

Connect to your AI observability platforms to fetch and import traces

); } return ( <>
{/* Header */}

Connections

Manage your AI observability platform connections

{/* Platform Connection Cards */}
{connections.map((connection) => { const isFetching = fetchingConnections.has(connection.id); const selectedProject = selectedProjects.get(connection.id); const connectionTraces = fetchedTraces.filter( (trace) => trace.connection_id === connection.id ); const connectionSelectedTraces = new Set( Array.from(selectedTraces).filter((id) => connectionTraces.some((trace) => trace.id === id) ) ); return ( {/* Connection Header */}
{getPlatformIcon(connection.platform)}

{connection.platform === "langfuse" ? "Langfuse" : "LangSmith"}

Connected{" "} {new Date( connection.connected_at ).toLocaleDateString()}

{/* Fetch Controls */}
{/* Project Selection */} {connection.projects && connection.projects.length > 0 && (
)} {/* Fetch Button */}
{/* Connection Fetched Traces */} {connectionTraces.length > 0 && (

Fetched Traces ({connectionTraces.length})

{/* Import All Button */} {/* Import Selected Button */}
{connectionTraces.map((trace) => ( ))}
{ const newSelected = new Set( selectedTraces ); if (e.target.checked) { connectionTraces.forEach((trace) => newSelected.add(trace.id) ); } else { connectionTraces.forEach((trace) => newSelected.delete(trace.id) ); } setSelectedTraces(newSelected); }} checked={ connectionTraces.length > 0 && connectionTraces.every((trace) => selectedTraces.has(trace.id) ) } /> Name Fetch Time Generated At Actions
{ const newSelected = new Set( selectedTraces ); if (e.target.checked) { newSelected.add(trace.id); } else { newSelected.delete(trace.id); } setSelectedTraces(newSelected); }} /> {trace.name} {trace.imported && ( Imported )}
{new Date( trace.fetched_at ).toLocaleString()} {trace.generated_timestamp ? new Date( trace.generated_timestamp ).toLocaleString() : new Date( trace.fetched_at ).toLocaleString()}
)}
); })}
{/* Delete Confirmation Dialog */} Delete Connection Are you sure you want to delete the {connectionToDelete?.platform}{" "} connection? This action cannot be undone. {/* Preview Trace Dialog */} Preview {previewTrace?.name} {previewTrace?.platform} trace data {/* Content Statistics */} {previewTrace && (
{JSON.stringify( previewTrace.data, null, 2 ).length.toLocaleString()}{" "} characters {JSON.stringify(previewTrace.data, null, 2) .split("\n") .length.toLocaleString()}{" "} lines {(() => { const content = JSON.stringify(previewTrace.data, null, 2); const wordCount = content.trim() ? content.trim().split(/\s+/).length : 0; return wordCount.toLocaleString(); })()}{" "} words {previewTrace.generated_timestamp ? new Date( previewTrace.generated_timestamp ).toLocaleString() : new Date(previewTrace.fetched_at).toLocaleString()}
)}
              {previewTrace ? JSON.stringify(previewTrace.data, null, 2) : ""}
            
{/* Preprocessing Method Selection Modal */} ); }