import type {Conversation, ConversationSummary, StoredChatMessage} from "./conversation-types"; import type {GenerationSettings} from "./generation-settings"; import { cacheConversation, cacheConversationSummaries, listPendingConversationChanges, loadCachedConversation, loadCachedConversationSummaries, queueConversationChange, removeCachedConversation, removePendingConversationChange } from "./offline-history"; class ConversationHttpError extends Error { constructor(message: string, readonly status: number) { super(message); } } async function conversationRequest(path: string, init?: RequestInit) { const response = await fetch(path, { cache: "no-store", ...init, headers: { "Accept": "application/json", ...(init?.body ? {"Content-Type": "application/json"} : {}), ...(init?.headers || {}) } }); if (response.status === 204) return undefined as T; const payload = await response.json(); if (!response.ok) throw new ConversationHttpError(payload.error || `HTTP ${response.status}`, response.status); return payload as T; } function isNetworkFailure(error: unknown) { return error instanceof TypeError || (typeof navigator !== "undefined" && !navigator.onLine); } export async function listConversationHistory() { try { const payload = await conversationRequest<{conversations: ConversationSummary[]}>("/api/conversations"); await cacheConversationSummaries(payload.conversations); return payload.conversations; } catch (error) { if (!isNetworkFailure(error)) throw error; const cached = await loadCachedConversationSummaries(); if (cached.length === 0) throw error; return cached; } } export async function createConversationHistory(providerId: string, model: string, generationSettings: GenerationSettings) { const payload = await conversationRequest<{conversation: Conversation}>("/api/conversations", { method: "POST", body: JSON.stringify({providerId, model, generationSettings}) }); await cacheConversation(payload.conversation); return payload.conversation; } export async function getConversationHistory(id: string) { try { const payload = await conversationRequest<{conversation: Conversation}>(`/api/conversations/${encodeURIComponent(id)}`); await cacheConversation(payload.conversation); return payload.conversation; } catch (error) { if (!isNetworkFailure(error)) throw error; const cached = await loadCachedConversation(id); if (!cached) throw error; return cached; } } export async function updateConversationHistory(id: string, providerId: string, model: string, generationSettings: GenerationSettings) { const body = JSON.stringify({providerId, model, generationSettings}); try { return await conversationRequest<{updated: true}>(`/api/conversations/${encodeURIComponent(id)}`, {method: "PATCH", body}); } catch (error) { if (!isNetworkFailure(error)) throw error; const cached = await loadCachedConversation(id); if (cached) await cacheConversation({...cached, providerId, model, generationSettings, updatedAt: new Date().toISOString()}); await queueConversationChange({conversationId: id, method: "PATCH", body}); return {updated: true as const}; } } export async function saveConversationHistory(id: string, providerId: string, model: string, messages: StoredChatMessage[]) { const body = JSON.stringify({providerId, model, messages}); try { const payload = await conversationRequest<{conversation: Conversation}>(`/api/conversations/${encodeURIComponent(id)}`, {method: "PUT", body}); await cacheConversation(payload.conversation); return payload.conversation; } catch (error) { if (!isNetworkFailure(error)) throw error; const cached = await loadCachedConversation(id); if (!cached) throw error; const firstUserText = messages.find((message) => message.role === "user")?.parts .filter((part) => part.type === "text" && typeof part.text === "string") .map((part) => String(part.text)).join(" ").replace(/\s+/g, " ").trim(); const updated: Conversation = { ...cached, title: firstUserText?.slice(0, 48) || cached.title, providerId, model, messages, messageCount: messages.length, updatedAt: new Date().toISOString() }; await cacheConversation(updated); await queueConversationChange({conversationId: id, method: "PUT", body}); return updated; } } export async function deleteConversationHistory(id: string) { try { await conversationRequest(`/api/conversations/${encodeURIComponent(id)}`, {method: "DELETE"}); } catch (error) { if (!isNetworkFailure(error)) throw error; await queueConversationChange({conversationId: id, method: "DELETE"}); } await removeCachedConversation(id); } export async function flushPendingConversationChanges() { const pending = await listPendingConversationChanges(); for (const change of pending) { try { await conversationRequest(`/api/conversations/${encodeURIComponent(change.conversationId)}`, { method: change.method, body: change.body }); await removePendingConversationChange(change.cacheKey); } catch (error) { if (error instanceof ConversationHttpError && change.method === "DELETE" && error.status === 404) { await removePendingConversationChange(change.cacheKey); continue; } if (isNetworkFailure(error)) break; throw error; } } } export async function synchronizeOfflineConversationHistory(summaries?: ConversationSummary[]) { const history = summaries || (await conversationRequest<{conversations: ConversationSummary[]}>("/api/conversations")).conversations; await cacheConversationSummaries(history); let cursor = 0; const worker = async () => { while (cursor < history.length) { const summary = history[cursor++]; const payload = await conversationRequest<{conversation: Conversation}>(`/api/conversations/${encodeURIComponent(summary.id)}`); await cacheConversation(payload.conversation); } }; await Promise.all(Array.from({length: Math.min(4, history.length)}, () => worker())); return history; }