import type {Conversation, ConversationSummary} from "./conversation-types"; const databaseName = "xiteng-chat-offline"; const databaseVersion = 1; const activeProfileKey = "xiteng-chat-offline-profile"; type CachedProfile = { id: string; config: T; summaries: ConversationSummary[]; updatedAt: string; }; type CachedConversation = Conversation & {cacheKey: string; profileId: string}; export type PendingConversationChange = { cacheKey: string; profileId: string; conversationId: string; method: "PUT" | "PATCH" | "DELETE"; body?: string; createdAt: string; }; function openDatabase() { return new Promise((resolve, reject) => { const request = indexedDB.open(databaseName, databaseVersion); request.onupgradeneeded = () => { const database = request.result; if (!database.objectStoreNames.contains("profiles")) database.createObjectStore("profiles", {keyPath: "id"}); if (!database.objectStoreNames.contains("conversations")) { const conversations = database.createObjectStore("conversations", {keyPath: "cacheKey"}); conversations.createIndex("profileId", "profileId"); } if (!database.objectStoreNames.contains("pending")) { const pending = database.createObjectStore("pending", {keyPath: "cacheKey"}); pending.createIndex("profileId", "profileId"); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error("Unable to open offline history")); }); } async function transaction(storeName: string, mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest) { const database = await openDatabase(); return new Promise((resolve, reject) => { const current = database.transaction(storeName, mode); const request = run(current.objectStore(storeName)); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error || new Error(`Offline ${storeName} operation failed`)); current.oncomplete = () => database.close(); current.onerror = () => reject(current.error || new Error(`Offline ${storeName} transaction failed`)); }); } function activeProfileId() { return window.localStorage.getItem(activeProfileKey) || ""; } function conversationCacheKey(profileId: string, conversationId: string) { return `${profileId}:${conversationId}`; } export function activateOfflineProfile(profileId: string) { window.localStorage.setItem(activeProfileKey, profileId); } export async function cacheChatConfig(profileId: string, config: T) { activateOfflineProfile(profileId); const current = await transaction | undefined>("profiles", "readonly", (store) => store.get(profileId)); const profile: CachedProfile = { id: profileId, config, summaries: current?.summaries || [], updatedAt: new Date().toISOString() }; await transaction("profiles", "readwrite", (store) => store.put(profile)); } export async function loadCachedChatConfig() { const profileId = activeProfileId(); if (!profileId) return null; const profile = await transaction | undefined>("profiles", "readonly", (store) => store.get(profileId)); return profile ? {profileId, config: profile.config, updatedAt: profile.updatedAt} : null; } export async function cacheConversationSummaries(summaries: ConversationSummary[]) { const profileId = activeProfileId(); if (!profileId) return; const current = await transaction("profiles", "readonly", (store) => store.get(profileId)); if (!current) return; await transaction("profiles", "readwrite", (store) => store.put({ ...current, summaries, updatedAt: new Date().toISOString() })); } export async function loadCachedConversationSummaries() { const profileId = activeProfileId(); if (!profileId) return []; const profile = await transaction("profiles", "readonly", (store) => store.get(profileId)); return profile?.summaries || []; } export async function cacheConversation(conversation: Conversation) { const profileId = activeProfileId(); if (!profileId) return; const record: CachedConversation = { ...conversation, cacheKey: conversationCacheKey(profileId, conversation.id), profileId }; await transaction("conversations", "readwrite", (store) => store.put(record)); const summaries = await loadCachedConversationSummaries(); const summary: ConversationSummary = { id: conversation.id, title: conversation.title, providerId: conversation.providerId, model: conversation.model, messageCount: conversation.messageCount, createdAt: conversation.createdAt, updatedAt: conversation.updatedAt }; await cacheConversationSummaries([summary, ...summaries.filter((item) => item.id !== conversation.id)].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))); } export async function loadCachedConversation(id: string) { const profileId = activeProfileId(); if (!profileId) return null; const record = await transaction("conversations", "readonly", (store) => store.get(conversationCacheKey(profileId, id))); if (!record) return null; const {cacheKey: _cacheKey, profileId: _profileId, ...conversation} = record; return conversation; } export async function removeCachedConversation(id: string) { const profileId = activeProfileId(); if (!profileId) return; await transaction("conversations", "readwrite", (store) => store.delete(conversationCacheKey(profileId, id))); const summaries = await loadCachedConversationSummaries(); await cacheConversationSummaries(summaries.filter((conversation) => conversation.id !== id)); } export async function queueConversationChange(change: Omit) { const profileId = activeProfileId(); if (!profileId) return; const record: PendingConversationChange = { ...change, cacheKey: `${profileId}:${change.conversationId}:${change.method}`, profileId, createdAt: new Date().toISOString() }; await transaction("pending", "readwrite", (store) => store.put(record)); } export async function listPendingConversationChanges() { const profileId = activeProfileId(); if (!profileId) return []; const database = await openDatabase(); return new Promise((resolve, reject) => { const current = database.transaction("pending", "readonly"); const request = current.objectStore("pending").index("profileId").getAll(profileId); request.onsuccess = () => resolve(request.result.sort((left, right) => left.createdAt.localeCompare(right.createdAt))); request.onerror = () => reject(request.error || new Error("Unable to read pending history changes")); current.oncomplete = () => database.close(); }); } export async function removePendingConversationChange(cacheKey: string) { await transaction("pending", "readwrite", (store) => store.delete(cacheKey)); }