Eduards commited on
Commit
082339c
·
1 Parent(s): 23d7182

Added tooltips and fork

Browse files
app/components/chat/Messages.client.tsx CHANGED
@@ -3,7 +3,11 @@ import React from 'react';
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
 
6
  import { useLocation, useNavigate } from '@remix-run/react';
 
 
 
7
 
8
  interface MessagesProps {
9
  id?: string;
@@ -19,54 +23,107 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
19
 
20
  const handleRewind = (messageId: string) => {
21
  const searchParams = new URLSearchParams(location.search);
22
- searchParams.set('rewindId', messageId);
23
  window.location.search = searchParams.toString();
24
- //navigate(`${location.pathname}?${searchParams.toString()}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  };
26
 
27
  return (
28
- <div id={id} ref={ref} className={props.className}>
29
- {messages.length > 0
30
- ? messages.map((message, index) => {
31
- const { role, content, id: messageId } = message;
32
- const isUserMessage = role === 'user';
33
- const isFirst = index === 0;
34
- const isLast = index === messages.length - 1;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- return (
37
- <div
38
- key={index}
39
- className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
40
- 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
41
- 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
42
- isStreaming && isLast,
43
- 'mt-4': !isFirst,
44
- })}
45
- >
46
- {isUserMessage && (
47
- <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
48
- <div className="i-ph:user-fill text-xl"></div>
 
 
 
 
 
 
 
 
 
49
  </div>
50
- )}
51
- <div className="grid grid-col-1 w-full">
52
- {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
53
  </div>
54
- {messageId && (
55
- <button
56
- onClick={() => handleRewind(messageId)}
57
- className="self-start p-2 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
58
- title="Rewind to this message"
59
- >
60
- <div className="i-ph:arrow-counter-clockwise text-xl"></div>
61
- </button>
62
- )}
63
- </div>
64
- );
65
- })
66
- : null}
67
- {isStreaming && (
68
- <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
69
- )}
70
- </div>
71
  );
72
  });
 
3
  import { classNames } from '~/utils/classNames';
4
  import { AssistantMessage } from './AssistantMessage';
5
  import { UserMessage } from './UserMessage';
6
+ import * as Tooltip from '@radix-ui/react-tooltip';
7
  import { useLocation, useNavigate } from '@remix-run/react';
8
+ import { db, chatId } from '~/lib/persistence/useChatHistory';
9
+ import { forkChat } from '~/lib/persistence/db';
10
+ import { toast } from 'react-toastify';
11
 
12
  interface MessagesProps {
13
  id?: string;
 
23
 
24
  const handleRewind = (messageId: string) => {
25
  const searchParams = new URLSearchParams(location.search);
26
+ searchParams.set('rewindTo', messageId);
27
  window.location.search = searchParams.toString();
28
+ };
29
+
30
+ const handleFork = async (messageId: string) => {
31
+ try {
32
+ if (!db || !chatId.get()) {
33
+ toast.error('Chat persistence is not available');
34
+ return;
35
+ }
36
+
37
+ const urlId = await forkChat(db, chatId.get()!, messageId);
38
+ window.location.href = `/chat/${urlId}`;
39
+ } catch (error) {
40
+ toast.error('Failed to fork chat: ' + (error as Error).message);
41
+ }
42
  };
43
 
44
  return (
45
+ <Tooltip.Provider delayDuration={200}>
46
+ <div id={id} ref={ref} className={props.className}>
47
+ {messages.length > 0
48
+ ? messages.map((message, index) => {
49
+ const { role, content, id: messageId } = message;
50
+ const isUserMessage = role === 'user';
51
+ const isFirst = index === 0;
52
+ const isLast = index === messages.length - 1;
53
+
54
+ return (
55
+ <div
56
+ key={index}
57
+ className={classNames('flex gap-4 p-6 w-full rounded-[calc(0.75rem-1px)]', {
58
+ 'bg-bolt-elements-messages-background': isUserMessage || !isStreaming || (isStreaming && !isLast),
59
+ 'bg-gradient-to-b from-bolt-elements-messages-background from-30% to-transparent':
60
+ isStreaming && isLast,
61
+ 'mt-4': !isFirst,
62
+ })}
63
+ >
64
+ {isUserMessage && (
65
+ <div className="flex items-center justify-center w-[34px] h-[34px] overflow-hidden bg-white text-gray-600 rounded-full shrink-0 self-start">
66
+ <div className="i-ph:user-fill text-xl"></div>
67
+ </div>
68
+ )}
69
+ <div className="grid grid-col-1 w-full">
70
+ {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
71
+ </div>
72
+ <div className="flex gap-2">
73
+ <Tooltip.Root>
74
+ <Tooltip.Trigger asChild>
75
+ {messageId && (<button
76
+ onClick={() => handleRewind(messageId)}
77
+ key='i-ph:arrow-u-up-left'
78
+ className={classNames(
79
+ 'i-ph:arrow-u-up-left',
80
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
81
+ )}
82
+ />)}
83
+ </Tooltip.Trigger>
84
+ <Tooltip.Portal>
85
+ <Tooltip.Content
86
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
87
+ sideOffset={5}
88
+ style={{zIndex: 1000}}
89
+ >
90
+ Revert to this message
91
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
92
+ </Tooltip.Content>
93
+ </Tooltip.Portal>
94
+ </Tooltip.Root>
95
 
96
+ <Tooltip.Root>
97
+ <Tooltip.Trigger asChild>
98
+ <button
99
+ onClick={() => handleFork(messageId)}
100
+ key='i-ph:git-fork'
101
+ className={classNames(
102
+ 'i-ph:git-fork',
103
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
104
+ )}
105
+ />
106
+ </Tooltip.Trigger>
107
+ <Tooltip.Portal>
108
+ <Tooltip.Content
109
+ className="bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg"
110
+ sideOffset={5}
111
+ style={{zIndex: 1000}}
112
+ >
113
+ Fork chat from this message
114
+ <Tooltip.Arrow className="fill-bolt-elements-tooltip-background" />
115
+ </Tooltip.Content>
116
+ </Tooltip.Portal>
117
+ </Tooltip.Root>
118
  </div>
 
 
 
119
  </div>
120
+ );
121
+ })
122
+ : null}
123
+ {isStreaming && (
124
+ <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
125
+ )}
126
+ </div>
127
+ </Tooltip.Provider>
 
 
 
 
 
 
 
 
 
128
  );
129
  });
