// ─── Types ──────────────────────────────────────────────────────────────────── export interface LoginResponse { status: string; message: string; data: { id: string; fullname: string; email: string; company: string; company_size: string; function: string; site: string; role: string; status: string; created_at: string; }; } export interface Room { id: string; title: string; user_id?: string; status?: string; created_at: string; updated_at: string | null; } export interface CreateRoomResponse { status: string; message: string; data: Room; } export interface ChatSource { id?: string; message_id?: string; document_id: string; filename: string; page_label: string | null; created_at?: string; } export interface RoomMessage { id: string; role: "user" | "assistant"; content: string; created_at: string; sources?: ChatSource[]; } export interface RoomDetail extends Room { messages: RoomMessage[]; } export type DocumentStatus = "uploading" | "uploaded" | "processing" | "completed" | "failed"; export interface ApiDocument { id: string; user_id?: string; filename: string; blob_name?: string; status: DocumentStatus; file_size: number; file_type: string; created_at: string; processed_at?: string; error_message?: string; } export interface UploadDocumentResponse { status: string; message: string; data: { id: string; user_id: string; filename: string; blob_name: string; file_size: number; file_type: string; status: DocumentStatus; created_at: string; }; } export interface DocTypeInfo { type: string; max_size_mb: number; status: "active" | "inactive"; message: string | null; } export interface DataCatalogSource { source_id: string; source_type: "schema" | "tabular" | "unstructured"; name: string; location_ref: string; table_count?: number; updated_at?: string; } export interface DataCatalog { user_id: string; schema_version: string; generated_at: string; sources: DataCatalogSource[]; } // ─── Base Clients ───────────────────────────────────────────────────────────── import { getEnv } from "@/env"; const ORCHESTRATION_BASE_URL = getEnv("VITE_ORCHESTRATION_API_BASE_URL"); const AGENTIC_BASE_URL = getEnv("VITE_AGENTIC_API_BASE_URL"); async function request(baseUrl: string, path: string, options?: RequestInit): Promise { const res = await fetch(`${baseUrl}${path}`, { headers: { "Content-Type": "application/json", ...options?.headers }, ...options, }); if (!res.ok) { const err = await res .json() .catch(() => ({ message: `HTTP ${res.status}` })); throw new Error(err.message ?? `HTTP ${res.status}`); } return res.json() as Promise; } // ─── Auth ───────────────────────────────────────────────────────────────────── export const login = (email: string, password: string) => request(ORCHESTRATION_BASE_URL, "/api/login", { method: "POST", body: JSON.stringify({ email, password }), }); // ─── Rooms ──────────────────────────────────────────────────────────────────── export const getRooms = (userId: string): Promise => request<{ status: string; message: string; data: Room[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/chat-rooms?user_id=${userId}`) .then(res => res.data ?? []); export const getRoom = (roomId: string): Promise => request<{ status: string; message: string; data: RoomDetail }>(ORCHESTRATION_BASE_URL, `/api/v1/chat-rooms/${roomId}`) .then(res => res.data); export const deleteRoom = (roomId: string, userId: string) => request<{ status: string; message: string }>( ORCHESTRATION_BASE_URL, `/api/v1/chat-rooms/${roomId}?user_id=${userId}`, { method: "DELETE" } ); export const createRoom = (userId: string, title?: string) => request(ORCHESTRATION_BASE_URL, "/api/v1/chat-rooms", { method: "POST", body: JSON.stringify({ user_id: userId, title }), }); // ─── Documents ──────────────────────────────────────────────────────────────── export const getDocuments = (userId: string): Promise => request<{ status: string; message: string; data: ApiDocument[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/documents/${userId}`) .then(res => res.data ?? []); export const uploadDocument = ( userId: string, file: File, onProgress?: (percent: number) => void ): Promise => new Promise((resolve, reject) => { const form = new FormData(); form.append("user_id", userId); form.append("file", file); console.log(`[upload] ▶ XHR start — file: "${file.name}", size: ${(file.size / 1024).toFixed(1)} KB`); const t0 = performance.now(); const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { const pct = Math.round((e.loaded / e.total) * 100); console.log(`[upload] ↑ ${pct}% — ${(e.loaded / 1024).toFixed(0)} / ${(e.total / 1024).toFixed(0)} KB`); onProgress?.(pct); } }; xhr.onload = () => { console.log(`[upload] ◀ HTTP ${xhr.status}, elapsed: ${(performance.now() - t0).toFixed(0)} ms`); if (xhr.status >= 200 && xhr.status < 300) { try { const data = JSON.parse(xhr.responseText) as UploadDocumentResponse; console.log(`[upload] ✓ done — total: ${(performance.now() - t0).toFixed(0)} ms`); resolve(data); } catch { reject(new Error("Failed to parse response")); } } else { try { const err = JSON.parse(xhr.responseText) as { message?: string }; reject(new Error(err.message ?? `HTTP ${xhr.status}`)); } catch { reject(new Error(`HTTP ${xhr.status}`)); } } }; xhr.onerror = () => reject(new Error("Network error during upload")); xhr.open("POST", `${ORCHESTRATION_BASE_URL}/api/v1/document/upload`); xhr.send(form); }); export const processDocument = async (userId: string, documentId: string) => { console.log(`[process] ▶ fetch start — document_id: ${documentId}`); const t0 = performance.now(); const result = await request<{ status: string; message: string; data: { document_id: string; file_type: string; status: string } }>( ORCHESTRATION_BASE_URL, `/api/v1/document/process`, { method: "POST", body: JSON.stringify({ user_id: userId, document_id: documentId }) } ); console.log(`[process] ✓ accepted — elapsed: ${(performance.now() - t0).toFixed(0)} ms, status: ${result.data?.status}`); return result; }; export const deleteDocument = (userId: string, documentId: string) => request<{ status: string; message: string }>( ORCHESTRATION_BASE_URL, `/api/v1/document/delete`, { method: "DELETE", body: JSON.stringify({ document_id: documentId, user_id: userId }) } ); export const getDocumentTypes = (): Promise => request<{ status: string; data: DocTypeInfo[] }>(ORCHESTRATION_BASE_URL, "/api/v1/documents/doctypes").then( (res) => res.data ); // ─── Database Clients ───────────────────────────────────────────────────────── export type DbType = "postgres" | "mysql" | "sqlserver" | "supabase" | "bigquery" | "snowflake"; export interface DbTypeField { name: string; type: "string" | "integer" | "select" | "boolean"; required: boolean; default: string | number | boolean | null; description: string; options?: string[]; sensitive?: boolean; } export interface DbTypeInfo { db_type: DbType; display_name: string; logo: string; status: "active" | "inactive"; message: string | null; fields: DbTypeField[]; } export interface DatabaseClient { id: string; user_id: string; name: string; db_type: DbType; status: "active" | "inactive"; created_at: string; updated_at: string | null; } export interface IngestColumn { name: string; data_type: string; nullable: boolean; } export interface IngestTable { name: string; row_count: number; columns: IngestColumn[]; fks: Array<{ column_name: string; foreign_table: string; foreign_column_name: string }>; } export interface IngestResponse { tables: IngestTable[]; } export const getDatabaseClientTypes = (): Promise => request<{ status: string; message: string; data: DbTypeInfo[] | null }>(ORCHESTRATION_BASE_URL, "/api/v1/database-clients/dbtypes") .then(res => res.data ?? []); export const connectDatabase = ( userId: string, dbType: DbType, name: string, credentials: Record ): Promise => request<{ status: string; message: string; data: DatabaseClient }>(ORCHESTRATION_BASE_URL, `/api/v1/database-clients`, { method: "POST", body: JSON.stringify({ user_id: userId, name, db_type: dbType, credentials }), }).then(res => res.data); export const getDatabaseClients = (userId: string): Promise => request<{ status: string; message: string; data: DatabaseClient[] | null }>(ORCHESTRATION_BASE_URL, `/api/v1/database-clients/${userId}`) .then(res => res.data ?? []); export const deleteDatabaseClient = (clientId: string, userId: string) => request<{ status: string; message: string }>( ORCHESTRATION_BASE_URL, `/api/v1/database-clients/${clientId}?user_id=${userId}`, { method: "DELETE" } ); export const ingestDatabaseClient = (clientId: string, userId: string): Promise => request<{ status: string; message: string; data: IngestResponse }>( ORCHESTRATION_BASE_URL, `/api/v1/database-clients/${clientId}/ingest?user_id=${userId}`, { method: "POST" } ).then(res => res.data); // ─── Data Catalog ───────────────────────────────────────────────────────────── export const getDataCatalog = (userId: string): Promise => request<{ status: string; message: string; data: DataCatalog }>( ORCHESTRATION_BASE_URL, `/api/v1/data-catalog/${userId}` ).then((res) => res.data); export const rebuildDataCatalog = (userId: string): Promise => request<{ status: string; message: string; data: DataCatalog }>( ORCHESTRATION_BASE_URL, "/api/v1/data-catalog/rebuild", { method: "POST", body: JSON.stringify({ user_id: userId }) } ).then((res) => res.data); // ─── Chat ───────────────────────────────────────────────────────────────────── export const streamChat = ( userId: string, roomId: string, message: string ): Promise => fetch(`${AGENTIC_BASE_URL}/api/v1/chat/stream`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, room_id: roomId, message }), });