From 9f1558f0fd2f37a55b603dc9b17699e61532e6fc Mon Sep 17 00:00:00 2001 From: Dustin Loring Date: Wed, 15 Jan 2025 10:27:36 -0500 Subject: [PATCH] feat: improved sidebar options added the export and rename options to each history item --- app/components/sidebar/HistoryItem.tsx | 90 ++++++++++++++++++++------ app/components/sidebar/Menu.client.tsx | 45 ++++++++++++- 2 files changed, 114 insertions(+), 21 deletions(-) diff --git a/app/components/sidebar/HistoryItem.tsx b/app/components/sidebar/HistoryItem.tsx index 8022e4d..60e642a 100644 --- a/app/components/sidebar/HistoryItem.tsx +++ b/app/components/sidebar/HistoryItem.tsx @@ -1,22 +1,27 @@ import * as Dialog from '@radix-ui/react-dialog'; +import type { ChangeEvent, KeyboardEvent, MouseEvent } from 'react'; import { useEffect, useRef, useState } from 'react'; import { type ChatHistoryItem } from '~/lib/persistence'; interface HistoryItemProps { item: ChatHistoryItem; - onDelete?: (event: React.UIEvent) => void; + onDelete?: (event: MouseEvent) => void; + onRename?: (id: string, newDescription: string) => void; + onExport?: (item: ChatHistoryItem) => void; } -export function HistoryItem({ item, onDelete }: HistoryItemProps) { +export function HistoryItem({ item, onDelete, onRename, onExport }: HistoryItemProps) { const [hovering, setHovering] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const [editedDescription, setEditedDescription] = useState(item.description || ''); const hoverRef = useRef(null); + const inputRef = useRef(null); useEffect(() => { - let timeout: NodeJS.Timeout | undefined; + let timeout: ReturnType | undefined; function mouseEnter() { setHovering(true); - if (timeout) { clearTimeout(timeout); } @@ -35,30 +40,77 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) { }; }, []); + useEffect(() => { + if (isEditing && inputRef.current) { + inputRef.current.focus(); + } + }, [isEditing]); + + const handleRename = () => { + if (editedDescription.trim() && onRename) { + onRename(item.id, editedDescription.trim()); + setIsEditing(false); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Enter') { + handleRename(); + } else if (e.key === 'Escape') { + setIsEditing(false); + setEditedDescription(item.description || ''); + } + }; + return (
- - {item.description} -
- {hovering && ( - + )} +
+ + )}
); } diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index e99d5bb..eb4826c 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -1,9 +1,10 @@ import { motion, type Variants } from 'framer-motion'; import { useCallback, useEffect, useRef, useState } from 'react'; import { toast } from 'react-toastify'; +import { saveAs } from 'file-saver'; import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog'; 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'; @@ -67,6 +68,40 @@ export function Menu() { } }, []); + const renameItem = useCallback((id: string, newDescription: string) => { + if (db) { + const item = list.find((item) => item.id === id); + if (item) { + setMessages(db, id, item.messages, item.urlId, newDescription) + .then(() => { + loadEntries(); + toast.success('Chat renamed successfully'); + }) + .catch((error) => { + toast.error('Failed to rename chat'); + logger.error(error); + }); + } + } + }, [list]); + + const exportItem = useCallback((item: ChatHistoryItem) => { + try { + const chatData = { + description: item.description, + messages: item.messages, + timestamp: item.timestamp, + }; + const blob = new Blob([JSON.stringify(chatData, null, 2)], { type: 'application/json' }); + const filename = `${item.description || 'chat'}-${new Date(item.timestamp).toISOString().split('T')[0]}.json`; + saveAs(blob, filename); + toast.success('Chat exported successfully'); + } catch (error) { + toast.error('Failed to export chat'); + logger.error(error); + } + }, []); + const closeDialog = () => { setDialogContent(null); }; @@ -127,7 +162,13 @@ export function Menu() { {category} {items.map((item) => ( - setDialogContent({ type: 'delete', item })} /> + setDialogContent({ type: 'delete', item })} + onRename={renameItem} + onExport={exportItem} + /> ))} ))}