import { api } from "./client"; import type { ChatConversation, ChatConversationListResponse, ChatMessage, ChatMessageListResponse, ChatMessageSearchResponse, ChatBookmarkToggleResponse, ChatBookmarkListResponse, } from "@paperclipai/shared"; export const chatApi = { listConversations(companyId: string, opts?: { cursor?: string; limit?: number; search?: string; agentId?: string }) { const params = new URLSearchParams(); if (opts?.cursor) params.set("cursor", opts.cursor); if (opts?.limit) params.set("limit", String(opts.limit)); if (opts?.search) params.set("search", opts.search); if (opts?.agentId) params.set("agentId", opts.agentId); const qs = params.toString(); return api.get( `/companies/${companyId}/conversations${qs ? `?${qs}` : ""}`, ); }, createConversation(companyId: string, data?: { title?: string; agentId?: string }) { return api.post(`/companies/${companyId}/conversations`, data ?? {}); }, getConversation(id: string) { return api.get(`/conversations/${id}`); }, updateConversation( id: string, data: { title?: string; pinnedAt?: string | null; archivedAt?: string | null; agentId?: string | null }, ) { return api.patch(`/conversations/${id}`, data); }, deleteConversation(id: string) { return api.delete(`/conversations/${id}`); }, listMessages(conversationId: string, opts?: { cursor?: string; limit?: number }) { const params = new URLSearchParams(); if (opts?.cursor) params.set("cursor", opts.cursor); if (opts?.limit) params.set("limit", String(opts.limit)); const qs = params.toString(); return api.get( `/conversations/${conversationId}/messages${qs ? `?${qs}` : ""}`, ); }, postMessage( conversationId: string, data: { role: string; content: string; agentId?: string }, ) { return api.post(`/conversations/${conversationId}/messages`, data); }, async postMessageAndStream( conversationId: string, data: { content: string; agentId?: string }, callbacks: { onToken: (token: string) => void; onDone: (messageId: string, content: string) => void; onError: (error: string) => void; }, signal?: AbortSignal, ): Promise { const response = await fetch(`/api/conversations/${conversationId}/stream`, { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify(data), signal, }); if (!response.ok) { callbacks.onError(`HTTP ${response.status}`); return; } const reader = response.body?.getReader(); if (!reader) { callbacks.onError("No response body"); return; } const decoder = new TextDecoder(); let buffer = ""; try { while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() ?? ""; for (const line of lines) { if (line.startsWith("data: ")) { const raw = line.slice(6).trim(); if (raw === "[DONE]") continue; try { const parsed = JSON.parse(raw) as { type: string; token?: string; messageId?: string; content?: string; error?: string }; if (parsed.type === "token" && parsed.token !== undefined) { callbacks.onToken(parsed.token); } else if (parsed.type === "done" && parsed.messageId !== undefined) { callbacks.onDone(parsed.messageId, parsed.content ?? ""); } else if (parsed.type === "error") { callbacks.onError(parsed.error ?? "Unknown error"); } } catch { // ignore malformed SSE lines } } } } } catch (err) { if ((err as Error).name !== "AbortError") { callbacks.onError((err as Error).message); } } finally { reader.releaseLock(); } }, savePartialMessage( conversationId: string, data: { role: string; content: string }, ) { return api.post(`/conversations/${conversationId}/messages`, data); }, async editMessage(conversationId: string, messageId: string, content: string) { return api.patch(`/conversations/${conversationId}/messages/${messageId}`, { content }); }, async truncateMessagesAfter(conversationId: string, messageId: string) { await fetch(`/api/conversations/${conversationId}/messages/after/${messageId}`, { method: "DELETE", credentials: "include", }); }, async deleteMessage(conversationId: string, messageId: string) { await fetch(`/api/conversations/${conversationId}/messages/${messageId}`, { method: "DELETE", credentials: "include", }); }, handoffSpec( conversationId: string, spec: { what: string; why: string; constraints: string; success: string }, targetRole: string = "pm", ) { return api.post<{ handoffMessageId: string; issues: Array<{ id: string; identifier: string; title: string }> }>( `/conversations/${conversationId}/handoff`, { spec, targetRole }, ); }, postStatusUpdate( conversationId: string, data: { agentName: string; taskId: string; taskTitle?: string; taskUrl?: string }, ) { return api.post<{ id: string }>(`/conversations/${conversationId}/status-update`, data); }, searchMessages(companyId: string, q: string, limit?: number) { const params = new URLSearchParams({ q }); if (limit) params.set("limit", String(limit)); return api.get( `/companies/${companyId}/messages/search?${params}`, ); }, toggleBookmark(conversationId: string, messageId: string) { return api.post( `/conversations/${conversationId}/bookmarks`, { messageId }, ); }, getBookmarks(companyId: string, conversationId?: string) { const params = new URLSearchParams(); if (conversationId) params.set("conversationId", conversationId); const qs = params.toString(); return api.get( `/companies/${companyId}/bookmarks${qs ? `?${qs}` : ""}`, ); }, branchConversation(conversationId: string, branchFromMessageId: string) { return api.post( `/conversations/${conversationId}/branch`, { branchFromMessageId }, ); }, listBranches(conversationId: string) { return api.get<{ items: ChatConversation[] }>( `/conversations/${conversationId}/branches`, ); }, exportConversation(conversationId: string, format: "markdown" | "json") { // Returns a download URL — use window.location.href to trigger return `/api/conversations/${conversationId}/export?format=${format}`; }, };