import { Trace, KnowledgeGraph, Entity, Relation, GraphComparisonOptions, GraphComparisonResults, GraphListResponse, GraphDetailsResponse, PerturbationConfig, } from "@/types"; import { UpdateContextRequest, ContextDocumentResponse } from "@/types/context"; const API_BASE = "/api"; class ApiError extends Error { constructor(public status: number, message: string) { super(message); this.name = "ApiError"; } } async function fetchApi( endpoint: string, options?: RequestInit, retryCount = 0 ): Promise { const response = await fetch(`${API_BASE}${endpoint}`, { headers: { "Content-Type": "application/json", ...options?.headers, }, credentials: "include", // Ensure cookies are sent with requests ...options, }); if (!response.ok) { // Handle 401 (Unauthorized) - session expired, redirect to login if (response.status === 401) { console.warn("🔐 Session expired - redirecting to login..."); // Redirect to login page window.location.href = "/auth/login-page"; throw new ApiError(401, "Session expired. Please log in again."); } // Handle 429 (Too Many Requests) with exponential backoff if (response.status === 429 && retryCount < 3) { const backoffDelay = Math.pow(2, retryCount) * 1000; // 1s, 2s, 4s console.warn( `🚨 Rate limit hit (429), retrying in ${backoffDelay}ms... (attempt ${ retryCount + 1 }/3)` ); await new Promise((resolve) => setTimeout(resolve, backoffDelay)); return fetchApi(endpoint, options, retryCount + 1); } throw new ApiError(response.status, `API Error: ${response.statusText}`); } return response.json(); } export const api = { // Traces traces: { list: async () => { const response = await fetchApi<{ status: string; traces: Trace[] }>( "/traces/" ); return response.traces; }, get: (id: string) => fetchApi(`/traces/${id}`), getEnhancedStatistics: async (id: string) => { const response = await fetchApi<{ status: string; enhanced_statistics: any; has_schema_analytics: boolean; }>(`/traces/${id}/enhanced-statistics`); return response; }, getContent: async (id: string) => { const response = await fetchApi<{ content: string }>( `/traces/${id}/content` ); return response.content; }, getNumberedContent: async (id: string) => { const response = await fetchApi<{ content: string }>( `/traces/${id}/content-numbered` ); return response.content; }, extractSegment: async ( traceId: string, startChar: number, endChar: number ) => { console.log("Extracting segment:", { traceId, startChar, endChar }); const response = await fetchApi<{ content: string }>( `/traces/${traceId}/content` ); const fullContent = response.content || ""; console.log("Full content length:", fullContent.length); console.log("Full content preview:", fullContent.substring(0, 200)); // Extract segment using character positions const segmentContent = fullContent.substring(startChar, endChar); console.log("Segment content length:", segmentContent.length); console.log("Segment content preview:", segmentContent.substring(0, 200)); return { content: segmentContent, fullContent, startChar, endChar, segmentLength: segmentContent.length, }; }, delete: (id: string) => fetchApi<{ message: string }>(`/traces/${id}`, { method: "DELETE", }), upload: (file: File, onProgress?: (progress: number) => void) => { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append("trace_file", file); const xhr = new XMLHttpRequest(); if (onProgress) { xhr.upload.addEventListener("progress", (e) => { if (e.lengthComputable) { const progress = (e.loaded / e.total) * 100; onProgress(progress); } }); } xhr.addEventListener("load", () => { if (xhr.status >= 200 && xhr.status < 300) { const response = JSON.parse(xhr.responseText); // Transform the backend response to match the Trace interface if (response.status === "success" && response.trace_id) { const trace: Trace = { id: 0, // Will be updated when we fetch the full trace trace_id: response.trace_id, filename: file.name, title: response.title || file.name, character_count: response.character_count, turn_count: response.turn_count, status: "uploaded", // Default status for newly uploaded traces upload_timestamp: new Date().toISOString(), trace_type: "user_upload", trace_source: "react_app", tags: ["uploaded"], knowledge_graphs: [], }; resolve(trace); } else { reject( new ApiError(xhr.status, response.message || "Upload failed") ); } } else { reject(new ApiError(xhr.status, xhr.statusText)); } }); xhr.addEventListener("error", () => { reject(new ApiError(0, "Network error")); }); xhr.open("POST", `${API_BASE}/traces/`); xhr.send(formData); }); }, process: (id: string, splitterType: string = "agent_semantic") => fetchApi<{ task_id: string }>(`/traces/${id}/process`, { method: "POST", body: JSON.stringify({ splitter_type: splitterType }), }), generateKnowledgeGraph: ( id: string, splitterType: string = "agent_semantic", forceRegenerate: boolean = true, methodName: string = "production", model: string = "gpt-5-mini", chunkingConfig?: { min_chunk_size?: number; max_chunk_size?: number } ) => { const requestBody = { splitter_type: splitterType, force_regenerate: forceRegenerate, method_name: methodName, model: model, chunking_config: chunkingConfig, }; console.log("API: generateKnowledgeGraph called with:", { id, splitterType, forceRegenerate, methodName, model, chunkingConfig, }); console.log("API: Request body:", requestBody); return fetchApi<{ task_id: string }>(`/traces/${id}/process`, { method: "POST", body: JSON.stringify(requestBody), }); }, // Context Documents context: { list: (traceId: string) => fetchApi(`/traces/${traceId}/context`), create: (traceId: string, data: any) => fetchApi(`/traces/${traceId}/context`, { method: "POST", body: JSON.stringify(data), }), update: ( traceId: string, contextId: string, data: UpdateContextRequest ) => fetchApi( `/traces/${traceId}/context/${contextId}`, { method: "PUT", body: JSON.stringify(data), } ), delete: (traceId: string, contextId: string) => fetchApi(`/traces/${traceId}/context/${contextId}`, { method: "DELETE", }), upload: (traceId: string, formData: FormData) => fetchApi(`/traces/${traceId}/context/upload`, { method: "POST", headers: {}, body: formData, }), }, }, // Knowledge Graphs knowledgeGraphs: { list: () => fetchApi("/knowledge-graphs"), get: (id: string) => fetchApi(`/knowledge-graphs/${id}`), getData: (id: string) => fetchApi<{ entities: Entity[]; relations: Relation[]; metadata?: Record; }>(`/knowledge-graphs/${id}`), enrich: (id: string) => fetchApi<{ task_id: string }>(`/knowledge-graphs/${id}/enrich`, { method: "POST", }), perturb: (id: string, config?: PerturbationConfig) => fetchApi<{ task_id: string; config?: PerturbationConfig }>( `/knowledge-graphs/${id}/perturb`, { method: "POST", headers: config ? { "Content-Type": "application/json" } : undefined, body: config ? JSON.stringify(config) : undefined, } ), analyze: (id: string) => fetchApi<{ task_id: string }>(`/knowledge-graphs/${id}/analyze`, { method: "POST", }), getStatus: (id: string) => fetchApi(`/knowledge-graphs/${id}/status`), getStageResults: (id: string, stage: string) => fetchApi(`/knowledge-graphs/${id}/stage-results/${stage}`), reset: (id: string) => fetchApi<{ message: string }>(`/reset-knowledge-graph/${id}`, { method: "POST", }), }, // Tasks tasks: { getStatus: (id: string) => fetchApi(`/tasks/${id}/status`), get: (id: string) => fetchApi(`/tasks/${id}`), }, // Graph Comparison graphComparison: { listAvailableGraphs: () => fetchApi("/graph-comparison/graphs"), compareGraphs: ( graph1Id: number, graph2Id: number, options?: GraphComparisonOptions ) => fetchApi("/graph-comparison/compare", { method: "POST", body: JSON.stringify({ graph1_id: graph1Id, graph2_id: graph2Id, ...options, }), }), getComparison: ( graph1Id: number, graph2Id: number, similarityThreshold?: number ) => fetchApi( `/graph-comparison/compare/${graph1Id}/${graph2Id}${ similarityThreshold ? `?similarity_threshold=${similarityThreshold}` : "" }` ), getGraphDetails: (graphId: number) => fetchApi(`/graph-comparison/graphs/${graphId}`), getCacheInfo: () => fetchApi("/graph-comparison/cache/info"), clearCache: () => fetchApi<{ message: string }>("/graph-comparison/cache/clear", { method: "DELETE", }), }, // Example Traces exampleTraces: { list: (subset?: string) => fetchApi( `/example-traces/${ subset ? `?subset=${encodeURIComponent(subset)}` : "" }` ), get: (subset: string, id: number) => fetchApi( `/example-traces/${encodeURIComponent(subset)}/${id}` ), import: (subset: string, id: number) => fetchApi(`/example-traces/import`, { method: "POST", body: JSON.stringify({ subset, id }), }), }, // Methods methods: { getAvailable: () => fetchApi<{ methods: Record }>("/methods/available"), }, // AI Observability observability: { connect: (connectionData: { platform: string; publicKey: string; secretKey: string; host?: string; }) => fetchApi<{ status: string; message: string; connection_id: string }>( "/observability/connect", { method: "POST", body: JSON.stringify(connectionData), } ), updateConnection: ( connectionId: string, connectionData: { platform: string; publicKey: string; secretKey: string; host?: string; } ) => fetchApi<{ status: string; message: string }>( `/observability/connections/${connectionId}`, { method: "PUT", body: JSON.stringify(connectionData), } ), deleteConnection: (connectionId: string) => fetchApi<{ status: string; message: string }>( `/observability/connections/${connectionId}`, { method: "DELETE", } ), getConnections: () => fetchApi<{ connections: Array<{ id: string; platform: string; status: string; connected_at: string; }>; }>("/observability/connections"), // Connection-specific methods getFetchedTracesByConnection: (connectionId: string) => fetchApi<{ traces: any[]; total: number }>( `/observability/connections/${connectionId}/fetched-traces` ), fetchTracesByConnection: ( connectionId: string, limit: number, projectId?: string, projectName?: string ) => fetchApi<{ traces: any[]; total: number }>( `/observability/connections/${connectionId}/fetch`, { method: "POST", body: JSON.stringify({ limit, project_id: projectId, project_name: projectName, }), } ), downloadTrace: (traceId: string) => fetchApi<{ data: any }>(`/observability/traces/${traceId}/download`), importTracesByConnection: ( connectionId: string, traceIds: string[], preprocessing?: { max_char?: number | null; topk?: number; raw?: boolean; hierarchy?: boolean; replace?: boolean; } ) => fetchApi<{ imported: number; errors: string[] }>( `/observability/connections/${connectionId}/import`, { method: "POST", body: JSON.stringify({ trace_ids: traceIds, preprocessing: preprocessing, }), } ), }, };