Eduards commited on
Commit
6041155
·
1 Parent(s): a6060b8

Make tooltip easier to reuse across the app

Browse files
app/components/chat/BaseChat.tsx CHANGED
@@ -1,7 +1,7 @@
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
- import React, { type RefCallback, useEffect } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
@@ -10,9 +10,11 @@ import { classNames } from '~/utils/classNames';
10
  import { MODEL_LIST, DEFAULT_PROVIDER, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
- import { useState } from 'react';
14
  import { APIKeyManager } from './APIKeyManager';
15
  import Cookies from 'js-cookie';
 
 
 
16
 
17
  import styles from './BaseChat.module.scss';
18
  import type { ProviderInfo } from '~/utils/types';
@@ -22,7 +24,7 @@ const EXAMPLE_PROMPTS = [
22
  { text: 'Build a simple blog using Astro' },
23
  { text: 'Create a cookie consent form using Material UI' },
24
  { text: 'Make a space invaders game' },
25
- { text: 'How do I center a div?' },
26
  ];
27
 
28
  const providerList = PROVIDER_LIST;
@@ -107,9 +109,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
107
  sendMessage,
108
  handleInputChange,
109
  enhancePrompt,
110
- handleStop,
111
  },
112
- ref,
113
  ) => {
114
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
115
  const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
@@ -145,7 +147,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
145
  expires: 30, // 30 days
146
  secure: true, // Only send over HTTPS
147
  sameSite: 'strict', // Protect against CSRF
148
- path: '/', // Accessible across the site
149
  });
150
  } catch (error) {
151
  console.error('Error saving API keys to cookies:', error);
@@ -153,72 +155,73 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
153
  };
154
 
155
  return (
156
- <div
157
- ref={ref}
158
- className={classNames(
159
- styles.BaseChat,
160
- 'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1',
161
- )}
162
- data-chat-visible={showChat}
163
- >
164
- <ClientOnly>{() => <Menu />}</ClientOnly>
165
- <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
166
- <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
167
- {!chatStarted && (
168
- <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
169
- <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
170
- Where ideas begin
171
- </h1>
172
- <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
173
- Bring ideas to life in seconds or get help on existing projects.
174
- </p>
175
- </div>
176
- )}
177
- <div
178
- className={classNames('pt-6 px-6', {
179
- 'h-full flex flex-col': chatStarted,
180
- })}
181
- >
182
- <ClientOnly>
183
- {() => {
184
- return chatStarted ? (
185
- <Messages
186
- ref={messageRef}
187
- className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
188
- messages={messages}
189
- isStreaming={isStreaming}
190
- />
191
- ) : null;
192
- }}
193
- </ClientOnly>
194
  <div
195
- className={classNames(
196
- 'bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
197
- {
198
- 'sticky bottom-0': chatStarted
199
- })}
200
  >
201
- <ModelSelector
202
- key={provider?.name + ':' + modelList.length}
203
- model={model}
204
- setModel={setModel}
205
- modelList={modelList}
206
- provider={provider}
207
- setProvider={setProvider}
208
- providerList={PROVIDER_LIST}
209
- />
210
- {provider && (
211
- <APIKeyManager
212
- provider={provider}
213
- apiKey={apiKeys[provider.name] || ''}
214
- setApiKey={(key) => updateApiKey(provider.name, key)}
215
- />
216
- )}
217
  <div
218
  className={classNames(
219
- 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all',
220
- )}
 
 
221
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
  <textarea
223
  ref={textareaRef}
224
  className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
@@ -239,88 +242,133 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
239
  }}
240
  style={{
241
  minHeight: TEXTAREA_MIN_HEIGHT,
242
- maxHeight: TEXTAREA_MAX_HEIGHT,
243
  }}
244
  placeholder="How can Bolt help you today?"
245
  translate="no"
246
  />
