feat: improved sidebar options

added the export and rename options to each history item
This commit is contained in:
Dustin Loring 2025-01-15 10:28:20 -05:00 committed by GitHub
commit 52a6108791
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 114 additions and 21 deletions

View File

@ -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<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
let timeout: NodeJS.Timeout | undefined;
let timeout: ReturnType<typeof setTimeout> | undefined;
function mouseEnter() {
setHovering(true);
if (timeout) {
clearTimeout(timeout);
}
@ -35,21 +40,67 @@ 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 (
<div
ref={hoverRef}
className="group rounded-md text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-background-depth-3 overflow-hidden flex justify-between items-center px-2 py-1"
>
{isEditing ? (
<input
ref={inputRef}
type="text"
value={editedDescription}
onChange={(e: ChangeEvent<HTMLInputElement>) => setEditedDescription(e.target.value)}
onBlur={handleRename}
onKeyDown={handleKeyDown}
className="flex-1 bg-transparent border border-bolt-elements-borderColor rounded px-2 py-1 text-bolt-elements-textPrimary focus:outline-none focus:border-bolt-elements-borderColorFocus"
/>
) : (
<a href={`/chat/${item.urlId}`} className="flex w-full relative truncate block">
{item.description}
<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%">
<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-24 group-hover:from-45%">
{hovering && (
<div className="flex items-center p-1 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger">
<div className="flex items-center gap-2 p-1">
<button
className="i-ph:pencil-simple scale-110 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
onClick={(event: MouseEvent) => {
event.preventDefault();
setIsEditing(true);
}}
/>
<button
className="i-ph:export scale-110 text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary"
onClick={(event: MouseEvent) => {
event.preventDefault();
onExport?.(item);
}}
/>
<Dialog.Trigger asChild>
<button
className="i-ph:trash scale-110"
onClick={(event) => {
// we prevent the default so we don't trigger the anchor above
className="i-ph:trash scale-110 text-bolt-elements-textSecondary hover:text-bolt-elements-item-contentDanger"
onClick={(event: MouseEvent) => {
event.preventDefault();
onDelete?.(event);
}}
@ -59,6 +110,7 @@ export function HistoryItem({ item, onDelete }: HistoryItemProps) {
)}
</div>
</a>
)}
</div>
);
}

View File

@ -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}
</div>
{items.map((item) => (
<HistoryItem key={item.id} item={item} onDelete={() => setDialogContent({ type: 'delete', item })} />
<HistoryItem
key={item.id}
item={item}
onDelete={() => setDialogContent({ type: 'delete', item })}
onRename={renameItem}
onExport={exportItem}
/>
))}
</div>
))}