diff --git a/app/components/sidebar/HistoryItem.tsx b/app/components/sidebar/HistoryItem.tsx index 8022e4d26e..5e68b70ebb 100644 --- a/app/components/sidebar/HistoryItem.tsx +++ b/app/components/sidebar/HistoryItem.tsx @@ -5,9 +5,11 @@ import { type ChatHistoryItem } from '~/lib/persistence'; interface HistoryItemProps { item: ChatHistoryItem; onDelete?: (event: React.UIEvent) => void; + onRename?: (event: React.UIEvent) => void; + onExport?: (event: React.UIEvent) => void; } -export function HistoryItem({ item, onDelete }: HistoryItemProps) { +export function HistoryItem({ item, onDelete, onRename, onExport }: HistoryItemProps) { const [hovering, setHovering] = useState(false); const hoverRef = useRef(null); @@ -16,7 +18,6 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) { function mouseEnter() { setHovering(true); - if (timeout) { clearTimeout(timeout); } @@ -42,17 +43,33 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) { > {item.description} -
+
{hovering && ( -
+
+
diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index cf6d97812c..fad513fbb9 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -4,7 +4,7 @@ import { toast } from 'react-toastify'; import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; import { IconButton } from '~/components/ui/IconButton'; import { ThemeSwitch } from '~/components/ui/ThemeSwitch'; -import { db, deleteById, getAll, chatId, type ChatHistoryItem } from '~/lib/persistence'; +import { db, deleteById, getAll, chatId, type ChatHistoryItem, setMessages } from '~/lib/persistence'; import { cubicEasingFn } from '~/utils/easings'; import { logger } from '~/utils/logger'; import { HistoryItem } from './HistoryItem'; @@ -31,13 +31,17 @@ const menuVariants = { }, } satisfies Variants; -type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null; +type DialogContent = + | { type: 'delete'; item: ChatHistoryItem } + | { type: 'rename'; item: ChatHistoryItem } + | null; export function Menu() { const menuRef = useRef(null); const [list, setList] = useState([]); const [open, setOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); + const [newName, setNewName] = useState(''); const loadEntries = useCallback(() => { if (db) { @@ -68,6 +72,43 @@ export function Menu() { } }, []); + const renameItem = useCallback(async (event: React.UIEvent, item: ChatHistoryItem, newDescription: string) => { + event.preventDefault(); + + if (db) { + try { + await setMessages(db, item.id, item.messages, item.urlId, newDescription); + loadEntries(); + toast.success('Chat renamed successfully'); + } catch (error) { + toast.error('Failed to rename chat'); + logger.error(error); + } + } + }, []); + + const exportItem = useCallback((event: React.UIEvent, item: ChatHistoryItem) => { + event.preventDefault(); + + const exportData = { + description: item.description, + messages: item.messages, + timestamp: item.timestamp + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `chat-${item.description || 'export'}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + toast.success('Chat exported successfully'); + }, []); + const closeDialog = () => { setDialogContent(null); }; @@ -102,24 +143,16 @@ export function Menu() { return ( -
{/* Placeholder */}
-
-
- - - Start new chat - +
+
+
History
-
Your Chats
-
+
{list.length === 0 &&
No previous conversations
} {binDates(list).map(({ category, items }) => ( @@ -128,7 +161,16 @@ export function Menu() { {category}
{items.map((item) => ( - setDialogContent({ type: 'delete', item })} /> + setDialogContent({ type: 'delete', item })} + onRename={() => { + setNewName(item.description || ''); + setDialogContent({ type: 'rename', item }); + }} + onExport={(event) => exportItem(event, item)} + /> ))}
))} @@ -160,12 +202,45 @@ export function Menu() {
)} + {dialogContent?.type === 'rename' && ( + <> + Rename Chat + +
+ setNewName(e.target.value)} + className="w-full p-2 mt-2 text-bolt-elements-textPrimary bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-md focus:outline-none focus:border-bolt-elements-borderColorFocus" + placeholder="Enter new name" + autoFocus + /> +
+
+
+ + Cancel + + { + if (newName.trim()) { + renameItem(event, dialogContent.item, newName.trim()); + closeDialog(); + } + }} + > + Rename + +
+ + )}
-
- -
+
+
+
); diff --git a/package-lock.json b/package-lock.json index c340e0b387..310bbdfdf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@codemirror/search": "^6.5.8", "@codemirror/state": "^6.5.1", "@codemirror/view": "^6.36.2", + "@iconify-json/ph": "^1.2.2", + "@iconify-json/svg-spinners": "^1.2.2", "@nanostores/react": "^0.8.4", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", @@ -3364,11 +3366,28 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify-json/ph": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/ph/-/ph-1.2.2.tgz", + "integrity": "sha512-PgkEZNtqa8hBGjHXQa4pMwZa93hmfu8FUSjs/nv4oUU6yLsgv+gh9nu28Kqi8Fz9CCVu4hj1MZs9/60J57IzFw==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify-json/svg-spinners": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@iconify-json/svg-spinners/-/svg-spinners-1.2.2.tgz", + "integrity": "sha512-DIErwfBWWzLfmAG2oQnbUOSqZhDxlXvr8941itMCrxQoMB0Hiv8Ww6Bln/zIgxwjDvSem2dKJtap+yKKwsB/2A==", + "license": "MIT", + "dependencies": { + "@iconify/types": "*" + } + }, "node_modules/@iconify/types": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", - "dev": true, "license": "MIT" }, "node_modules/@iconify/utils": {