Eduards commited on
Commit
fb34a4c
·
1 Parent(s): 9f49c25

Add import, fix export

Browse files
app/components/chat/BaseChat.tsx CHANGED
@@ -12,7 +12,6 @@ import { Messages } from './Messages.client';
12
  import { SendButton } from './SendButton.client';
13
  import { APIKeyManager } from './APIKeyManager';
14
  import Cookies from 'js-cookie';
15
- import { exportChat, importChat } from '~/utils/chatExport';
16
  import { toast } from 'react-toastify';
17
  import * as Tooltip from '@radix-ui/react-tooltip';
18
 
@@ -90,6 +89,8 @@ interface BaseChatProps {
90
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
91
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
92
  enhancePrompt?: () => void;
 
 
93
  }
94
 
95
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -113,7 +114,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
113
  sendMessage,
114
  handleInputChange,
115
  enhancePrompt,
116
- handleStop
 
 
117
  },
118
  ref
119
  ) => {
@@ -292,7 +295,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
292
  </>
293
  )}
294
  </IconButton>
295
- <ClientOnly>{() => <ExportChatButton description={description} messages={messages}/>}</ClientOnly>
296
  </div>
297
  {input.length > 3 ? (
298
  <div className="text-xs text-bolt-elements-textTertiary">
@@ -317,18 +320,31 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
317
  accept=".json"
318
  onChange={async (e) => {
319
  const file = e.target.files?.[0];
320
- if (file) {
321
  try {
322
- const { messages: importedMessages } = await importChat(file);
323
- // Import each message
324
- for (const msg of importedMessages) {
325
- await sendMessage(new Event('import') as unknown as React.UIEvent, msg.content);
326
- }
327
- toast.success('Chat imported successfully');
 
 
 
 
 
 
 
 
 
 
 
328
  } catch (error) {
329
  toast.error(error instanceof Error ? error.message : 'Failed to import chat');
330
  }
331
  e.target.value = ''; // Reset file input
 
 
332
  }
333
  }}
334
  />
 
12
  import { SendButton } from './SendButton.client';
13
  import { APIKeyManager } from './APIKeyManager';
14
  import Cookies from 'js-cookie';
 
15
  import { toast } from 'react-toastify';
16
  import * as Tooltip from '@radix-ui/react-tooltip';
17
 
 
89
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
90
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
91
  enhancePrompt?: () => void;
92
+ importChat?: (description: string, messages: Message[]) => Promise<void>;
93
+ exportChat?: () => void;
94
  }
95
 
96
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
 
114
  sendMessage,
115
  handleInputChange,
116
  enhancePrompt,
117
+ handleStop,
118
+ importChat,
119
+ exportChat
120
  },
121
  ref
122
  ) => {
 
295
  </>
296
  )}
297
  </IconButton>
298
+ <ClientOnly>{() => <ExportChatButton exportChat={exportChat}/>}</ClientOnly>
299
  </div>
