bolt.diy/app/components/sidebar/HistoryItem.tsx
Stijnus b54d160a3b
feat: bulk delete chats from sidebar (#1586)
* feat: Bulk Delete Chats from Sidebar

feat(sidebar): Implement bulk chat deletion

Adds the ability for users to select multiple chats from the history sidebar and delete them in bulk.

**Key Changes:**

*   **Selection Mode:** Introduced a selection mode toggled by a dedicated button next to "Start new chat".
*   **Checkboxes:** Added checkboxes to each `HistoryItem` visible only when selection mode is active.
*   **Bulk Actions:** Added "Select All" / "Deselect All" and "Delete Selected" buttons (`Button` component with `ghost` variant) that appear above the chat list in selection mode.
*   **Confirmation Dialog:** Implemented a confirmation dialog (`Dialog` component) to prevent accidental deletion, listing the chats selected for removal.
*   **Deletion Logic:** Updated `Menu.client.tsx` to handle the selection state and perform bulk deletion using `deleteById` from persistence layer.
*   **Styling:** Ensured all new UI elements (`Checkbox`, `Button`) adhere to the existing project design system and support both light and dark themes using appropriate CSS classes and UnoCSS icons (`i-ph:` prefix).
*   **Refinement:** Replaced initial plain `<button>` elements with the project's `Button` component for consistency. Fixed incorrect icon prefixes.

* Fix selection and Dark mode
2025-04-06 20:36:53 +02:00

188 lines
6.1 KiB
TypeScript

import { useParams } from '@remix-run/react';
import { classNames } from '~/utils/classNames';
import { type ChatHistoryItem } from '~/lib/persistence';
import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
import { forwardRef, type ForwardedRef, useCallback } from 'react';
import { Checkbox } from '~/components/ui/Checkbox';
interface HistoryItemProps {
item: ChatHistoryItem;
onDelete?: (event: React.UIEvent) => void;
onDuplicate?: (id: string) => void;
exportChat: (id?: string) => void;
selectionMode?: boolean;
isSelected?: boolean;
onToggleSelection?: (id: string) => void;
}
export function HistoryItem({
item,
onDelete,
onDuplicate,
exportChat,
selectionMode = false,
isSelected = false,
onToggleSelection,
}: HistoryItemProps) {
const { id: urlId } = useParams();
const isActiveChat = urlId === item.urlId;
const { editing, handleChange, handleBlur, handleSubmit, handleKeyDown, currentDescription, toggleEditMode } =
useEditChatDescription({
initialDescription: item.description,
customChatId: item.id,
syncWithGlobalStore: isActiveChat,
});
const handleItemClick = useCallback(
(e: React.MouseEvent) => {
if (selectionMode) {
e.preventDefault();
e.stopPropagation();
console.log('Item clicked in selection mode:', item.id);
onToggleSelection?.(item.id);
}
},
[selectionMode, item.id, onToggleSelection],
);
const handleCheckboxChange = useCallback(() => {
console.log('Checkbox changed for item:', item.id);
onToggleSelection?.(item.id);
}, [item.id, onToggleSelection]);
const handleDeleteClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
event.stopPropagation();
console.log('Delete button clicked for item:', item.id);
if (onDelete) {
onDelete(event as unknown as React.UIEvent);
}
},
[onDelete, item.id],
);
return (
<div
className={classNames(
'group rounded-lg text-sm text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white hover:bg-gray-50/80 dark:hover:bg-gray-800/30 overflow-hidden flex justify-between items-center px-3 py-2 transition-colors',
{ 'text-gray-900 dark:text-white bg-gray-50/80 dark:bg-gray-800/30': isActiveChat },
{ 'cursor-pointer': selectionMode },
)}
onClick={selectionMode ? handleItemClick : undefined}
>
{selectionMode && (
<div className="flex items-center mr-2" onClick={(e) => e.stopPropagation()}>
<Checkbox
id={`select-${item.id}`}
checked={isSelected}
onCheckedChange={handleCheckboxChange}
className="h-4 w-4"
/>
</div>
)}
{editing ? (
<form onSubmit={handleSubmit} className="flex-1 flex items-center gap-2">
<input
type="text"
className="flex-1 bg-white dark:bg-gray-900 text-gray-900 dark:text-white rounded-md px-3 py-1.5 text-sm border border-gray-200 dark:border-gray-800 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
autoFocus
value={currentDescription}
onChange={handleChange}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
<button
type="submit"
className="i-ph:check h-4 w-4 text-gray-500 hover:text-purple-500 transition-colors"
onMouseDown={handleSubmit}
/>
</form>
) : (
<a
href={`/chat/${item.urlId}`}
className="flex w-full relative truncate block"
onClick={selectionMode ? handleItemClick : undefined}
>
<WithTooltip tooltip={currentDescription}>
<span className="truncate pr-24">{currentDescription}</span>
</WithTooltip>
<div
className={classNames(
'absolute right-0 top-0 bottom-0 flex items-center bg-transparent px-2 transition-colors',
)}
>
<div className="flex items-center gap-2.5 text-gray-400 dark:text-gray-500 opacity-0 group-hover:opacity-100 transition-opacity">
<ChatActionButton
toolTipContent="Export"
icon="i-ph:download-simple h-4 w-4"
onClick={(event) => {
event.preventDefault();
exportChat(item.id);
}}
/>
{onDuplicate && (
<ChatActionButton
toolTipContent="Duplicate"
icon="i-ph:copy h-4 w-4"
onClick={(event) => {
event.preventDefault();
onDuplicate?.(item.id);
}}
/>
)}
<ChatActionButton
toolTipContent="Rename"
icon="i-ph:pencil-fill h-4 w-4"
onClick={(event) => {
event.preventDefault();
toggleEditMode();
}}
/>
<ChatActionButton
toolTipContent="Delete"
icon="i-ph:trash h-4 w-4"
className="hover:text-red-500 dark:hover:text-red-400"
onClick={handleDeleteClick}
/>
</div>
</div>
</a>
)}
</div>
);
}
const ChatActionButton = forwardRef(
(
{
toolTipContent,
icon,
className,
onClick,
}: {
toolTipContent: string;
icon: string;
className?: string;
onClick: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
btnTitle?: string;
},
ref: ForwardedRef<HTMLButtonElement>,
) => {
return (
<WithTooltip tooltip={toolTipContent} position="bottom" sideOffset={4}>
<button
ref={ref}
type="button"
className={`text-gray-400 dark:text-gray-500 hover:text-purple-500 dark:hover:text-purple-400 transition-colors ${icon} ${className ? className : ''}`}
onClick={onClick}
/>
</WithTooltip>
);
},
);