diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 353e326..d5c2e0f 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -12,7 +12,6 @@ import { Messages } from './Messages.client'; import { SendButton } from './SendButton.client'; import { APIKeyManager } from './APIKeyManager'; import Cookies from 'js-cookie'; -import { exportChat, importChat } from '~/utils/chatExport'; import { toast } from 'react-toastify'; import * as Tooltip from '@radix-ui/react-tooltip'; @@ -90,6 +89,8 @@ interface BaseChatProps { sendMessage?: (event: React.UIEvent, messageInput?: string) => void; handleInputChange?: (event: React.ChangeEvent) => void; enhancePrompt?: () => void; + importChat?: (description: string, messages: Message[]) => Promise; + exportChat?: () => void; } export const BaseChat = React.forwardRef( @@ -113,7 +114,9 @@ export const BaseChat = React.forwardRef( sendMessage, handleInputChange, enhancePrompt, - handleStop + handleStop, + importChat, + exportChat }, ref ) => { @@ -292,7 +295,7 @@ export const BaseChat = React.forwardRef( )} - {() => } + {() => } {input.length > 3 ? (
@@ -317,18 +320,31 @@ export const BaseChat = React.forwardRef( accept=".json" onChange={async (e) => { const file = e.target.files?.[0]; - if (file) { + if (file && importChat) { try { - const { messages: importedMessages } = await importChat(file); - // Import each message - for (const msg of importedMessages) { - await sendMessage(new Event('import') as unknown as React.UIEvent, msg.content); - } - toast.success('Chat imported successfully'); + const reader = new FileReader(); + reader.onload = async (e) => { + try { + const content = e.target?.result as string; + const data = JSON.parse(content); + if (!Array.isArray(data.messages)) { + toast.error('Invalid chat file format'); + } + await importChat(data.description, data.messages); + toast.success('Chat imported successfully'); + } catch (error) { + toast.error('Failed to parse chat file'); + } + }; + reader.onerror = () => toast.error('Failed to read chat file'); + reader.readAsText(file); + } catch (error) { toast.error(error instanceof Error ? error.message : 'Failed to import chat'); } e.target.value = ''; // Reset file input + } else { + toast.error('Something went wrong'); } }} /> diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index 8b56419..de5cb37 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -28,12 +28,12 @@ const logger = createScopedLogger('Chat'); export function Chat() { renderLogger.trace('Chat'); - const { ready, initialMessages, storeMessageHistory } = useChatHistory(); + const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory(); const title = useStore(description); return ( <> - {ready && } + {ready && } { return ( @@ -68,9 +68,11 @@ export function Chat() { interface ChatProps { initialMessages: Message[]; storeMessageHistory: (messages: Message[]) => Promise; + importChat: (description: string, messages: Message[]) => Promise; + exportChat: () => void; } -export const ChatImpl = memo(({ description, initialMessages, storeMessageHistory }: ChatProps) => { +export const ChatImpl = memo(({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => { useShortcuts(); const textareaRef = useRef(null); @@ -254,6 +256,8 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor handleInputChange={handleInputChange} handleStop={abort} description={description} + importChat={importChat} + exportChat={exportChat} messages={messages.map((message, i) => { if (message.role === 'user') { return message; diff --git a/app/components/chat/ExportChatButton.tsx b/app/components/chat/ExportChatButton.tsx index 5b0906d..4816fe4 100644 --- a/app/components/chat/ExportChatButton.tsx +++ b/app/components/chat/ExportChatButton.tsx @@ -1,14 +1,12 @@ import WithTooltip from '~/components/ui/Tooltip'; import { IconButton } from '~/components/ui/IconButton'; -import { exportChat } from '~/utils/chatExport'; import React from 'react'; -import type { Message } from 'ai'; -export const ExportChatButton = ({description, messages}: {description: string, messages: Message[]}) => { +export const ExportChatButton = ({exportChat}: {exportChat: () => void}) => { return ( exportChat(messages || [], description)} + onClick={exportChat} >
diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx index 713dd02..2f7476f 100644 --- a/app/components/chat/Messages.client.tsx +++ b/app/components/chat/Messages.client.tsx @@ -33,7 +33,6 @@ export const Messages = React.forwardRef((props: toast.error('Chat persistence is not available'); return; } - const urlId = await forkChat(db, chatId.get()!, messageId); window.location.href = `/chat/${urlId}`; } catch (error) { diff --git a/app/components/sidebar/HistoryItem.tsx b/app/components/sidebar/HistoryItem.tsx index 44abeed..9b8e2dc 100644 --- a/app/components/sidebar/HistoryItem.tsx +++ b/app/components/sidebar/HistoryItem.tsx @@ -1,16 +1,16 @@ import * as Dialog from '@radix-ui/react-dialog'; import { useEffect, useRef, useState } from 'react'; import { type ChatHistoryItem } from '~/lib/persistence'; -import { exportChat } from '~/utils/chatExport'; import WithTooltip from '~/components/ui/Tooltip'; interface HistoryItemProps { item: ChatHistoryItem; onDelete?: (event: React.UIEvent) => void; onDuplicate?: (id: string) => void; + exportChat: (id?: string) => void; } -export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) { +export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) { const [hovering, setHovering] = useState(false); const hoverRef = useRef(null); @@ -53,7 +53,8 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) { className="i-ph:download-simple scale-110 mr-2" onClick={(event) => { event.preventDefault(); - exportChat(item.messages, item.description); + exportChat(item.id); + //exportChat(item.messages, item.description); }} title="Export chat" /> diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index b3d9631..e7f5d2e 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -34,7 +34,7 @@ const menuVariants = { type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null; export function Menu() { - const { duplicateCurrentChat } = useChatHistory(); + const { duplicateCurrentChat, exportChat } = useChatHistory(); const menuRef = useRef(null); const [list, setList] = useState([]); const [open, setOpen] = useState(false); @@ -102,7 +102,6 @@ export function Menu() { const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => { event.preventDefault(); - setDialogContent({ type: 'delete', item }); }; @@ -144,6 +143,7 @@ export function Menu() { handleDeleteClick(event, item)} onDuplicate={() => handleDuplicate(item.id)} /> diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index 3aa2004..676bbf4 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -170,37 +170,29 @@ export async function forkChat(db: IDBDatabase, chatId: string, messageId: strin // Get messages up to and including the selected message const messages = chat.messages.slice(0, messageIndex + 1); - // Generate new IDs - const newId = await getNextId(db); - const urlId = await getUrlId(db, newId); - - // Create the forked chat - await setMessages( - db, - newId, - messages, - urlId, - chat.description ? `${chat.description} (fork)` : 'Forked chat' - ); - - return urlId; + return createChatFromMessages(db, chat.description ? `${chat.description} (fork)` : 'Forked chat', messages); } + + export async function duplicateChat(db: IDBDatabase, id: string): Promise { const chat = await getMessages(db, id); if (!chat) { throw new Error('Chat not found'); } + return createChatFromMessages(db, `${chat.description || 'Chat'} (copy)`, chat.messages); +} +export async function createChatFromMessages(db: IDBDatabase, description: string, messages: Message[]) : Promise { const newId = await getNextId(db); const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat await setMessages( db, newId, - chat.messages, + messages, newUrlId, // Use the new urlId - `${chat.description || 'Chat'} (copy)` + description ); return newUrlId; // Return the urlId instead of id for navigation diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index f5e8138..9cf649b 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -4,7 +4,15 @@ import { atom } from 'nanostores'; import type { Message } from 'ai'; import { toast } from 'react-toastify'; import { workbenchStore } from '~/lib/stores/workbench'; -import { getMessages, getNextId, getUrlId, openDatabase, setMessages, duplicateChat } from './db'; +import { + getMessages, + getNextId, + getUrlId, + openDatabase, + setMessages, + duplicateChat, + createChatFromMessages +} from './db'; export interface ChatHistoryItem { id: string; @@ -111,6 +119,41 @@ export function useChatHistory() { } catch (error) { toast.error('Failed to duplicate chat'); } + }, + importChat: async (description: string, messages:Message[]) => { + if (!db) { + return; + } + + try { + const newId = await createChatFromMessages(db, description, messages); + window.location.href = `/chat/${newId}`; + toast.success('Chat imported successfully'); + } catch (error) { + 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); } }; } diff --git a/app/utils/chatExport.ts b/app/utils/chatExport.ts deleted file mode 100644 index 1750ddd..0000000 --- a/app/utils/chatExport.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { Message } from 'ai'; -import { toast } from 'react-toastify'; - -export interface ChatExportData { - messages: Message[]; - description?: string; - exportDate: string; -} - -export const exportChat = (messages: Message[], description?: string) => { - const chatData: ChatExportData = { - messages, - 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); -}; - -export const importChat = async (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => { - try { - const content = e.target?.result as string; - const data = JSON.parse(content); - if (!Array.isArray(data.messages)) { - throw new Error('Invalid chat file format'); - } - resolve(data); - } catch (error) { - reject(new Error('Failed to parse chat file')); - } - }; - reader.onerror = () => reject(new Error('Failed to read chat file')); - reader.readAsText(file); - }); -};