247
- <ClientOnly>
248
- {() => (
249
- <SendButton
250
- show={input.length > 0 || isStreaming}
251
- isStreaming={isStreaming}
252
- onClick={(event) => {
253
- if (isStreaming) {
254
- handleStop?.();
255
- return;
256
- }
257
 
258
- sendMessage?.(event);
259
- }}
260
- />
261
- )}
262
- </ClientOnly>
263
- <div className="flex justify-between items-center text-sm p-4 pt-2">
264
- <div className="flex gap-1 items-center">
265
- <IconButton
266
- title="Enhance prompt"
267
- disabled={input.length === 0 || enhancingPrompt}
268
- className={classNames('transition-all', {
269
- 'opacity-100!': enhancingPrompt,
270
- 'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
271
- promptEnhanced,
272
- })}
273
- onClick={() => enhancePrompt?.()}
274
- >
275
- {enhancingPrompt ? (
276
- <>
277
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
278
- <div className="ml-1.5">Enhancing prompt...</div>
279
- </>
280
- ) : (
281
- <>
282
- <div className="i-bolt:stars text-xl"></div>
283
- {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
284
- </>
285
- )}
286
- </IconButton>
287
- </div>
288
- {input.length > 3 ? (
289
- <div className="text-xs text-bolt-elements-textTertiary">
290
- Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
291
- <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for
292
- a new line
293
  </div>
294
- ) : null}
 
 
 
 
 
 
 
 
 
295
  </div>
 
296
  </div>
297
- <div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
298
  </div>
299
- </div>
300
- {!chatStarted && (
301
- <div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
302
- <div className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
303
- {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
304
- return (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
305
  <button
306
- key={index}
307
- onClick={(event) => {
308
- sendMessage?.(event, examplePrompt.text);
309
  }}
310
- className="group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme"
311
  >
312
- {examplePrompt.text}
313
- <div className="i-ph:arrow-bend-down-left" />
314
  </button>
315
- );
316
- })}
317
  </div>
318
- </div>
319
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
320
  </div>
321
- <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
322
  </div>
323
- </div>
324
  );
325
- },
326
  );
 
1
  // @ts-nocheck
2
  // Preventing TS checks with files presented in the video for a better presentation.
3
  import type { Message } from 'ai';
4
+ import React, { type RefCallback, useEffect, useState } from 'react';
5
  import { ClientOnly } from 'remix-utils/client-only';
6
  import { Menu } from '~/components/sidebar/Menu.client';
7
  import { IconButton } from '~/components/ui/IconButton';
 
10
  import { MODEL_LIST, DEFAULT_PROVIDER, PROVIDER_LIST, initializeModelList } from '~/utils/constants';
11
  import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
 
13
  import { APIKeyManager } from './APIKeyManager';
14
  import Cookies from 'js-cookie';
15
+ import { importChat } from '~/utils/chatExport';
16
+ import { toast } from 'react-toastify';
17
+ import * as Tooltip from '@radix-ui/react-tooltip';
18
 
19
  import styles from './BaseChat.module.scss';
20
  import type { ProviderInfo } from '~/utils/types';
 
24
  { text: 'Build a simple blog using Astro' },
25
  { text: 'Create a cookie consent form using Material UI' },
26
  { text: 'Make a space invaders game' },
27
+ { text: 'How do I center a div?' }
28
  ];
29
 
30
  const providerList = PROVIDER_LIST;
 
109
  sendMessage,
110
  handleInputChange,
111
  enhancePrompt,
112
+ handleStop
113
  },
114
+ ref
115
  ) => {
116
  const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200;
117
  const [apiKeys, setApiKeys] = useState<Record<string, string>>({});
 
147
  expires: 30, // 30 days
148
  secure: true, // Only send over HTTPS
149
  sameSite: 'strict', // Protect against CSRF
150
+ path: '/' // Accessible across the site
151
  });
152
  } catch (error) {
153
  console.error('Error saving API keys to cookies:', error);
 
155
  };
156
 
