import { userService } from "./userService"; export type SSEEvent = | { type: "tool_start"; tool: string } | { type: "tool_end"; tool: string } | { type: "response"; message: string } | { type: "error"; message: string }; export type SSEEventCallback = (event: SSEEvent) => void; interface Message { id: string; text: string; speaker: "user" | "simba"; created_at: string; image_key?: string | null; } interface Conversation { id: string; name: string; messages?: Message[]; created_at: string; updated_at: string; user_id?: string; } interface QueryRequest { query: string; } interface QueryResponse { response: string; } interface CreateConversationRequest { user_id: string; } class ConversationService { private baseUrl = "/api"; private conversationBaseUrl = "/api/conversation"; async sendQuery( query: string, conversation_id: string, signal?: AbortSignal, ): Promise { const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/query`, { method: "POST", body: JSON.stringify({ query, conversation_id }), signal, }, ); if (!response.ok) { throw new Error("Failed to send query"); } return await response.json(); } async getMessages(): Promise { const response = await userService.fetchWithRefreshToken( `${this.baseUrl}/messages`, { method: "GET", }, ); if (!response.ok) { throw new Error("Failed to fetch messages"); } return await response.json(); } async getConversation(conversationId: string): Promise { const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/${conversationId}`, { method: "GET", }, ); if (!response.ok) { throw new Error("Failed to fetch conversation"); } return await response.json(); } async createConversation(): Promise { const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/`, { method: "POST", }, ); if (!response.ok) { throw new Error("Failed to create conversation"); } return await response.json(); } async getAllConversations(): Promise { const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/`, { method: "GET", }, ); if (!response.ok) { throw new Error("Failed to fetch conversations"); } return await response.json(); } async uploadImage( file: File, conversationId: string, ): Promise<{ image_key: string }> { const formData = new FormData(); formData.append("file", file); formData.append("conversation_id", conversationId); const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/upload-image`, { method: "POST", body: formData, }, { skipContentType: true }, ); if (!response.ok) { const data = await response.json(); throw new Error(data.error || "Failed to upload image"); } return await response.json(); } async getPresignedImageUrl(imageKey: string): Promise { const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/image/${imageKey}`, ); if (!response.ok) { throw new Error("Failed to get image URL"); } const data = await response.json(); return data.url; } async streamQuery( query: string, conversation_id: string, onEvent: SSEEventCallback, signal?: AbortSignal, imageKey?: string, ): Promise { const body: Record = { query, conversation_id }; if (imageKey) { body.image_key = imageKey; } const response = await userService.fetchWithRefreshToken( `${this.conversationBaseUrl}/stream-query`, { method: "POST", body: JSON.stringify(body), signal, }, ); if (!response.ok) { throw new Error("Failed to stream query"); } await this._readSSEStream(response, onEvent); } private async _readSSEStream( response: Response, onEvent: SSEEventCallback, ): Promise { const reader = response.body!.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const parts = buffer.split("\n\n"); buffer = parts.pop() ?? ""; for (const part of parts) { const line = part.trim(); if (!line.startsWith("data: ")) continue; const data = line.slice(6); if (data === "[DONE]") return; try { const event = JSON.parse(data) as SSEEvent; onEvent(event); } catch { // ignore malformed events } } } } } export const conversationService = new ConversationService();