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

Add ability to duplicate chat in sidebar

Browse files
app/components/sidebar/HistoryItem.tsx CHANGED
@@ -5,9 +5,10 @@ import { type ChatHistoryItem } from '~/lib/persistence';
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
 
8
  }
9
 
10
- export function HistoryItem({ item, onDelete }: HistoryItemProps) {
11
  const [hovering, setHovering] = useState(false);
12
  const hoverRef = useRef<HTMLDivElement>(null);
13
 
@@ -44,7 +45,14 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) {
44
  {item.description}
45
  <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 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
46
  {hovering && (
47
- <div className="flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger">
 
 
 
 
 
 
 
48
  <Dialog.Trigger asChild>
49
  <button
50
  className="i-ph:trash scale-110"
 
5
  interface HistoryItemProps {
6
  item: ChatHistoryItem;
7
  onDelete?: (event: React.UIEvent) => void;
8
+ onDuplicate?: (id: string) => void;
9
  }
10
 
11
+ export function HistoryItem({ item, onDelete, onDuplicate }: HistoryItemProps) {
12
  const [hovering, setHovering] = useState(false);
13
  const hoverRef = useRef<HTMLDivElement>(null);
14
 
 
45
  {item.description}
46
  <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 to-transparent w-10 flex justify-end group-hover:w-15 group-hover:from-45%">
47
  {hovering && (
48
+ <div className="flex items-center p-1 text-bolt-elements-textSecondary">
49
+ {onDuplicate && (
50
+ <button
51
+ className="i-ph:copy scale-110 mr-2"
52
+ onClick={() => onDuplicate?.(item.id)}
53
+ title="Duplicate chat"
54
+ />
55
+ )}
56
  <Dialog.Trigger asChild>
57
  <button
58
  className="i-ph:trash scale-110"
app/components/sidebar/Menu.client.tsx CHANGED
@@ -4,7 +4,7 @@ import { toast } from 'react-toastify';
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { IconButton } from '~/components/ui/IconButton';
6
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
7
- import { db, deleteById, getAll, chatId, type ChatHistoryItem } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
@@ -34,6 +34,7 @@ const menuVariants = {
34
  type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
35
 
36
  export function Menu() {
 
37
  const menuRef = useRef<HTMLDivElement>(null);
38
  const [list, setList] = useState<ChatHistoryItem[]>([]);
39
  const [open, setOpen] = useState(false);
@@ -99,6 +100,17 @@ export function Menu() {
99
  };
100
  }, []);
101
 
 
 
 
 
 
 
 
 
 
 
 
102
  return (
103
  <motion.div
104
  ref={menuRef}
@@ -128,7 +140,12 @@ export function Menu() {
128
  {category}
129
  </div>
130
  {items.map((item) => (
131
- <HistoryItem key={item.id} item={item} onDelete={() => setDialogContent({ type: 'delete', item })} />
 
 
 
 
 
132
  ))}
133
  </div>
134
  ))}
 
4
  import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
5
  import { IconButton } from '~/components/ui/IconButton';
6
  import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
7
+ import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
8
  import { cubicEasingFn } from '~/utils/easings';
9
  import { logger } from '~/utils/logger';
10
  import { HistoryItem } from './HistoryItem';
 
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);
 
100
  };
101
  }, []);
102
 
103
+ const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
104
+ event.preventDefault();
105
+
106
+ setDialogContent({ type: 'delete', item });
107
+ };
108
+
109
+ const handleDuplicate = async (id: string) => {
110
+ await duplicateCurrentChat(id);
111
+ loadEntries(); // Reload the list after duplication
112
+ };
113
+
114
  return (
115
  <motion.div
116
  ref={menuRef}
 
140
  {category}
141
  </div>
142
  {items.map((item) => (
143
+ <HistoryItem
144
+ key={item.id}
145
+ item={item}
146
+ onDelete={(event) => handleDeleteClick(event, item)}
147
+ onDuplicate={() => handleDuplicate(item.id)}
148
+ />
149
  ))}
150
  </div>
151
  ))}
app/lib/persistence/db.ts CHANGED
@@ -158,3 +158,23 @@ async function getUrlIds(db: IDBDatabase): Promise<string[]> {
158
  };
159
  });
160
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  };
159
  });
160
  }
161
+
162
+ export async function duplicateChat(db: IDBDatabase, id: string): Promise<string> {
163
+ const chat = await getMessages(db, id);
164
+ if (!chat) {
165
+ throw new Error('Chat not found');
166
+ }
167
+
168
+ const newId = await getNextId(db);
169
+ const newUrlId = await getUrlId(db, newId); // Get a new urlId for the duplicated chat
170
+
171
+ await setMessages(
172
+ db,
173
+ newId,
174
+ chat.messages,
175
+ newUrlId, // Use the new urlId
176
+ `${chat.description || 'Chat'} (copy)`
177
+ );
178
+
179
+ return newUrlId; // Return the urlId instead of id for navigation
180
+ }
app/lib/persistence/useChatHistory.ts CHANGED
@@ -4,7 +4,7 @@ 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 } from './db';
8
 
9
  export interface ChatHistoryItem {
10
  id: string;
@@ -46,11 +46,11 @@ export function useChatHistory() {
46
  .then((storedMessages) => {
47
  if (storedMessages && storedMessages.messages.length > 0) {
48
  const rewindId = searchParams.get('rewindId');
49
- const filteredMessages = rewindId
50
- ? storedMessages.messages.slice(0,
51
  storedMessages.messages.findIndex(m => m.id === rewindId) + 1)
52
  : storedMessages.messages;
53
-
54
  setInitialMessages(filteredMessages);
55
  setUrlId(storedMessages.urlId);
56
  description.set(storedMessages.description);
@@ -100,6 +100,19 @@ export function useChatHistory() {
100
 
101
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
102
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  };
104
  }
105
 
 
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;
 
46
  .then((storedMessages) => {
47
  if (storedMessages && storedMessages.messages.length > 0) {
48
  const rewindId = searchParams.get('rewindId');
49
+ const filteredMessages = rewindId
50
+ ? storedMessages.messages.slice(0,
51
  storedMessages.messages.findIndex(m => m.id === rewindId) + 1)
52
  : storedMessages.messages;
53
+
54
  setInitialMessages(filteredMessages);
55
  setUrlId(storedMessages.urlId);
56
  description.set(storedMessages.description);
 
100
 
101
  await setMessages(db, chatId.get() as string, messages, urlId, description.get());
102
  },
103
+ duplicateCurrentChat: async (listItemId:string) => {
104
+ if (!db || (!mixedId && !listItemId)) {
105
+ return;
106
+ }
107
+
108
+ try {
109
+ const newId = await duplicateChat(db, mixedId || listItemId);
110
+ navigate(`/chat/${newId}`);
111
+ toast.success('Chat duplicated successfully');
112
+ } catch (error) {
113
+ toast.error('Failed to duplicate chat');
114
+ }
115
+ }
116
  };
117
  }
118