157
  return (
158
+ <Tooltip.Provider delayDuration={200}>
159
+ <div
160
+ ref={ref}
161
+ className={classNames(
162
+ styles.BaseChat,
163
+ 'relative flex h-full w-full overflow-hidden bg-bolt-elements-background-depth-1'
164
+ )}
165
+ data-chat-visible={showChat}
166
+ >
167
+ <ClientOnly>{() => <Menu />}</ClientOnly>
168
+ <div ref={scrollRef} className="flex overflow-y-auto w-full h-full">
169
+ <div className={classNames(styles.Chat, 'flex flex-col flex-grow min-w-[var(--chat-min-width)] h-full')}>
170
+ {!chatStarted && (
171
+ <div id="intro" className="mt-[26vh] max-w-chat mx-auto text-center">
172
+ <h1 className="text-6xl font-bold text-bolt-elements-textPrimary mb-4 animate-fade-in">
173
+ Where ideas begin
174
+ </h1>
175
+ <p className="text-xl mb-8 text-bolt-elements-textSecondary animate-fade-in animation-delay-200">
176
+ Bring ideas to life in seconds or get help on existing projects.
177
+ </p>
178
+ </div>
179
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  <div
181
+ className={classNames('pt-6 px-6', {
182
+ 'h-full flex flex-col': chatStarted
183
+ })}
 
 
184
  >
185
+ <ClientOnly>
186
+ {() => {
187
+ return chatStarted ? (
188
+ <Messages
189
+ ref={messageRef}
190
+ className="flex flex-col w-full flex-1 max-w-chat px-4 pb-6 mx-auto z-1"
191
+ messages={messages}
192
+ isStreaming={isStreaming}
193
+ />
194
+ ) : null;
195
+ }}
196
+ </ClientOnly>
 
 
 
 
197
  <div
198
  className={classNames(
199
+ 'bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
200
+ {
201
+ 'sticky bottom-0': chatStarted
202
+ })}
203
  >
204
+ <ModelSelector
205
+ key={provider?.name + ':' + modelList.length}
206
+ model={model}
207
+ setModel={setModel}
208
+ modelList={modelList}
209
+ provider={provider}
210
+ setProvider={setProvider}
211
+ providerList={PROVIDER_LIST}
212
+ />
213
+ {provider && (
214
+ <APIKeyManager
215
+ provider={provider}
216
+ apiKey={apiKeys[provider.name] || ''}
217
+ setApiKey={(key) => updateApiKey(provider.name, key)}
218
+ />
219
+ )}
220
+ <div
221
+ className={classNames(
222
+ 'shadow-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background backdrop-filter backdrop-blur-[8px] rounded-lg overflow-hidden transition-all'
223
+ )}
224
+ >
225
  <textarea
226
  ref={textareaRef}
227
  className={`w-full pl-4 pt-4 pr-16 focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus resize-none text-md text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent transition-all`}
 
242
  }}
243
  style={{
244
  minHeight: TEXTAREA_MIN_HEIGHT,
245
+ maxHeight: TEXTAREA_MAX_HEIGHT
246
  }}
247
  placeholder="How can Bolt help you today?"
248
  translate="no"
249
  />
250
+ <ClientOnly>
251
+ {() => (
252
+ <SendButton
253
+ show={input.length > 0 || isStreaming}
254
+ isStreaming={isStreaming}
255
+ onClick={(event) => {
256
+ if (isStreaming) {
257
+ handleStop?.();
258
+ return;
259
+ }
260
 
261
+ sendMessage?.(event);
262
+ }}
263
+ />
264
+ )}
265
+ </ClientOnly>
266
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
267
+ <div className="flex gap-1 items-center">
268
+ <IconButton
269
+ title="Enhance prompt"
270
+ disabled={input.length === 0 || enhancingPrompt}
271
+ className={classNames('transition-all', {
272
+ 'opacity-100!': enhancingPrompt,
273
+ 'text-bolt-elements-item-contentAccent! pr-1.5 enabled:hover:bg-bolt-elements-item-backgroundAccent!':
274
+ promptEnhanced
275
+ })}
276
+ onClick={() => enhancePrompt?.()}
277
+ >
278
+ {enhancingPrompt ? (
279
+ <>
280
+ <div
281
+ className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
282
+ <div className="ml-1.5">Enhancing prompt...</div>
283
+ </>
284
+ ) : (
285
+ <>
286
+ <div className="i-bolt:stars text-xl"></div>
287
+ {promptEnhanced && <div className="ml-1.5">Prompt enhanced</div>}
288
+ </>
289
+ )}
290
+ </IconButton>
 
 
 
 
 
291
  </div>
292
+ {input.length > 3 ? (
293
+ <div className="text-xs text-bolt-elements-textTertiary">
294
+ Use <kbd
295
+ className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
296
+ <kbd
297
+ className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for
298
+ a new line
299
+ </div>
300
+ ) : null}
301
+ </div>
302
  </div>
303
+ <div className="bg-bolt-elements-background-depth-1 pb-6">{/* Ghost Element */}</div>
304
  </div>
 
305
  </div>
306
+ {!chatStarted && (
307
+ <div className="flex flex-col items-center justify-center flex-1 p-4">
308
+ <input
309
+ type="file"
310
+ id="chat-import"
311
+ className="hidden"
312
+ accept=".json"
313
+ onChange={async (e) => {
314
+ const file = e.target.files?.[0];
315
+ if (file) {
316
+ try {
317
+ const { messages: importedMessages } = await importChat(file);
318
+ // Import each message
319
+ for (const msg of importedMessages) {
320
+ await sendMessage(new Event('import') as unknown as React.UIEvent, msg.content);
321
+ }
322
+ toast.success('Chat imported successfully');
323
+ } catch (error) {
324
+ toast.error(error instanceof Error ? error.message : 'Failed to import chat');
325
+ }
326
+ e.target.value = ''; // Reset file input
327
+ }
328
+ }}
329
+ />
330
+ <div className="flex flex-col items-center gap-4 max-w-2xl text-center">
331
+ <div className="flex gap-2">
332
  <button
333
+ onClick={() => {
334
+ const input = document.getElementById('chat-import');
335
+ input?.click();
336
  }}
337
+ className="px-4 py-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 transition-all flex items-center gap-2"
338
  >
339
+ <div className="i-ph:upload-simple" />
340
+ Import Chat
341
  </button>
342
+ </div>
343
+ </div>
344
  </div>
345
+ )}
346
+ {!chatStarted && (
347
+ <div id="examples" className="relative w-full max-w-xl mx-auto mt-8 flex justify-center">
348
+ <div
349
+ className="flex flex-col space-y-2 [mask-image:linear-gradient(to_bottom,black_0%,transparent_180%)] hover:[mask-image:none]">
350
+ {EXAMPLE_PROMPTS.map((examplePrompt, index) => {
351
+ return (
352
+ <button
353
+ key={index}
354
+ onClick={(event) => {
355
+ sendMessage?.(event, examplePrompt.text);
356
+ }}
357
+ className="group flex items-center w-full gap-2 justify-center bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary transition-theme"
358
+ >
359
+ {examplePrompt.text}
360
+ <div className="i-ph:arrow-bend-down-left" />
361
+ </button>
362
+ );
363
+ })}
364
+ </div>
365
+ </div>
366
+ )}
367
+ </div>
368
+ <ClientOnly>{() => <Workbench chatStarted={chatStarted} isStreaming={isStreaming} />}</ClientOnly>
369
  </div>
 
370
  </div>
371
+ </Tooltip.Provider>
372
  );
