wu981526092's picture
Fix: Handle 401 errors with automatic redirect to login
0e57db8
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<T>(
endpoint: string,
options?: RequestInit,
retryCount = 0
): Promise<T> {
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<Trace>(`/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<Trace>((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<any[]>(`/traces/${traceId}/context`),
create: (traceId: string, data: any) =>
fetchApi<any>(`/traces/${traceId}/context`, {
method: "POST",
body: JSON.stringify(data),
}),
update: (
traceId: string,
contextId: string,
data: UpdateContextRequest
) =>
fetchApi<ContextDocumentResponse>(
`/traces/${traceId}/context/${contextId}`,
{
method: "PUT",
body: JSON.stringify(data),
}
),
delete: (traceId: string, contextId: string) =>
fetchApi<any>(`/traces/${traceId}/context/${contextId}`, {
method: "DELETE",
}),
upload: (traceId: string, formData: FormData) =>
fetchApi<any>(`/traces/${traceId}/context/upload`, {
method: "POST",
headers: {},
body: formData,
}),
},
},
// Knowledge Graphs
knowledgeGraphs: {
list: () => fetchApi<KnowledgeGraph[]>("/knowledge-graphs"),
get: (id: string) => fetchApi<KnowledgeGraph>(`/knowledge-graphs/${id}`),
getData: (id: string) =>
fetchApi<{
entities: Entity[];
relations: Relation[];
metadata?: Record<string, any>;
}>(`/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<KnowledgeGraph>(`/knowledge-graphs/${id}/status`),
getStageResults: (id: string, stage: string) =>
fetchApi<any>(`/knowledge-graphs/${id}/stage-results/${stage}`),
reset: (id: string) =>
fetchApi<{ message: string }>(`/reset-knowledge-graph/${id}`, {
method: "POST",
}),
},
// Tasks
tasks: {
getStatus: (id: string) => fetchApi<any>(`/tasks/${id}/status`),
get: (id: string) => fetchApi<any>(`/tasks/${id}`),
},
// Graph Comparison
graphComparison: {
listAvailableGraphs: () =>
fetchApi<GraphListResponse>("/graph-comparison/graphs"),
compareGraphs: (
graph1Id: number,
graph2Id: number,
options?: GraphComparisonOptions
) =>
fetchApi<GraphComparisonResults>("/graph-comparison/compare", {
method: "POST",
body: JSON.stringify({
graph1_id: graph1Id,
graph2_id: graph2Id,
...options,
}),
}),
getComparison: (
graph1Id: number,
graph2Id: number,
similarityThreshold?: number
) =>
fetchApi<GraphComparisonResults>(
`/graph-comparison/compare/${graph1Id}/${graph2Id}${
similarityThreshold
? `?similarity_threshold=${similarityThreshold}`
: ""
}`
),
getGraphDetails: (graphId: number) =>
fetchApi<GraphDetailsResponse>(`/graph-comparison/graphs/${graphId}`),
getCacheInfo: () => fetchApi<any>("/graph-comparison/cache/info"),
clearCache: () =>
fetchApi<{ message: string }>("/graph-comparison/cache/clear", {
method: "DELETE",
}),
},
// Example Traces
exampleTraces: {
list: (subset?: string) =>
fetchApi<import("@/types").ExampleTraceLite[]>(
`/example-traces/${
subset ? `?subset=${encodeURIComponent(subset)}` : ""
}`
),
get: (subset: string, id: number) =>
fetchApi<import("@/types").ExampleTrace>(
`/example-traces/${encodeURIComponent(subset)}/${id}`
),
import: (subset: string, id: number) =>
fetchApi<import("@/types").Trace>(`/example-traces/import`, {
method: "POST",
body: JSON.stringify({ subset, id }),
}),
},
// Methods
methods: {
getAvailable: () =>
fetchApi<{ methods: Record<string, any> }>("/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,
}),
}
),
},
};