300
  {input.length > 3 ? (
301
  <div className="text-xs text-bolt-elements-textTertiary">
 
320
  accept=".json"
321
  onChange={async (e) => {
322
  const file = e.target.files?.[0];
323
+ if (file && importChat) {
324
  try {
325
+ const reader = new FileReader();
326
+ reader.onload = async (e) => {
327
+ try {
328
+ const content = e.target?.result as string;
329
+ const data = JSON.parse(content);
330
+ if (!Array.isArray(data.messages)) {
331
+ toast.error('Invalid chat file format');
332
+ }
333
+ await importChat(data.description, data.messages);
334
+ toast.success('Chat imported successfully');
335
+ } catch (error) {
336
+ toast.error('Failed to parse chat file');
337
+ }
338
+ };
339
+ reader.onerror = () => toast.error('Failed to read chat file');
340
+ reader.readAsText(file);
341
+
342
  } catch (error) {
343
  toast.error(error instanceof Error ? error.message : 'Failed to import chat');
344
  }
345
  e.target.value = ''; // Reset file input
346
+ } else {
347
+ toast.error('Something went wrong');
348
  }
349
  }}
350
  />
app/components/chat/Chat.client.tsx CHANGED
@@ -28,12 +28,12 @@ const logger = createScopedLogger('Chat');
28
  export function Chat() {
29
  renderLogger.trace('Chat');
30
 
31
- const { ready, initialMessages, storeMessageHistory } = useChatHistory();
32
  const title = useStore(description);
33
 
34
  return (
35
  <>
36
- {ready && <ChatImpl description={title} initialMessages={initialMessages} storeMessageHistory={storeMessageHistory} />}
37
  <ToastContainer
38
  closeButton={({ closeToast }) => {
39
  return (
@@ -68,9 +68,11 @@ export function Chat() {
68
  interface ChatProps {
69
  initialMessages: Message[];
70
  storeMessageHistory: (messages: Message[]) => Promise<void>;
 
 
71
  }
72
 
73
- export const ChatImpl = memo(({ description, initialMessages, storeMessageHistory }: ChatProps) => {
74
  useShortcuts();
75
 
76
  const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -254,6 +256,8 @@ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistor
254
  handleInputChange={handleInputChange}
255
  handleStop={abort}
256
  description={description}
 
 
257
  messages={messages.map((message, i) => {
258
  if (message.role === 'user') {
259
  return message;
 
28
  export function Chat() {
29
  renderLogger.trace('Chat');
30
 
31
+ const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
32
  const title = useStore(description);
33
 
34
  return (
35
  <>
36
+ {ready && <ChatImpl description={title} initialMessages={initialMessages} exportChat={exportChat} storeMessageHistory={storeMessageHistory} importChat={importChat} />}
37
  <ToastContainer
38
  closeButton={({ closeToast }) => {
39
  return (
 
68
  interface ChatProps {
69
  initialMessages: Message[];
70
  storeMessageHistory: (messages: Message[]) => Promise<void>;
71
+ importChat: (description: string, messages: Message[]) => Promise<void>;
72
+ exportChat: () => void;
73
  }
74
 
75
+ export const ChatImpl = memo(({ description, initialMessages, storeMessageHistory, importChat, exportChat }: ChatProps) => {
76
  useShortcuts();
77
 
78
  const textareaRef = useRef<HTMLTextAreaElement>(null);
 
256
  handleInputChange={handleInputChange}
257
  handleStop={abort}
258
  description={description}
259
+ importChat={importChat}
260
+ exportChat={exportChat}
261
  messages={messages.map((message, i) => {
262
  if (message.role === 'user') {
263
  return message;
app/components/chat/ExportChatButton.tsx CHANGED
@@ -1,14 +1,12 @@
1
  import WithTooltip from '~/components/ui/Tooltip';
2
  import { IconButton } from '~/components/ui/IconButton';
3
- import { exportChat } from '~/utils/chatExport';
4
  import React from 'react';
5
- import type { Message } from 'ai';
6
 
7
- export const ExportChatButton = ({description, messages}: {description: string, messages: Message[]}) => {
8
  return (<WithTooltip tooltip="Export Chat">
9
  <IconButton
10
  title="Export Chat"
11
- onClick={() => exportChat(messages || [], description)}
12
  >
13
  <div className="i-ph:download-simple text-xl"></div>
14
  </IconButton>
 
1
  import WithTooltip from '~/components/ui/Tooltip';
2
  import { IconButton } from '~/components/ui/IconButton';
 
3
  import React from 'react';
 
4
 
5
+ export const ExportChatButton = ({exportChat}: {exportChat: () => void}) => {
6
  return (<WithTooltip tooltip="Export Chat">
7
  <IconButton
8
  title="Export Chat"
9
+ onClick={exportChat}
10
  >
11
  <div className="i-ph:download-simple text-xl"></div>
12
  </IconButton>
app/components/chat/Messages.client.tsx CHANGED
@@ -33,7 +33,6 @@ export const Messages = React.forwardRef<HTMLDivElement, MessagesProps>((props:
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) {
 
33
  toast.error('Chat persistence is not available');
34
  return;
35
  }
 
36
  const urlId = await forkChat(db, chatId.get()!, messageId);
37
  window.location.href = `/chat/${urlId}`;
38
  } catch (error) {
app/components/sidebar/HistoryItem.tsx CHANGED
@@ -1,16 +1,16 @@
1
  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
  import WithTooltip from '~/components/ui/Tooltip';
6
 
7
  interface HistoryItemProps {
8
  item: ChatHistoryItem;
9
  onDelete?: (event: React.UIEvent) => void;
10
  onDuplicate?: (id: string) => void;
 
11
  }
12
 
13
- export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
14
  const [hovering, setHovering] = useState(false);
15
  const hoverRef = useRef<HTMLDivElement>(null);
16
 
@@ -53,7 +53,8 @@ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
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
  />
 
1
  import * as Dialog from '@radix-ui/react-dialog';
2
  import { useEffect, useRef, useState } from 'react';
3
  import { type ChatHistoryItem } from '~/lib/persistence';
 
4
  import WithTooltip from '~/components/ui/Tooltip';
5
 
6
  interface HistoryItemProps {
7
  item: ChatHistoryItem;
8
  onDelete?: (event: React.UIEvent) => void;
9
  onDuplicate?: (id: string) => void;
10
+ exportChat: (id?: string) => void;
11
  }
12
 
13
+ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: HistoryItemProps) {
14
  const [hovering, setHovering] = useState(false);
15
  const hoverRef = useRef<HTMLDivElement>(null);
16
 
 
53
  className="i-ph:download-simple scale-110 mr-2"
54
  onClick={(event) => {
55
  event.preventDefault();
56
+ exportChat(item.id);
57
+ //exportChat(item.messages, item.description);
58
  }}
59
  title="Export chat"
60
  />
app/components/sidebar/Menu.client.tsx CHANGED
@@ -34,7 +34,7 @@ const menuVariants = {
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export function Menu() {
37
- const { duplicateCurrentChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
@@ -102,7 +102,6 @@ export function Menu() {
102
 
103
  const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
104
  event.preventDefault();
105
-
106
  setDialogContent({ type: 'delete', item });
107
  };
108
 
@@ -144,6 +143,7 @@ export function Menu() {
144
  <HistoryItem
145
  key={item.id}
146
  item={item}
 
147
  onDelete={(event) => handleDeleteClick(event, item)}
148
  onDuplicate={() => handleDuplicate(item.id)}
149
  />
 
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export function Menu() {
37
+ const { duplicateCurrentChat, exportChat } = useChatHistory();
38
  const menuRef = useRef<HTMLDivElement>(null);
39
  const [list, setList] = useState<ChatHistoryItem[]>([]);
40
  const [open, setOpen] = useState(false);
 
102
 
103
  const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
104
  event.preventDefault();
 
105
  setDialogContent({ type: 'delete', item });
106
  };
107
 
 
143
  <HistoryItem
144
  key={item.id}
145
  item={item}
146
+ exportChat={exportChat}
147
  onDelete={(event) => handleDeleteClick(event, item)}
148
  onDuplicate={() => handleDuplicate(item.id)}
149
  />
app/lib/persistence/db.ts CHANGED
@@ -170,37 +170,29 @@ export async function forkChat(db: IDBDatabase, chatId: string, messageId: strin
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) {
192
  throw new Error('Chat not found');
193
  }
 
 
194
 
 
195
  const newId = await getNextId(db);
196
  const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
197
 
198
  await setMessages(
199
  db,
200
  newId,
201
- chat.messages,
202
  newUrlId, // Use the new urlId
203
- `${chat.description || 'Chat'} (copy)`
204
  );
205
 
206
  return newUrlId; // Return the urlId instead of id for navigation
 
170
  // Get messages up to and including the selected message
171
  const messages = chat.messages.slice(0, messageIndex + 1);
172
 
173
+ return createChatFromMessages(db, chat.description ? `${chat.description} (fork)` : 'Forked chat', messages);
174
+ }
 
175
 
 
 
 
 
 
 
 
 
176
 
 
 
177
 
178
  export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
179
  const chat = await getMessages(db, id);
180
  if (!chat) {
181
  throw new Error('Chat not found');
182
  }
183
+ return createChatFromMessages(db, `${chat.description || 'Chat'} (copy)`, chat.messages);
184
+ }
185
 
186
+ export async function createChatFromMessages(db: IDBDatabase, description: string, messages: Message[]) : Promise<string> {
187
  const newId = await getNextId(db);
188
  const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
189
 
190
  await setMessages(
191
  db,
192
  newId,
193
+ messages,
194
  newUrlId, // Use the new urlId
195
+ description
196
  );
197
 
198
  return newUrlId; // Return the urlId instead of id for navigation
app/lib/persistence/useChatHistory.ts CHANGED
@@ -4,7 +4,15 @@ import { atom } from 'nanostores';
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
- import { getMessages, getNextId, getUrlId, openDatabase, setMessages, duplicateChat } from './db';
 
 
 
 
 
 
 
 
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
@@ -111,6 +119,41 @@ export function useChatHistory() {
111
  } catch (error) {
112
  toast.error('Failed to duplicate chat');
113
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
  };
116
  }
 
4
  import type { Message } from 'ai';
5
  import { toast } from 'react-toastify';
6
  import { workbenchStore } from '~/lib/stores/workbench';
7
+ import {
8
+ getMessages,
9
+ getNextId,
10
+ getUrlId,
11
+ openDatabase,
12
+ setMessages,
13
+ duplicateChat,
14
+ createChatFromMessages
15
+ } from './db';
16
 
17
  export interface ChatHistoryItem {
18
  id: string;
 
119
  } catch (error) {
120
  toast.error('Failed to duplicate chat');
121
  }
122
+ },
123
+ importChat: async (description: string, messages:Message[]) => {
124
+ if (!db) {
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const newId = await createChatFromMessages(db, description, messages);
130
+ window.location.href = `/chat/${newId}`;
131
+ toast.success('Chat imported successfully');
132
+ } catch (error) {
133
+ toast.error('Failed to import chat');
134
+ }
135
+ },
136
+ exportChat: async (id = urlId) => {
137
+ if (!db || !id) {
138
+ return;
139
+ }
140
+
141
+ const chat = await getMessages(db, id);
142
+ const chatData = {
143
+ messages: chat.messages,
144
+ description: chat.description,
145
+ exportDate: new Date().toISOString(),
146
+ };
147
+
148
+ const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
149
+ const url = URL.createObjectURL(blob);
150
+ const a = document.createElement('a');
151
+ a.href = url;
152
+ a.download = `chat-${new Date().toISOString()}.json`;
153
+ document.body.appendChild(a);
154
+ a.click();
155
+ document.body.removeChild(a);
156
+ URL.revokeObjectURL(url);
157
  }
158
  };
159
  }
app/utils/chatExport.ts DELETED
@@ -1,46 +0,0 @@
1
- import type { Message } from 'ai';
2
- import { toast } from 'react-toastify';
3
-
4
- export interface ChatExportData {
5
- messages: Message[];
6
- description?: string;
7
- exportDate: string;
8
- }
9
-
10
- export const exportChat = (messages: Message[], description?: string) => {
11
- const chatData: ChatExportData = {
12
- messages,
13
- description,
14
- exportDate: new Date().toISOString(),
15
- };
16
-
17
- const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' });
18
- const url = URL.createObjectURL(blob);
19
- const a = document.createElement('a');
20
- a.href = url;
21
- a.download = `chat-${new Date().toISOString()}.json`;
22
- document.body.appendChild(a);
23
- a.click();
24
- document.body.removeChild(a);
25
- URL.revokeObjectURL(url);
26
- };
27
-
28
- export const importChat = async (file: File): Promise<ChatExportData> => {
29
- return new Promise((resolve, reject) => {
30
- const reader = new FileReader();
31
- reader.onload = (e) => {
32
- try {
33
- const content = e.target?.result as string;
34
- const data = JSON.parse(content);
35
- if (!Array.isArray(data.messages)) {
36
- throw new Error('Invalid chat file format');
37
- }
38
- resolve(data);
39
- } catch (error) {
40
- reject(new Error('Failed to parse chat file'));
41
- }
42
- };
43
- reader.onerror = () => reject(new Error('Failed to read chat file'));
44
- reader.readAsText(file);
45
- });
46
- };