Spaces:
Sleeping
Sleeping
| import { getEnv } from "@/env"; | |
| const ORCHESTRATION_BASE_URL = getEnv("VITE_ORCHESTRATION_API_BASE_URL"); | |
| const SESSION_STORAGE_KEY = "chatbot_user"; | |
| export interface ApiEnvelope<T> { | |
| status?: "success" | "error" | string; | |
| message?: string; | |
| data: T; | |
| } | |
| export class ApiError extends Error { | |
| status: number; | |
| data: unknown; | |
| constructor(message: string, status: number, data?: unknown) { | |
| super(message); | |
| this.name = "ApiError"; | |
| this.status = status; | |
| this.data = data; | |
| } | |
| } | |
| export interface AuthUser { | |
| id: string; | |
| email: string; | |
| fullname: string; | |
| role?: string; | |
| status?: string; | |
| } | |
| export interface AuthTokens { | |
| access_token: string; | |
| refresh_token: string; | |
| token_type?: string; | |
| expires_in?: number; | |
| refresh_expires_in?: number; | |
| } | |
| export interface UserSession extends AuthTokens { | |
| user_id: string; | |
| email: string; | |
| name: string; | |
| role?: string; | |
| status?: string; | |
| loginTime: string; | |
| expires_at?: string; | |
| refresh_expires_at?: string; | |
| } | |
| export interface LoginData extends AuthTokens { | |
| user: AuthUser; | |
| } | |
| export type LoginResponse = ApiEnvelope<LoginData>; | |
| export type RefreshResponse = ApiEnvelope<LoginData>; | |
| export type DocumentStatus = "uploading" | "uploaded" | "processing" | "processed" | "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 | null; | |
| error_message?: string | null; | |
| } | |
| export interface UploadDocumentResponse { | |
| status: string; | |
| message: string; | |
| data: ApiDocument; | |
| } | |
| export interface DocTypeInfo { | |
| type: string; | |
| max_size_mb: number; | |
| status: "active" | "inactive"; | |
| message: string | null; | |
| } | |
| export type DbType = "postgres" | "mysql" | "sqlserver" | "supabase" | "bigquery" | "snowflake" | string; | |
| 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" | string; | |
| 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 interface DataCatalogSource { | |
| source_id: string; | |
| source_type: "schema" | "tabular" | "unstructured" | string; | |
| 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[]; | |
| } | |
| export interface DataBindItem { | |
| id: string; | |
| name: string; | |
| group_type: "document" | "database"; | |
| type: string; | |
| } | |
| export interface Analysis { | |
| id: string; | |
| user_id: string; | |
| analysis_title: string; | |
| objective: string; | |
| business_questions: string[]; | |
| status: "active" | "inactive" | string; | |
| data_bind: DataBindItem[]; | |
| data_bind_version: number; | |
| report_collection?: unknown[]; | |
| created_at: string; | |
| updated_at: string; | |
| } | |
| export interface AnalysisListResponse { | |
| analyses: Analysis[]; | |
| pagination?: { | |
| page: number; | |
| limit: number; | |
| total?: number; | |
| total_pages?: number; | |
| }; | |
| } | |
| export interface AnalysisMessage { | |
| id: string; | |
| analysis_id: string; | |
| user_id: string; | |
| role: "user" | "ai"; | |
| content: string; | |
| message_id?: string | null; | |
| status?: "success" | "failed"; | |
| note?: string; | |
| created_at: string; | |
| } | |
| export const RESERVED_FAILED_MESSAGE_ID = "00000000-0000-0000-0000-000000000000"; | |
| export interface CreateAnalysisPayload { | |
| analysis_title: string; | |
| objective: string; | |
| business_questions: string[]; | |
| data_bind: DataBindItem[]; | |
| } | |
| export interface UpdateAnalysisPayload { | |
| analysis_title?: string; | |
| objective?: string; | |
| status?: "active" | "inactive"; | |
| } | |
| function readStoredSession(): UserSession | null { | |
| try { | |
| const raw = localStorage.getItem(SESSION_STORAGE_KEY); | |
| return raw ? (JSON.parse(raw) as UserSession) : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function writeStoredSession(session: UserSession): void { | |
| localStorage.setItem(SESSION_STORAGE_KEY, JSON.stringify(session)); | |
| } | |
| function clearStoredSession(): void { | |
| localStorage.removeItem(SESSION_STORAGE_KEY); | |
| } | |
| function secondsFromNow(seconds?: number): string | undefined { | |
| if (!seconds) return undefined; | |
| return new Date(Date.now() + seconds * 1000).toISOString(); | |
| } | |
| function sessionFromAuthData(data: LoginData, previous?: UserSession | null): UserSession { | |
| return { | |
| user_id: data.user.id, | |
| email: data.user.email, | |
| name: data.user.fullname, | |
| role: data.user.role, | |
| status: data.user.status, | |
| access_token: data.access_token, | |
| refresh_token: data.refresh_token, | |
| token_type: data.token_type ?? "Bearer", | |
| expires_in: data.expires_in, | |
| refresh_expires_in: data.refresh_expires_in, | |
| expires_at: secondsFromNow(data.expires_in), | |
| refresh_expires_at: secondsFromNow(data.refresh_expires_in), | |
| loginTime: previous?.loginTime ?? new Date().toISOString(), | |
| }; | |
| } | |
| async function parseError(res: Response): Promise<ApiError> { | |
| const body = await res.json().catch(() => null); | |
| const message = | |
| (body && typeof body === "object" && "message" in body && String((body as { message?: string }).message)) || | |
| (body && typeof body === "object" && "detail" in body && String((body as { detail?: string }).detail)) || | |
| `HTTP ${res.status}`; | |
| return new ApiError(message, res.status, body); | |
| } | |
| let _refreshPromise: Promise<UserSession | null> | null = null; | |
| async function refreshWithStoredSession(): Promise<UserSession | null> { | |
| if (_refreshPromise) return _refreshPromise; | |
| _refreshPromise = (async () => { | |
| const current = readStoredSession(); | |
| if (!current?.refresh_token) return null; | |
| const res = await fetch(`${ORCHESTRATION_BASE_URL}/api/refresh`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ refresh_token: current.refresh_token }), | |
| }); | |
| if (!res.ok) { | |
| clearStoredSession(); | |
| return null; | |
| } | |
| const envelope = (await res.json()) as RefreshResponse; | |
| const next = sessionFromAuthData(envelope.data, current); | |
| writeStoredSession(next); | |
| return next; | |
| })().finally(() => { | |
| _refreshPromise = null; | |
| }); | |
| return _refreshPromise; | |
| } | |
| async function orchestrationRequest<T>(path: string, options: RequestInit = {}, retry = true): Promise<T> { | |
| const session = readStoredSession(); | |
| const headers = new Headers(options.headers); | |
| const hasFormBody = options.body instanceof FormData; | |
| if (!hasFormBody && !headers.has("Content-Type")) { | |
| headers.set("Content-Type", "application/json"); | |
| } | |
| if (session?.access_token && !headers.has("Authorization")) { | |
| headers.set("Authorization", `${session.token_type ?? "Bearer"} ${session.access_token}`); | |
| } | |
| const res = await fetch(`${ORCHESTRATION_BASE_URL}${path}`, { | |
| ...options, | |
| headers, | |
| }); | |
| if (res.status === 401 && retry) { | |
| const refreshed = await refreshWithStoredSession(); | |
| if (refreshed) { | |
| return orchestrationRequest<T>(path, options, false); | |
| } | |
| } | |
| if (!res.ok) throw await parseError(res); | |
| if (res.status === 204) return undefined as T; | |
| return (await res.json()) as T; | |
| } | |
| export async function login(email: string, password: string): Promise<LoginResponse> { | |
| return orchestrationRequest<LoginResponse>("/api/login", { | |
| method: "POST", | |
| body: JSON.stringify({ email, password }), | |
| }, false); | |
| } | |
| export async function refreshSession(refreshToken: string): Promise<RefreshResponse> { | |
| return orchestrationRequest<RefreshResponse>("/api/refresh", { | |
| method: "POST", | |
| body: JSON.stringify({ refresh_token: refreshToken }), | |
| }, false); | |
| } | |
| export function persistAuthData(data: LoginData): UserSession { | |
| const session = sessionFromAuthData(data, readStoredSession()); | |
| writeStoredSession(session); | |
| return session; | |
| } | |
| export const getDocuments = (userId: string): Promise<ApiDocument[]> => | |
| orchestrationRequest<ApiEnvelope<ApiDocument[] | null>>(`/api/v1/documents/${encodeURIComponent(userId)}`) | |
| .then((res) => res.data ?? []); | |
| export async function uploadDocument(userId: string, file: File): Promise<UploadDocumentResponse> { | |
| const form = new FormData(); | |
| form.append("user_id", userId); | |
| form.append("file", file); | |
| return orchestrationRequest<UploadDocumentResponse>("/api/v1/document/upload", { | |
| method: "POST", | |
| body: form, | |
| }); | |
| } | |
| export const processDocument = (userId: string, documentId: string) => | |
| orchestrationRequest<ApiEnvelope<{ document_id: string; file_type: string; status: string }>>("/api/v1/document/process", { | |
| method: "POST", | |
| body: JSON.stringify({ user_id: userId, document_id: documentId }), | |
| }); | |
| export const deleteDocument = (userId: string, documentId: string) => | |
| orchestrationRequest<ApiEnvelope<unknown>>("/api/v1/document/delete", { | |
| method: "DELETE", | |
| body: JSON.stringify({ document_id: documentId, user_id: userId }), | |
| }); | |
| export const getDocumentTypes = (): Promise<DocTypeInfo[]> => | |
| orchestrationRequest<ApiEnvelope<DocTypeInfo[]>>("/api/v1/documents/doctypes").then((res) => res.data ?? []); | |
| export const getDatabaseClientTypes = (): Promise<DbTypeInfo[]> => | |
| orchestrationRequest<ApiEnvelope<DbTypeInfo[] | null>>("/api/v1/database-clients/dbtypes").then((res) => res.data ?? []); | |
| export const connectDatabase = ( | |
| userId: string, | |
| dbType: DbType, | |
| name: string, | |
| credentials: Record<string, string | number | boolean> | |
| ): Promise<DatabaseClient> => | |
| orchestrationRequest<ApiEnvelope<DatabaseClient>>("/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<DatabaseClient[]> => | |
| orchestrationRequest<ApiEnvelope<DatabaseClient[] | null>>(`/api/v1/database-clients/${encodeURIComponent(userId)}`) | |
| .then((res) => res.data ?? []); | |
| export const deleteDatabaseClient = (clientId: string, userId: string) => | |
| orchestrationRequest<ApiEnvelope<unknown>>( | |
| `/api/v1/database-clients/${encodeURIComponent(clientId)}?user_id=${encodeURIComponent(userId)}`, | |
| { method: "DELETE" } | |
| ); | |
| export const ingestDatabaseClient = (clientId: string, userId: string): Promise<IngestResponse> => | |
| orchestrationRequest<ApiEnvelope<IngestResponse>>( | |
| `/api/v1/database-clients/${encodeURIComponent(clientId)}/ingest?user_id=${encodeURIComponent(userId)}`, | |
| { method: "POST" } | |
| ).then((res) => res.data); | |
| export const getDataCatalog = (userId: string): Promise<DataCatalog> => | |
| orchestrationRequest<ApiEnvelope<DataCatalog>>(`/api/v1/data-catalog/${encodeURIComponent(userId)}`).then((res) => res.data); | |
| export const rebuildDataCatalog = (userId: string): Promise<DataCatalog> => | |
| orchestrationRequest<ApiEnvelope<DataCatalog>>("/api/v1/data-catalog/rebuild", { | |
| method: "POST", | |
| body: JSON.stringify({ user_id: userId }), | |
| }).then((res) => res.data); | |
| function normalizeAnalysisList(data: AnalysisListResponse | Analysis[] | null): AnalysisListResponse { | |
| if (!data) return { analyses: [] }; | |
| if (Array.isArray(data)) return { analyses: data }; | |
| return { analyses: data.analyses ?? [], pagination: data.pagination }; | |
| } | |
| export const listAnalyses = (params: { status?: "active" | "inactive"; page?: number; limit?: number } = {}) => { | |
| const query = new URLSearchParams(); | |
| if (params.status) query.set("status", params.status); | |
| if (params.page) query.set("page", String(params.page)); | |
| if (params.limit) query.set("limit", String(params.limit)); | |
| const suffix = query.toString() ? `?${query.toString()}` : ""; | |
| return orchestrationRequest<ApiEnvelope<AnalysisListResponse | Analysis[] | null>>(`/api/v1/analyses${suffix}`).then((res) => | |
| normalizeAnalysisList(res.data) | |
| ); | |
| }; | |
| export const getAnalysis = (analysisId: string): Promise<Analysis> => | |
| orchestrationRequest<ApiEnvelope<Analysis>>(`/api/v1/analyses/${encodeURIComponent(analysisId)}`).then((res) => res.data); | |
| export const createAnalysis = (payload: CreateAnalysisPayload): Promise<Analysis> => | |
| orchestrationRequest<ApiEnvelope<Analysis>>("/api/v1/analyses", { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| }).then((res) => res.data); | |
| export const updateAnalysis = (analysisId: string, payload: UpdateAnalysisPayload): Promise<Analysis> => | |
| orchestrationRequest<ApiEnvelope<Analysis>>(`/api/v1/analyses/${encodeURIComponent(analysisId)}`, { | |
| method: "PATCH", | |
| body: JSON.stringify(payload), | |
| }).then((res) => res.data); | |
| export const deleteAnalysis = (analysisId: string): Promise<void> => | |
| orchestrationRequest<void>(`/api/v1/analyses/${encodeURIComponent(analysisId)}`, { method: "DELETE" }); | |
| export const updateAnalysisDataBind = ( | |
| analysisId: string, | |
| expectedVersion: number, | |
| dataBind: DataBindItem[] | |
| ): Promise<Analysis> => | |
| orchestrationRequest<ApiEnvelope<Analysis>>(`/api/v1/analyses/${encodeURIComponent(analysisId)}/data-bind`, { | |
| method: "PUT", | |
| body: JSON.stringify({ expected_version: expectedVersion, data_bind: dataBind }), | |
| }).then((res) => res.data); | |
| export const getAnalysisDataCatalog = (analysisId: string): Promise<DataCatalog> => | |
| orchestrationRequest<ApiEnvelope<DataCatalog>>(`/api/v1/analyses/${encodeURIComponent(analysisId)}/data-catalog`).then((res) => res.data); | |
| export const rebuildAnalysisDataCatalog = (analysisId: string): Promise<DataCatalog> => | |
| orchestrationRequest<ApiEnvelope<DataCatalog>>(`/api/v1/analyses/${encodeURIComponent(analysisId)}/data-catalog/rebuild`, { | |
| method: "POST", | |
| }).then((res) => res.data); | |
| export const getAnalysisMessages = (analysisId: string, limit = 100): Promise<AnalysisMessage[]> => | |
| orchestrationRequest<ApiEnvelope<{ messages?: AnalysisMessage[] } | AnalysisMessage[]>>( | |
| `/api/v1/analyses/${encodeURIComponent(analysisId)}/messages?limit=${encodeURIComponent(String(limit))}` | |
| ).then((res) => (Array.isArray(res.data) ? res.data : res.data.messages ?? [])); | |
| export const createAnalysisMessage = ( | |
| analysisId: string, | |
| payload: { role: "user" | "ai"; content: string; message_id?: string } | |
| ): Promise<AnalysisMessage> => | |
| orchestrationRequest<ApiEnvelope<{ message?: AnalysisMessage } | AnalysisMessage>>( | |
| `/api/v1/analyses/${encodeURIComponent(analysisId)}/messages`, | |
| { | |
| method: "POST", | |
| body: JSON.stringify(payload), | |
| } | |
| ).then((res) => ("message" in (res.data as object) ? (res.data as { message: AnalysisMessage }).message : (res.data as AnalysisMessage))); | |