/** * Mike API client — all requests to the Node.js backend. * Attaches the Supabase auth token for user authentication. */ import { supabase } from "@/app/lib/supabase"; import type { AssistantEvent, Chat, ChatDetailOut, Citation, Document, Folder, LibraryFolder, Message, OpenSourceWorkflowContributorMode, OpenSourceWorkflowResponse, Project, Workflow, WorkflowContributor, TabularReview, TabularReviewDetailOut, } from "@/app/components/shared/types"; // Server-side shape before mapping interface ServerMessage { id: string; chat_id: string; role: "user" | "assistant"; content: string | AssistantEvent[] | null; files?: { filename: string; document_id?: string }[] | null; workflow?: { id: string; title: string } | null; citations?: Citation[] | null; created_at: string; } interface ServerChatDetailOut { chat: Chat; messages: ServerMessage[]; } const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://localhost:3001"; const isDev = process.env.NODE_ENV !== "production"; const devLog = (...args: Parameters) => { if (isDev) console.log(...args); }; export class MikeApiError extends Error { status: number; code: string | null; constructor(args: { message: string; status: number; code?: string | null }) { super(args.message); this.name = "MikeApiError"; this.status = args.status; this.code = args.code ?? null; } } export function isMfaRequiredError(error: unknown) { return ( error instanceof MikeApiError && error.status === 403 && error.code === "mfa_verification_required" ); } async function getAuthHeader(): Promise> { const { data: { session }, } = await supabase.auth.getSession(); if (!session?.access_token) return {}; return { Authorization: `Bearer ${session.access_token}` }; } async function apiRequest(path: string, init?: RequestInit): Promise { const authHeaders = await getAuthHeader(); const { headers: initHeaders, ...restInit } = init ?? {}; const response = await fetch(`${API_BASE}${path}`, { cache: "no-store", ...restInit, headers: { Accept: "application/json", ...authHeaders, ...(initHeaders as Record | undefined), }, }); if (!response.ok) { throw await toApiError(response, path); } if ( response.status === 204 || response.headers.get("content-length") === "0" ) { return undefined as T; } return (await response.json()) as T; } async function apiBlobRequest(path: string): Promise<{ blob: Blob; filename: string | null; }> { const authHeaders = await getAuthHeader(); const response = await fetch(`${API_BASE}${path}`, { cache: "no-store", headers: { Accept: "application/json", ...authHeaders, }, }); if (!response.ok) { throw await toApiError(response, path); } const disposition = response.headers.get("content-disposition") ?? ""; const filenameMatch = disposition.match(/filename="?([^";]+)"?/i); return { blob: await response.blob(), filename: filenameMatch?.[1] ?? null, }; } async function toApiError(response: Response, path: string) { const text = await response.text(); try { const parsed = JSON.parse(text) as { detail?: unknown; code?: unknown; }; devLog("[mike-api] non-ok response", { path, status: response.status, code: parsed.code, detail: parsed.detail, }); return new MikeApiError({ status: response.status, code: typeof parsed.code === "string" ? parsed.code : null, message: typeof parsed.detail === "string" && parsed.detail ? parsed.detail : `API error: ${response.status}`, }); } catch { devLog("[mike-api] non-ok non-json response", { path, status: response.status, bodyPreview: text.slice(0, 200), }); return new MikeApiError({ status: response.status, message: text || `API error: ${response.status}`, }); } } // --------------------------------------------------------------------------- // Projects // --------------------------------------------------------------------------- export async function listProjects(options?: { includeDocuments?: boolean; }): Promise { const query = options?.includeDocuments ? "?include=documents" : ""; return apiRequest(`/projects${query}`); } export async function createProject( name: string, cm_number?: string, practice?: string, shared_with?: string[], ): Promise { return apiRequest("/projects", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, cm_number, practice, shared_with }), }); } export async function deleteAccount(): Promise { return apiRequest("/user/account", { method: "DELETE" }); } export async function deleteAllChats(): Promise { return apiRequest("/user/chats", { method: "DELETE" }); } export async function deleteAllProjects(): Promise { return apiRequest("/user/projects", { method: "DELETE" }); } export async function deleteAllTabularReviews(): Promise { return apiRequest("/user/tabular-reviews", { method: "DELETE" }); } export async function exportAccountData(): Promise<{ blob: Blob; filename: string | null; }> { return apiBlobRequest("/user/export"); } export async function exportChatData(): Promise<{ blob: Blob; filename: string | null; }> { return apiBlobRequest("/user/chats/export"); } export async function exportTabularReviewsData(): Promise<{ blob: Blob; filename: string | null; }> { return apiBlobRequest("/user/tabular-reviews/export"); } export interface UserProfile { displayName: string | null; organisation: string | null; messageCreditsUsed: number; creditsResetDate: string; creditsRemaining: number; tier: string; titleModel: string; tabularModel: string; mfaOnLogin: boolean; legalResearchUs: boolean; apiKeyStatus: ApiKeyStatus; } export interface UserLookupResult { exists: boolean; email: string; display_name: string | null; } export async function getUserProfile(): Promise { return apiRequest("/user/profile"); } export async function lookupUserByEmail( email: string, ): Promise { return apiRequest( `/user/lookup?email=${encodeURIComponent(email)}`, ); } export async function updateUserProfile(payload: { displayName?: string | null; organisation?: string | null; titleModel?: string; tabularModel?: string; legalResearchUs?: boolean; }): Promise { return apiRequest("/user/profile", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function updateUserMfaOnLogin( enabled: boolean, ): Promise { return apiRequest("/user/security/mfa-login", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled }), }); } export type ApiKeyProvider = | "claude" | "gemini" | "openai" | "openrouter" | "courtlistener"; export type ApiKeySource = "user" | "env" | null; export type ApiKeyState = Record< ApiKeyProvider, { configured: boolean; source: ApiKeySource; } >; export type ApiKeyStatus = Record & { sources?: Partial>; }; export async function getApiKeyStatus(): Promise { return apiRequest("/user/api-keys"); } export async function saveApiKey( provider: ApiKeyProvider, apiKey: string | null, ): Promise { return apiRequest(`/user/api-keys/${provider}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ api_key: apiKey }), }); } export interface McpToolSummary { id: string; toolName: string; openaiToolName: string; title: string | null; description: string | null; enabled: boolean; readOnly: boolean; destructive: boolean; requiresConfirmation: boolean; lastSeenAt: string; } export interface McpConnectorSummary { id: string; name: string; transport: "streamable_http"; serverUrl: string; authType: "none" | "bearer" | "oauth"; enabled: boolean; hasAuthConfig: boolean; customHeaderKeys: string[]; oauthConnected: boolean; toolPolicy: Record; tools: McpToolSummary[]; toolCount: number; createdAt: string; updatedAt: string; } export async function listMcpConnectors(): Promise { return apiRequest("/user/mcp-connectors"); } export async function getMcpConnector( connectorId: string, ): Promise { return apiRequest( `/user/mcp-connectors/${connectorId}`, ); } export async function createMcpConnector(payload: { name: string; serverUrl: string; bearerToken?: string | null; headers?: Record; }): Promise { return apiRequest("/user/mcp-connectors", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function updateMcpConnector( connectorId: string, payload: { name?: string; serverUrl?: string; enabled?: boolean; bearerToken?: string | null; headers?: Record; }, ): Promise { return apiRequest( `/user/mcp-connectors/${connectorId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, ); } export async function deleteMcpConnector(connectorId: string): Promise { return apiRequest(`/user/mcp-connectors/${connectorId}`, { method: "DELETE", }); } export async function refreshMcpConnectorTools( connectorId: string, ): Promise { return apiRequest( `/user/mcp-connectors/${connectorId}/refresh-tools`, { method: "POST" }, ); } export async function startMcpConnectorOAuth( connectorId: string, ): Promise<{ authorizationUrl: string | null; alreadyAuthorized: boolean }> { return apiRequest<{ authorizationUrl: string | null; alreadyAuthorized: boolean }>( `/user/mcp-connectors/${connectorId}/oauth/start`, { method: "POST" }, ); } export async function setMcpToolEnabled( connectorId: string, toolId: string, enabled: boolean, ): Promise { return apiRequest( `/user/mcp-connectors/${connectorId}/tools/${toolId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled }), }, ); } export async function getProject(projectId: string): Promise { return apiRequest(`/projects/${projectId}`); } export async function updateProject( projectId: string, payload: { name?: string; cm_number?: string; practice?: string | null; shared_with?: string[]; }, ): Promise { return apiRequest(`/projects/${projectId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function deleteProject(projectId: string): Promise { await apiRequest(`/projects/${projectId}`, { method: "DELETE" }); } export interface ProjectPeople { owner: { user_id: string; email: string | null; display_name: string | null; }; members: { email: string; display_name: string | null }[]; } export async function getProjectPeople( projectId: string, ): Promise { return apiRequest(`/projects/${projectId}/people`); } // --------------------------------------------------------------------------- // Documents // --------------------------------------------------------------------------- // --------------------------------------------------------------------------- // Folders // --------------------------------------------------------------------------- export async function createProjectFolder( projectId: string, name: string, parentFolderId?: string | null, ): Promise { return apiRequest(`/projects/${projectId}/folders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, parent_folder_id: parentFolderId ?? null, }), }); } export async function renameProjectFolder( projectId: string, folderId: string, name: string, ): Promise { return apiRequest( `/projects/${projectId}/folders/${folderId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }, ); } export async function deleteProjectFolder( projectId: string, folderId: string, ): Promise { await apiRequest(`/projects/${projectId}/folders/${folderId}`, { method: "DELETE", }); } export async function moveSubfolderToFolder( projectId: string, folderId: string, parentFolderId: string | null, ): Promise { return apiRequest( `/projects/${projectId}/folders/${folderId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parent_folder_id: parentFolderId }), }, ); } export async function moveDocumentToFolder( projectId: string, documentId: string, folderId: string | null, ): Promise { return apiRequest( `/projects/${projectId}/documents/${documentId}/folder`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folder_id: folderId }), }, ); } export async function renameProjectDocument( projectId: string, documentId: string, filename: string, ): Promise { return apiRequest( `/projects/${projectId}/documents/${documentId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename }), }, ); } export type LibraryKind = "files" | "templates"; export interface LibraryCollection { documents: Document[]; folders: LibraryFolder[]; } export async function getLibrary( kind: LibraryKind, ): Promise { return apiRequest(`/library/${kind}`); } export async function uploadLibraryDocument( kind: LibraryKind, file: File, ): Promise { const authHeaders = await getAuthHeader(); const form = new FormData(); form.append("file", file); const response = await fetch(`${API_BASE}/library/${kind}/documents`, { method: "POST", headers: { ...authHeaders }, body: form, }); if (!response.ok) throw new Error(await response.text()); return response.json() as Promise; } export async function createLibraryFolder( kind: LibraryKind, name: string, parentFolderId?: string | null, ): Promise { return apiRequest(`/library/${kind}/folders`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, parent_folder_id: parentFolderId ?? null, }), }); } export async function renameLibraryFolder( kind: LibraryKind, folderId: string, name: string, ): Promise { return apiRequest(`/library/${kind}/folders/${folderId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }), }); } export async function deleteLibraryFolder( kind: LibraryKind, folderId: string, ): Promise { await apiRequest(`/library/${kind}/folders/${folderId}`, { method: "DELETE", }); } export async function moveLibraryFolder( kind: LibraryKind, folderId: string, parentFolderId: string | null, ): Promise { return apiRequest(`/library/${kind}/folders/${folderId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ parent_folder_id: parentFolderId }), }); } export async function moveLibraryDocument( kind: LibraryKind, documentId: string, folderId: string | null, ): Promise { return apiRequest( `/library/${kind}/documents/${documentId}/folder`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ folder_id: folderId }), }, ); } export async function renameLibraryDocument( kind: LibraryKind, documentId: string, filename: string, ): Promise { return apiRequest(`/library/${kind}/documents/${documentId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename }), }); } export async function addDocumentToProject( projectId: string, documentId: string, ): Promise { return apiRequest( `/projects/${projectId}/documents/${documentId}`, { method: "POST" }, ); } export interface DocumentVersion { id: string; version_number: number | null; source: string; created_at: string; filename: string | null; file_type?: string | null; size_bytes?: number | null; page_count?: number | null; deleted_at?: string | null; deleted_by?: string | null; } export async function listDocumentVersions(documentId: string): Promise<{ current_version_id: string | null; versions: DocumentVersion[]; }> { return apiRequest(`/single-documents/${documentId}/versions`); } export async function uploadDocumentVersion( documentId: string, file: File, filename?: string, ): Promise { const authHeaders = await getAuthHeader(); const form = new FormData(); form.append("file", file); if (filename) form.append("filename", filename); const response = await fetch( `${API_BASE}/single-documents/${documentId}/versions`, { method: "POST", headers: { ...authHeaders }, body: form, }, ); if (!response.ok) throw new Error(await response.text()); return response.json() as Promise; } export async function replaceDocumentVersionFile( documentId: string, versionId: string, file: File, filename?: string, ): Promise { const authHeaders = await getAuthHeader(); const form = new FormData(); form.append("file", file); if (filename) form.append("filename", filename); const response = await fetch( `${API_BASE}/single-documents/${documentId}/versions/${versionId}/file`, { method: "PUT", headers: { ...authHeaders }, body: form, }, ); if (!response.ok) throw new Error(await response.text()); return response.json() as Promise; } export async function copyDocumentVersionFromDocument( documentId: string, sourceDocumentId: string, filename?: string, ): Promise { return apiRequest( `/single-documents/${documentId}/versions/from-document`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ source_document_id: sourceDocumentId, filename, }), }, ); } export async function renameDocumentVersion( documentId: string, versionId: string, filename: string | null, ): Promise { return apiRequest( `/single-documents/${documentId}/versions/${versionId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename }), }, ); } export async function deleteDocumentVersion( documentId: string, versionId: string, ): Promise<{ deleted_version_id: string; current_version_id: string | null; }> { return apiRequest(`/single-documents/${documentId}/versions/${versionId}`, { method: "DELETE", }); } export async function uploadProjectDocument( projectId: string, file: File, ): Promise { const authHeaders = await getAuthHeader(); const form = new FormData(); form.append("file", file); const response = await fetch( `${API_BASE}/projects/${projectId}/documents`, { method: "POST", headers: { ...authHeaders }, body: form, }, ); if (!response.ok) throw new Error(await response.text()); return response.json() as Promise; } export async function uploadStandaloneDocument( file: File, ): Promise { const authHeaders = await getAuthHeader(); const form = new FormData(); form.append("file", file); const response = await fetch(`${API_BASE}/single-documents`, { method: "POST", headers: { ...authHeaders }, body: form, }); if (!response.ok) throw new Error(await response.text()); return response.json() as Promise; } export async function listStandaloneDocuments(): Promise { return apiRequest("/single-documents"); } export async function deleteDocument(documentId: string): Promise { await apiRequest(`/single-documents/${documentId}`, { method: "DELETE" }); } export async function getDocumentUrl( documentId: string, versionId?: string | null, ): Promise<{ url: string; filename: string; version_id: string | null }> { const qs = versionId ? `?version_id=${encodeURIComponent(versionId)}` : ""; return apiRequest(`/single-documents/${documentId}/url${qs}`); } export async function downloadDocumentsZip( documentIds: string[], ): Promise { const authHeaders = await getAuthHeader(); const response = await fetch(`${API_BASE}/single-documents/download-zip`, { method: "POST", cache: "no-store", headers: { "Content-Type": "application/json", ...authHeaders, }, body: JSON.stringify({ document_ids: documentIds }), }); if (!response.ok) { const detail = await response.text(); throw new Error(detail || `API error: ${response.status}`); } return response.blob(); } // --------------------------------------------------------------------------- // Chat // --------------------------------------------------------------------------- export async function createChat(payload?: { project_id?: string; }): Promise<{ id: string }> { return apiRequest<{ id: string }>("/chat/create", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload ?? {}), }); } export async function listChats(options?: { limit?: number }): Promise { const params = new URLSearchParams(); if (options?.limit) params.set("limit", String(options.limit)); const query = params.toString(); return apiRequest(`/chat${query ? `?${query}` : ""}`); } export async function listProjectChats(projectId: string): Promise { return apiRequest(`/projects/${projectId}/chats`); } export async function getChat(chatId: string): Promise { const raw = await apiRequest(`/chat/${chatId}`); const messages: Message[] = raw.messages.map((m) => { if (m.role === "user") { return { id: m.id, role: "user", content: typeof m.content === "string" ? m.content : "", files: m.files ?? undefined, workflow: m.workflow ?? undefined, }; } const events = Array.isArray(m.content) ? (m.content as AssistantEvent[]) : undefined; return { id: m.id, role: "assistant", content: events ?.filter((e) => e.type === "content") .map((e) => (e as { type: "content"; text: string }).text) .join("") ?? "", citations: m.citations ?? undefined, events, }; }); return { chat: raw.chat, messages }; } export async function renameChat(chatId: string, title: string): Promise { await apiRequest(`/chat/${chatId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); } export async function deleteChat(chatId: string): Promise { await apiRequest(`/chat/${chatId}`, { method: "DELETE" }); } export async function generateChatTitle( chatId: string, message: string, ): Promise<{ title: string }> { return apiRequest<{ title: string }>(`/chat/${chatId}/generate-title`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message }), }); } export type CaseLawOpinion = { opinionId: number | null; apiUrl?: string | null; type: string | null; author: string | null; url: string | null; text?: string | null; html?: string | null; }; export async function getCourtlistenerOpinions( clusterId: number, ): Promise { const result = await apiRequest<{ opinions: CaseLawOpinion[] }>( "/case-law/case-opinions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clusterId, }), }, ); return result.opinions; } export async function streamChat(payload: { messages: { role: string; content: string; files?: { filename: string; document_id?: string }[]; workflow?: { id: string; title: string }; }[]; chat_id?: string; project_id?: string; model?: string; ask_inputs_response?: { responses: ( | { id: string; kind: "choice"; question: string; answer?: string; skipped?: boolean; } | { id: string; kind: "documents"; filenames: string[]; skipped?: boolean; } )[]; }; signal?: AbortSignal; }): Promise { const { signal, ...body } = payload; const authHeaders = await getAuthHeader(); return fetch(`${API_BASE}/chat`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...authHeaders, }, body: JSON.stringify(body), signal, }); } type StreamChatMessage = { role: string; content: string; files?: { filename: string; document_id?: string }[]; workflow?: { id: string; title: string }; }; export async function streamProjectChat(payload: { projectId: string; messages: StreamChatMessage[]; chat_id?: string; model?: string; displayed_doc?: { filename: string; document_id: string }; attached_documents?: { filename: string; document_id: string }[]; ask_inputs_response?: { responses: ( | { id: string; kind: "choice"; question: string; answer?: string; skipped?: boolean; } | { id: string; kind: "documents"; filenames: string[]; skipped?: boolean; } )[]; }; signal?: AbortSignal; }): Promise { const { projectId, signal, ...body } = payload; const authHeaders = await getAuthHeader(); return fetch(`${API_BASE}/projects/${projectId}/chat`, { method: "POST", headers: { "Content-Type": "application/json", Accept: "text/event-stream", ...authHeaders, }, body: JSON.stringify(body), signal, }); } // --------------------------------------------------------------------------- // Tabular Review // --------------------------------------------------------------------------- export async function listTabularReviews( projectId?: string, ): Promise { const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : ""; return apiRequest(`/tabular-review${qs}`); } export async function createTabularReview(payload: { title?: string; document_ids: string[]; columns_config: { index: number; name: string; prompt: string }[]; workflow_id?: string; project_id?: string; }): Promise { return apiRequest("/tabular-review", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function getTabularReview( reviewId: string, ): Promise { return apiRequest(`/tabular-review/${reviewId}`); } export async function updateTabularReview( reviewId: string, payload: { title?: string; columns_config?: { index: number; name: string; prompt: string }[]; document_ids?: string[]; project_id?: string | null; shared_with?: string[]; }, ): Promise { return apiRequest(`/tabular-review/${reviewId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function getTabularReviewPeople( reviewId: string, ): Promise { return apiRequest(`/tabular-review/${reviewId}/people`); } export async function generateTabularColumnPrompt( title: string, options?: { format?: string; documentName?: string; tags?: string[] }, ): Promise<{ prompt: string; source: "preset" | "llm" | "fallback" }> { return apiRequest<{ prompt: string; source: "preset" | "llm" | "fallback"; }>("/tabular-review/prompt", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title, format: options?.format, documentName: options?.documentName, tags: options?.tags, }), }); } export async function uploadReviewDocument( reviewId: string, file: File, options?: { projectId?: string; documentIds?: string[]; columnsConfig?: { index: number; name: string; prompt: string }[]; }, ): Promise { const uploaded = options?.projectId ? await uploadProjectDocument(options.projectId, file) : await uploadStandaloneDocument(file); await updateTabularReview(reviewId, { columns_config: options?.columnsConfig, document_ids: [...(options?.documentIds ?? []), uploaded.id], }); return uploaded; } export async function deleteTabularReview(reviewId: string): Promise { await apiRequest(`/tabular-review/${reviewId}`, { method: "DELETE" }); } export async function streamTabularGeneration( reviewId: string, ): Promise { const authHeaders = await getAuthHeader(); return fetch(`${API_BASE}/tabular-review/${reviewId}/generate`, { method: "POST", headers: { ...authHeaders }, }); } export async function streamTabularChat( reviewId: string, messages: { role: string; content: string }[], chat_id?: string | null, signal?: AbortSignal, context?: { reviewTitle?: string | null; projectName?: string | null }, ): Promise { const authHeaders = await getAuthHeader(); return fetch(`${API_BASE}/tabular-review/${reviewId}/chat`, { method: "POST", headers: { "Content-Type": "application/json", ...authHeaders }, body: JSON.stringify({ messages, chat_id: chat_id ?? undefined, review_title: context?.reviewTitle ?? undefined, project_name: context?.projectName ?? undefined, }), signal: signal ?? undefined, }); } export interface TRCitationAnnotation { type: "tabular_citation"; ref: number; col_index: number; row_index: number; col_name: string; doc_name: string; quote: string; } interface RawTRMessage { id: string; chat_id: string; role: "user" | "assistant"; content: string | AssistantEvent[] | null; annotations?: TRCitationAnnotation[] | null; created_at: string; } export interface TRDisplayMessage { role: "user" | "assistant"; content: string; events?: AssistantEvent[]; annotations?: TRCitationAnnotation[]; } export interface TRChat { id: string; title: string | null; created_at: string; updated_at: string; } export function mapTRMessages(raw: RawTRMessage[]): TRDisplayMessage[] { return raw.map((m) => { if (m.role === "user") { return { role: "user" as const, content: typeof m.content === "string" ? m.content : "", }; } const events = Array.isArray(m.content) ? (m.content as AssistantEvent[]) : undefined; const content = events ?.filter((e) => e.type === "content") .map((e) => (e as { type: "content"; text: string }).text) .join("") ?? ""; return { role: "assistant" as const, content, events, annotations: m.annotations ?? undefined, }; }); } export async function getTabularChats(reviewId: string): Promise { return apiRequest(`/tabular-review/${reviewId}/chats`); } export async function getTabularChatMessages( reviewId: string, chatId: string, ): Promise { return apiRequest( `/tabular-review/${reviewId}/chats/${chatId}/messages`, ); } export async function deleteTabularChat( reviewId: string, chatId: string, ): Promise { await apiRequest(`/tabular-review/${reviewId}/chats/${chatId}`, { method: "DELETE", }); } export async function renameTabularChat( reviewId: string, chatId: string, title: string, ): Promise { await apiRequest(`/tabular-review/${reviewId}/chats/${chatId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ title }), }); } export async function regenerateTabularCell( reviewId: string, documentId: string, columnIndex: number, ): Promise<{ summary: string; flag: "green" | "grey" | "yellow" | "red"; reasoning: string; }> { return apiRequest(`/tabular-review/${reviewId}/regenerate-cell`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ document_id: documentId, column_index: columnIndex, }), }); } export async function clearTabularCells( reviewId: string, documentIds: string[], ): Promise { await apiRequest(`/tabular-review/${reviewId}/clear-cells`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ document_ids: documentIds }), }); } // --------------------------------------------------------------------------- // Workflows // --------------------------------------------------------------------------- type WorkflowType = Workflow["metadata"]["type"]; export async function listWorkflows( type: WorkflowType, ): Promise { return apiRequest(`/workflows?type=${type}`); } export async function getWorkflow(workflowId: string): Promise { return apiRequest(`/workflows/${workflowId}`); } export async function createWorkflow(payload: { metadata: { title: string; type: "assistant" | "tabular"; language?: string | null; practice?: string | null; jurisdictions?: string[] | null; }; skill_md?: string; columns_config?: { index: number; name: string; prompt: string }[]; }): Promise { return apiRequest("/workflows", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function updateWorkflow( workflowId: string, payload: { metadata?: { title?: string; language?: string | null; practice?: string | null; jurisdictions?: string[] | null; }; skill_md?: string; columns_config?: { index: number; name: string; prompt: string }[]; }, ): Promise { return apiRequest(`/workflows/${workflowId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function deleteWorkflow(workflowId: string): Promise { await apiRequest(`/workflows/${workflowId}`, { method: "DELETE" }); } export async function openSourceWorkflow( workflowId: string, payload: { contributor_mode: OpenSourceWorkflowContributorMode; contributor?: WorkflowContributor | null; }, ): Promise { return apiRequest( `/workflows/${workflowId}/open-source`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, ); } export async function listHiddenWorkflows(): Promise { return apiRequest("/workflows/hidden"); } export async function hideWorkflow(workflowId: string): Promise { await apiRequest("/workflows/hidden", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ workflow_id: workflowId }), }); } export async function unhideWorkflow(workflowId: string): Promise { await apiRequest(`/workflows/hidden/${workflowId}`, { method: "DELETE" }); } export async function shareWorkflow( workflowId: string, payload: { emails: string[]; allow_edit: boolean }, ): Promise { await apiRequest(`/workflows/${workflowId}/share`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); } export async function listWorkflowShares(workflowId: string): Promise< { id: string; shared_with_email: string; allow_edit: boolean; created_at: string; }[] > { return apiRequest(`/workflows/${workflowId}/shares`); } export async function deleteWorkflowShare( workflowId: string, shareId: string, ): Promise { await apiRequest(`/workflows/${workflowId}/shares/${shareId}`, { method: "DELETE", }); }