diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index 57b23b8..86425fd 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -1,6 +1,7 @@ import type { Message } from 'ai'; import { createScopedLogger } from '~/utils/logger'; import type { ChatHistoryItem } from './useChatHistory'; +import type { Snapshot } from './types'; // Import Snapshot type export interface IChatMetadata { gitUrl: string; @@ -18,15 +19,24 @@ export async function openDatabase(): Promise { } return new Promise((resolve) => { - const request = indexedDB.open('boltHistory', 1); + const request = indexedDB.open('boltHistory', 2); request.onupgradeneeded = (event: IDBVersionChangeEvent) => { const db = (event.target as IDBOpenDBRequest).result; + const oldVersion = event.oldVersion; - if (!db.objectStoreNames.contains('chats')) { - const store = db.createObjectStore('chats', { keyPath: 'id' }); - store.createIndex('id', 'id', { unique: true }); - store.createIndex('urlId', 'urlId', { unique: true }); + if (oldVersion < 1) { + if (!db.objectStoreNames.contains('chats')) { + const store = db.createObjectStore('chats', { keyPath: 'id' }); + store.createIndex('id', 'id', { unique: true }); + store.createIndex('urlId', 'urlId', { unique: true }); + } + } + + if (oldVersion < 2) { + if (!db.objectStoreNames.contains('snapshots')) { + db.createObjectStore('snapshots', { keyPath: 'chatId' }); + } } }; @@ -113,12 +123,46 @@ export async function getMessagesById(db: IDBDatabase, id: string): Promise { return new Promise((resolve, reject) => { - const transaction = db.transaction('chats', 'readwrite'); - const store = transaction.objectStore('chats'); - const request = store.delete(id); + const transaction = db.transaction(['chats', 'snapshots'], 'readwrite'); // Add snapshots store to transaction + const chatStore = transaction.objectStore('chats'); + const snapshotStore = transaction.objectStore('snapshots'); - request.onsuccess = () => resolve(undefined); - request.onerror = () => reject(request.error); + const deleteChatRequest = chatStore.delete(id); + const deleteSnapshotRequest = snapshotStore.delete(id); // Also delete snapshot + + let chatDeleted = false; + let snapshotDeleted = false; + + const checkCompletion = () => { + if (chatDeleted && snapshotDeleted) { + resolve(undefined); + } + }; + + deleteChatRequest.onsuccess = () => { + chatDeleted = true; + checkCompletion(); + }; + deleteChatRequest.onerror = () => reject(deleteChatRequest.error); + + deleteSnapshotRequest.onsuccess = () => { + snapshotDeleted = true; + checkCompletion(); + }; + + deleteSnapshotRequest.onerror = (event) => { + if ((event.target as IDBRequest).error?.name === 'NotFoundError') { + snapshotDeleted = true; + checkCompletion(); + } else { + reject(deleteSnapshotRequest.error); + } + }; + + transaction.oncomplete = () => { + // This might resolve before checkCompletion if one operation finishes much faster + }; + transaction.onerror = () => reject(transaction.error); }); } @@ -257,3 +301,43 @@ export async function updateChatMetadata( await setMessages(db, id, chat.messages, chat.urlId, chat.description, chat.timestamp, metadata); } + +export async function getSnapshot(db: IDBDatabase, chatId: string): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('snapshots', 'readonly'); + const store = transaction.objectStore('snapshots'); + const request = store.get(chatId); + + request.onsuccess = () => resolve(request.result?.snapshot as Snapshot | undefined); + request.onerror = () => reject(request.error); + }); +} + +export async function setSnapshot(db: IDBDatabase, chatId: string, snapshot: Snapshot): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('snapshots', 'readwrite'); + const store = transaction.objectStore('snapshots'); + const request = store.put({ chatId, snapshot }); + + request.onsuccess = () => resolve(); + request.onerror = () => reject(request.error); + }); +} + +export async function deleteSnapshot(db: IDBDatabase, chatId: string): Promise { + return new Promise((resolve, reject) => { + const transaction = db.transaction('snapshots', 'readwrite'); + const store = transaction.objectStore('snapshots'); + const request = store.delete(chatId); + + request.onsuccess = () => resolve(); + + request.onerror = (event) => { + if ((event.target as IDBRequest).error?.name === 'NotFoundError') { + resolve(); + } else { + reject(request.error); + } + }; + }); +} diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index 3077ca4..f359635 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -13,6 +13,8 @@ import { setMessages, duplicateChat, createChatFromMessages, + getSnapshot, + setSnapshot, type IChatMetadata, } from './db'; import type { FileMap } from '~/lib/stores/files'; @@ -61,19 +63,25 @@ export function useChatHistory() { } if (mixedId) { - getMessages(db, mixedId) - .then(async (storedMessages) => { + Promise.all([ + getMessages(db, mixedId), + getSnapshot(db, mixedId), // Fetch snapshot from DB + ]) + .then(async ([storedMessages, snapshot]) => { if (storedMessages && storedMessages.messages.length > 0) { - const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`); - const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; - const summary = snapshot.summary; + /* + * const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`); // Remove localStorage usage + * const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; // Use snapshot from DB + */ + const validSnapshot = snapshot || { chatIndex: '', files: {} }; // Ensure snapshot is not undefined + const summary = validSnapshot.summary; const rewindId = searchParams.get('rewindTo'); let startingIdx = -1; const endingIdx = rewindId ? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1 : storedMessages.messages.length; - const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === snapshot.chatIndex); + const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === validSnapshot.chatIndex); if (snapshotIndex >= 0 && snapshotIndex < endingIdx) { startingIdx = snapshotIndex; @@ -93,7 +101,7 @@ export function useChatHistory() { setArchivedMessages(archivedMessages); if (startingIdx > 0) { - const files = Object.entries(snapshot?.files || {}) + const files = Object.entries(validSnapshot?.files || {}) .map(([key, value]) => { if (value?.type !== 'file') { return null; @@ -197,17 +205,20 @@ ${value.content} .catch((error) => { console.error(error); - logStore.logError('Failed to load chat messages', error); - toast.error(error.message); + logStore.logError('Failed to load chat messages or snapshot', error); // Updated error message + toast.error('Failed to load chat: ' + error.message); // More specific error }); + } else { + // Handle case where there is no mixedId (e.g., new chat) + setReady(true); } - }, [mixedId]); + }, [mixedId, db, navigate, searchParams]); // Added db, navigate, searchParams dependencies const takeSnapshot = useCallback( async (chatIdx: string, files: FileMap, _chatId?: string | undefined, chatSummary?: string) => { - const id = _chatId || chatId; + const id = _chatId || chatId.get(); - if (!id) { + if (!id || !db) { return; } @@ -216,23 +227,29 @@ ${value.content} files, summary: chatSummary, }; - localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot)); + + // localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot)); // Remove localStorage usage + try { + await setSnapshot(db, id, snapshot); + } catch (error) { + console.error('Failed to save snapshot:', error); + toast.error('Failed to save chat snapshot.'); + } }, - [chatId], + [db], ); - const restoreSnapshot = useCallback(async (id: string) => { - const snapshotStr = localStorage.getItem(`snapshot:${id}`); + const restoreSnapshot = useCallback(async (id: string, snapshot?: Snapshot) => { + // const snapshotStr = localStorage.getItem(`snapshot:${id}`); // Remove localStorage usage const container = await webcontainer; - // if (snapshotStr)setSnapshot(JSON.parse(snapshotStr)); - const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; + const validSnapshot = snapshot || { chatIndex: '', files: {} }; - if (!snapshot?.files) { + if (!validSnapshot?.files) { return; } - Object.entries(snapshot.files).forEach(async ([key, value]) => { + Object.entries(validSnapshot.files).forEach(async ([key, value]) => { if (key.startsWith(container.workdir)) { key = key.replace(container.workdir, ''); } @@ -241,7 +258,7 @@ ${value.content} await container.fs.mkdir(key, { recursive: true }); } }); - Object.entries(snapshot.files).forEach(async ([key, value]) => { + Object.entries(validSnapshot.files).forEach(async ([key, value]) => { if (value?.type === 'file') { if (key.startsWith(container.workdir)) { key = key.replace(container.workdir, ''); @@ -311,6 +328,7 @@ ${value.content} description.set(firstArtifact?.title); } + // Ensure chatId.get() is used here as well if (initialMessages.length === 0 && !chatId.get()) { const nextId = await getNextId(db); @@ -321,9 +339,19 @@ ${value.content} } } + // Ensure chatId.get() is used for the final setMessages call + const finalChatId = chatId.get(); + + if (!finalChatId) { + console.error('Cannot save messages, chat ID is not set.'); + toast.error('Failed to save chat messages: Chat ID missing.'); + + return; + } + await setMessages( db, - chatId.get() as string, + finalChatId, // Use the potentially updated chatId [...archivedMessages, ...messages], urlId, description.get(),