|
import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react'; |
|
import { useState, useEffect } from 'react'; |
|
import { atom } from 'nanostores'; |
|
import type { Message } from 'ai'; |
|
import { toast } from 'react-toastify'; |
|
import { workbenchStore } from '~/lib/stores/workbench'; |
|
import { logStore } from '~/lib/stores/logs'; |
|
import { |
|
getMessages, |
|
getNextId, |
|
getUrlId, |
|
openDatabase, |
|
setMessages, |
|
duplicateChat, |
|
createChatFromMessages, |
|
type IChatMetadata, |
|
} from './db'; |
|
|
|
export interface ChatHistoryItem { |
|
id: string; |
|
urlId?: string; |
|
description?: string; |
|
messages: Message[]; |
|
timestamp: string; |
|
metadata?: IChatMetadata; |
|
} |
|
|
|
const persistenceEnabled = !import.meta.env.VITE_DISABLE_PERSISTENCE; |
|
|
|
export const db = persistenceEnabled ? await openDatabase() : undefined; |
|
|
|
export const chatId = atom<string | undefined>(undefined); |
|
export const description = atom<string | undefined>(undefined); |
|
export const chatMetadata = atom<IChatMetadata | undefined>(undefined); |
|
export function useChatHistory() { |
|
const navigate = useNavigate(); |
|
const { id: mixedId } = useLoaderData<{ id?: string }>(); |
|
const [searchParams] = useSearchParams(); |
|
|
|
const [initialMessages, setInitialMessages] = useState<Message[]>([]); |
|
const [ready, setReady] = useState<boolean>(false); |
|
const [urlId, setUrlId] = useState<string | undefined>(); |
|
|
|
useEffect(() => { |
|
if (!db) { |
|
setReady(true); |
|
|
|
if (persistenceEnabled) { |
|
const error = new Error('Chat persistence is unavailable'); |
|
logStore.logError('Chat persistence initialization failed', error); |
|
toast.error('Chat persistence is unavailable'); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if (mixedId) { |
|
getMessages(db, mixedId) |
|
.then((storedMessages) => { |
|
if (storedMessages && storedMessages.messages.length > 0) { |
|
const rewindId = searchParams.get('rewindTo'); |
|
const filteredMessages = rewindId |
|
? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1) |
|
: storedMessages.messages; |
|
|
|
setInitialMessages(filteredMessages); |
|
setUrlId(storedMessages.urlId); |
|
description.set(storedMessages.description); |
|
chatId.set(storedMessages.id); |
|
chatMetadata.set(storedMessages.metadata); |
|
} else { |
|
navigate('/', { replace: true }); |
|
} |
|
|
|
setReady(true); |
|
}) |
|
.catch((error) => { |
|
logStore.logError('Failed to load chat messages', error); |
|
toast.error(error.message); |
|
}); |
|
} |
|
}, []); |
|
|
|
return { |
|
ready: !mixedId || ready, |
|
initialMessages, |
|
updateChatMestaData: async (metadata: IChatMetadata) => { |
|
const id = chatId.get(); |
|
|
|
if (!db || !id) { |
|
return; |
|
} |
|
|
|
try { |
|
await setMessages(db, id, initialMessages, urlId, description.get(), undefined, metadata); |
|
chatMetadata.set(metadata); |
|
} catch (error) { |
|
toast.error('Failed to update chat metadata'); |
|
console.error(error); |
|
} |
|
}, |
|
storeMessageHistory: async (messages: Message[]) => { |
|
if (!db || messages.length === 0) { |
|
return; |
|
} |
|
|
|
const { firstArtifact } = workbenchStore; |
|
|
|
if (!urlId && firstArtifact?.id) { |
|
const urlId = await getUrlId(db, firstArtifact.id); |
|
|
|
navigateChat(urlId); |
|
setUrlId(urlId); |
|
} |
|
|
|
if (!description.get() && firstArtifact?.title) { |
|
description.set(firstArtifact?.title); |
|
} |
|
|
|
if (initialMessages.length === 0 && !chatId.get()) { |
|
const nextId = await getNextId(db); |
|
|
|
chatId.set(nextId); |
|
|
|
if (!urlId) { |
|
navigateChat(nextId); |
|
} |
|
} |
|
|
|
await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get()); |
|
}, |
|
duplicateCurrentChat: async (listItemId: string) => { |
|
if (!db || (!mixedId && !listItemId)) { |
|
return; |
|
} |
|
|
|
try { |
|
const newId = await duplicateChat(db, mixedId || listItemId); |
|
navigate(`/chat/${newId}`); |
|
toast.success('Chat duplicated successfully'); |
|
} catch (error) { |
|
toast.error('Failed to duplicate chat'); |
|
console.log(error); |
|
} |
|
}, |
|
importChat: async (description: string, messages: Message[], metadata?: IChatMetadata) => { |
|
if (!db) { |
|
return; |
|
} |
|
|
|
try { |
|
const newId = await createChatFromMessages(db, description, messages, metadata); |
|
window.location.href = `/chat/${newId}`; |
|
toast.success('Chat imported successfully'); |
|
} catch (error) { |
|
if (error instanceof Error) { |
|
toast.error('Failed to import chat: ' + error.message); |
|
} else { |
|
toast.error('Failed to import chat'); |
|
} |
|
} |
|
}, |
|
exportChat: async (id = urlId) => { |
|
if (!db || !id) { |
|
return; |
|
} |
|
|
|
const chat = await getMessages(db, id); |
|
const chatData = { |
|
messages: chat.messages, |
|
description: chat.description, |
|
exportDate: new Date().toISOString(), |
|
}; |
|
|
|
const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' }); |
|
const url = URL.createObjectURL(blob); |
|
const a = document.createElement('a'); |
|
a.href = url; |
|
a.download = `chat-${new Date().toISOString()}.json`; |
|
document.body.appendChild(a); |
|
a.click(); |
|
document.body.removeChild(a); |
|
URL.revokeObjectURL(url); |
|
}, |
|
}; |
|
} |
|
|
|
function navigateChat(nextId: string) { |
|
|
|
|
|
|
|
|
|
|
|
const url = new URL(window.location.href); |
|
url.pathname = `/chat/${nextId}`; |
|
|
|
window.history.replaceState({}, '', url); |
|
} |
|
|