373
+ }
374
  );
app/components/chat/Messages.client.tsx CHANGED
@@ -3,11 +3,11 @@ import React from 'react';
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;
@@ -42,7 +42,6 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
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) => {
@@ -70,51 +69,27 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
70
  {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
71
  </div>
72
  {!isUserMessage && (<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
  );
@@ -124,6 +99,5 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
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
  });
 
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
  import { db, chatId } from '~/lib/persistence/useChatHistory';
8
  import { forkChat } from '~/lib/persistence/db';
9
  import { toast } from 'react-toastify';
10
+ import WithTooltip from '~/components/ui/Tooltip';
11
 
12
  interface MessagesProps {
13
  id?: string;
 
42
  };
43
 
44
  return (
 
45
  <div id={id} ref={ref} className={props.className}>
46
  {messages.length > 0
47
  ? messages.map((message, index) => {
 
69
  {isUserMessage ? <UserMessage content={content} /> : <AssistantMessage content={content} />}
70
  </div>
71
  {!isUserMessage && (<div className="flex gap-2">
72
+ <WithTooltip tooltip="Revert to this message">
73
+ {messageId && (<button
74
+ onClick={() => handleRewind(messageId)}
75
+ key='i-ph:arrow-u-up-left'
76
+ className={classNames(
77
+ 'i-ph:arrow-u-up-left',
78
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
79
+ )}
80
+ />)}
81
+ </WithTooltip>
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ <WithTooltip tooltip="Fork chat from this message">
84
+ <button
85
+ onClick={() => handleFork(messageId)}
86
+ key="i-ph:git-fork"
87
+ className={classNames(
88
+ 'i-ph:git-fork',
89
+ 'text-xl text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary transition-colors'
90
+ )}
91
+ />
92
+ </WithTooltip>
 
 
 
 
 
 
 
 
 
 
 
 
93
  </div>)}
94
  </div>
95
  );
 
99
  <div className="text-center w-full text-bolt-elements-textSecondary i-svg-spinners:3-dots-fade text-4xl mt-4"></div>
100
  )}
101
  </div>
 
102
  );
103
  });