app/lib/persistence/db.ts CHANGED
@@ -159,6 +159,33 @@ async function getUrlIds(db: IDBDatabase): Promise<string[]> {
159
  });
160
  }
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
163
  const chat = await getMessages(db, id);
164
  if (!chat) {
 
159
  });
160
  }
161
 
162
+ export async function forkChat(db: IDBDatabase, chatId: string, messageId: string): Promise<string> {
163
+ const chat = await getMessages(db, chatId);
164
+ if (!chat) throw new Error('Chat not found');
165
+
166
+ // Find the index of the message to fork at
167
+ const messageIndex = chat.messages.findIndex(msg => msg.id === messageId);
168
+ if (messageIndex === -1) throw new Error('Message not found');
169
+
170
+ // Get messages up to and including the selected message
171
+ const messages = chat.messages.slice(0, messageIndex + 1);
172
+
173
+ // Generate new IDs
174
+ const newId = await getNextId(db);
175
+ const urlId = await getUrlId(db, newId);
176
+
177
+ // Create the forked chat
178
+ await setMessages(
179
+ db,
180
+ newId,
181
+ messages,
182
+ urlId,
183
+ chat.description ? `${chat.description} (fork)` : 'Forked chat'
184
+ );
185
+
186
+ return urlId;
187
+ }
188
+
189
  export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
190
  const chat = await getMessages(db, id);
191
  if (!chat) {
eslint.config.mjs CHANGED
@@ -4,7 +4,7 @@ import { getNamingConventionRule, tsFileExtensions } from '@blitz/eslint-plugin/
4
 
5
  export default [
6
  {
7
- ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
 
4
 
5
  export default [
6
  {
7
+ ignores: ['**/dist', '**/node_modules', '**/.wrangler', '**/bolt/build', '**/.history'],
8
  },
9
  ...blitzPlugin.configs.recommended(),
10
  {
package.json CHANGED
@@ -54,6 +54,7 @@
54
  "@openrouter/ai-sdk-provider": "^0.0.5",
55
  "@radix-ui/react-dialog": "^1.1.1",
56
  "@radix-ui/react-dropdown-menu": "^2.1.1",
 
57
  "@remix-run/cloudflare": "^2.10.2",
58
  "@remix-run/cloudflare-pages": "^2.10.2",
59
  "@remix-run/react": "^2.10.2",
 
54
  "@openrouter/ai-sdk-provider": "^0.0.5",
55
  "@radix-ui/react-dialog": "^1.1.1",
56
  "@radix-ui/react-dropdown-menu": "^2.1.1",
57
+ "@radix-ui/react-tooltip": "^1.1.4",
58
  "@remix-run/cloudflare": "^2.10.2",
59
  "@remix-run/cloudflare-pages": "^2.10.2",
60
  "@remix-run/react": "^2.10.2",