diff --git a/app/components/chat/Messages.client.tsx b/app/components/chat/Messages.client.tsx index 2809a5f9..35fe7d1c 100644 --- a/app/components/chat/Messages.client.tsx +++ b/app/components/chat/Messages.client.tsx @@ -3,7 +3,11 @@ import React from 'react'; import { classNames } from '~/utils/classNames'; import { AssistantMessage } from './AssistantMessage'; import { UserMessage } from './UserMessage'; +import * as Tooltip from '@radix-ui/react-tooltip'; import { useLocation, useNavigate } from '@remix-run/react'; +import { db, chatId } from '~/lib/persistence/useChatHistory'; +import { forkChat } from '~/lib/persistence/db'; +import { toast } from 'react-toastify'; interface MessagesProps { id?: string; @@ -19,54 +23,107 @@ export const Messages = React.forwardRef((props: const handleRewind = (messageId: string) => { const searchParams = new URLSearchParams(location.search); - searchParams.set('rewindId', messageId); + searchParams.set('rewindTo', messageId); window.location.search = searchParams.toString(); - //navigate(`${location.pathname}?${searchParams.toString()}`); + }; + + const handleFork = async (messageId: string) => { + try { + if (!db || !chatId.get()) { + toast.error('Chat persistence is not available'); + return; + } + + const urlId = await forkChat(db, chatId.get()!, messageId); + window.location.href = `/chat/${urlId}`; + } catch (error) { + toast.error('Failed to fork chat: ' + (error as Error).message); + } }; return ( -
- {messages.length > 0 - ? messages.map((message, index) => { - const { role, content, id: messageId } = message; - const isUserMessage = role === 'user'; - const isFirst = index === 0; - const isLast = index === messages.length - 1; + +
+ {messages.length > 0 + ? messages.map((message, index) => { + const { role, content, id: messageId } = message; + const isUserMessage = role === 'user'; + const isFirst = index === 0; + const isLast = index === messages.length - 1; - return ( -
- {isUserMessage && ( -
-
+ return ( +
+ {isUserMessage && ( +
+
+
+ )} +
+ {isUserMessage ? : } +
+
+ + + {messageId && (
- )} -
- {isUserMessage ? : }
- {messageId && ( - - )} -
- ); - }) - : null} - {isStreaming && ( -
- )} -
+ ); + }) + : null} + {isStreaming && ( +
+ )} +
+ ); }); diff --git a/app/lib/persistence/db.ts b/app/lib/persistence/db.ts index 5b96f009..3aa2004a 100644 --- a/app/lib/persistence/db.ts +++ b/app/lib/persistence/db.ts @@ -159,6 +159,33 @@ async function getUrlIds(db: IDBDatabase): Promise { }); } +export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise { + const chat = await getMessages(db, chatId); + if (!chat) throw new Error('Chat not found'); + + // Find the index of the message to fork at + const messageIndex = chat.messages.findIndex(msg => msg.id === messageId); + if (messageIndex === -1) throw new Error('Message not found'); + + // 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; +} + export async function duplicateChat(db: IDBDatabase, id: string): Promise { const chat = await getMessages(db, id); if (!chat) { diff --git a/eslint.config.mjs b/eslint.config.mjs index 95df41ef..123aaf1f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,7 +4,7 @@ import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/ export default [ { - ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build'], + ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'], }, ...blitzPlugin.configs.recommended(), { diff --git a/package.json b/package.json index ce8e95d0..40ede0f7 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@openrouter/ai-sdk-provider": "^0.0.5", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-tooltip": "^1.1.4", "@remix-run/cloudflare": "^2.10.2", "@remix-run/cloudflare-pages": "^2.10.2", "@remix-run/react": "^2.10.2",