app/components/sidebar/HistoryItem.tsx CHANGED
@@ -2,6 +2,7 @@ import * as Dialog from '@radix-ui/react-dialog';
2
  import { useEffect, useRef, useState } from 'react';
3
  import { type ChatHistoryItem } from '~/lib/persistence';
4
  import { exportChat } from '~/utils/chatExport';
 
5
 
6
  interface HistoryItemProps {
7
  item: ChatHistoryItem;
@@ -47,22 +48,27 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
47
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-99%">
48
  {hovering && (
49
  <div className="flex items-center p-1 text-bolt-elements-textSecondary">
50
- <button
51
- className="i-ph:download-simple scale-110 mr-2"
52
- onClick={(event) => {
53
- event.preventDefault();
54
- exportChat(item.messages, item.description);
55
- }}
56
- title="Export chat"
57
- />
58
- {onDuplicate && (
59
  <button
60
- className="i-ph:copy scale-110 mr-2"
61
- onClick={() => onDuplicate?.(item.id)}
62
- title="Duplicate chat"
 
 
 
63
  />
 
 
 
 
 
 
 
 
 
64
  )}
65
  <Dialog.Trigger asChild>
 
66
  <button
67
  className="i-ph:trash scale-110"
68
  onClick={(event) => {
@@ -71,6 +77,7 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
71
  onDelete?.(event);
72
  }}
73
  />
 
74
  </Dialog.Trigger>
75
  </div>
76
  )}
 
2
  import { useEffect, useRef, useState } from 'react';
3
  import { type ChatHistoryItem } from '~/lib/persistence';
4
  import { exportChat } from '~/utils/chatExport';
5
+ import WithTooltip from '~/components/ui/Tooltip';
6
 
7
  interface HistoryItemProps {
8
  item: ChatHistoryItem;
 
48
  <div className="absolute right-0 z-1 top-0 bottom-0 bg-gradient-to-l from-bolt-elements-background-depth-2 group-hover:from-bolt-elements-background-depth-3 box-content pl-3 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-99%">
49
  {hovering && (
50
  <div className="flex items-center p-1 text-bolt-elements-textSecondary">
51
+ <WithTooltip tooltip="Export chat">
 
 
 
 
 
 
 
 
52
  <button
53
+ className="i-ph:download-simple scale-110 mr-2"
54
+ onClick={(event) => {
55
+ event.preventDefault();
56
+ exportChat(item.messages, item.description);
57
+ }}
58
+ title="Export chat"
59
  />
60
+ </WithTooltip>
61
+ {onDuplicate && (
62
+ <WithTooltip tooltip="Duplicate chat">
63
+ <button
64
+ className="i-ph:copy scale-110 mr-2"
65
+ onClick={() => onDuplicate?.(item.id)}
66
+ title="Duplicate chat"
67
+ />
68
+ </WithTooltip>
69
  )}
70
  <Dialog.Trigger asChild>
71
+ <WithTooltip tooltip="Delete chat">
72
  <button
73
  className="i-ph:trash scale-110"
74
  onClick={(event) => {
 
77
  onDelete?.(event);
78
  }}
79
  />
80
+ </WithTooltip>
81
  </Dialog.Trigger>
82
  </div>
83
  )}
app/components/sidebar/Menu.client.tsx CHANGED
@@ -17,8 +17,8 @@ const menuVariants = {
17
  left: '-150px',
18
  transition: {
19
  duration: 0.2,
20
- ease: cubicEasingFn,
21
- },
22
  },
23
  open: {
24
  opacity: 1,
@@ -26,9 +26,9 @@ const menuVariants = {
26
  left: 0,
27
  transition: {
28
  duration: 0.2,
29
- ease: cubicEasingFn,
30
- },
31
- },
32
  } satisfies Variants;
33
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
@@ -136,7 +136,8 @@ export function Menu() {
136
  <DialogRoot open={dialogContent !== null}>
137
  {binDates(list).map(({ category, items }) => (
138
  <div key={category} className="mt-4 first:mt-0 space-y-1">
139
- <div className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
 
140
  {category}
141
  </div>
142
  {items.map((item) => (
 
17
  left: '-150px',
18
  transition: {
19
  duration: 0.2,
20
+ ease: cubicEasingFn
21
+ }
22
  },
23
  open: {
24
  opacity: 1,
 
26
  left: 0,
27
  transition: {
28
  duration: 0.2,
29
+ ease: cubicEasingFn
30
+ }
31
+ }
32
  } satisfies Variants;
33
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
 
136
  <DialogRoot open={dialogContent !== null}>
137
  {binDates(list).map(({ category, items }) => (
138
  <div key={category} className="mt-4 first:mt-0 space-y-1">
139
+ <div
140
+ className="text-bolt-elements-textTertiary sticky top-0 z-1 bg-bolt-elements-background-depth-2 pl-2 pt-2 pb-1">
141
  {category}
142
  </div>
143
  {items.map((item) => (
app/components/ui/Tooltip.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import * as Tooltip from '@radix-ui/react-tooltip';
3
+
4
+ const WithTooltip = ({ tooltip, children, sideOffset = 5, className = '', arrowClassName = '', tooltipStyle = {} }) => {
5
+ return (
6
+ <Tooltip.Root>
7
+ <Tooltip.Trigger asChild>
8
+ {children}
9
+ </Tooltip.Trigger>
10
+ <Tooltip.Portal>
11
+ <Tooltip.Content
12
+ className={`bg-bolt-elements-tooltip-background text-bolt-elements-textPrimary px-3 py-2 rounded-lg text-sm shadow-lg ${className}`}
13
+ sideOffset={sideOffset}
14
+ style={{ zIndex: 2000, backgroundColor: "white", ...tooltipStyle }}
15
+ >
16
+ {tooltip}
17
+ <Tooltip.Arrow className={`fill-bolt-elements-tooltip-background ${arrowClassName}`} />
18
+ </Tooltip.Content>
19
+ </Tooltip.Portal>
20
+ </Tooltip.Root>
21
+ );
22
+ };
23
+
24
+ export default WithTooltip;