diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
index d61b6fdc..74e60a04 100644
--- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx
+++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
@@ -149,6 +149,48 @@ export default function ConnectionsTab() {
+ {/* Cloudflare Deployment Note - Highly visible */}
+
+
+
+
Using Cloudflare Pages?
+
+
+ If you're experiencing GitHub connection issues (500 errors) on Cloudflare Pages deployments, you need to
+ configure environment variables in your Cloudflare dashboard:
+
+
+
+
+ Go to Cloudflare Pages dashboard → Your project → Settings → Environment variables
+
+
+ Add both of these secrets (Production environment):
+
+
+ GITHUB_ACCESS_TOKEN
{' '}
+ (server-side API calls)
+
+
+ VITE_GITHUB_ACCESS_TOKEN
{' '}
+ (client-side access)
+
+
+
+
+ Add VITE_GITHUB_TOKEN_TYPE
if
+ using fine-grained tokens
+
+ Deploy a fresh build after adding these variables
+
+
+
+
}>
diff --git a/app/components/sidebar/HistoryItem.tsx b/app/components/sidebar/HistoryItem.tsx
index a6f0d0a5..422faaa7 100644
--- a/app/components/sidebar/HistoryItem.tsx
+++ b/app/components/sidebar/HistoryItem.tsx
@@ -1,19 +1,30 @@
import { useParams } from '@remix-run/react';
import { classNames } from '~/utils/classNames';
-import * as Dialog from '@radix-ui/react-dialog';
import { type ChatHistoryItem } from '~/lib/persistence';
import WithTooltip from '~/components/ui/Tooltip';
import { useEditChatDescription } from '~/lib/hooks';
-import { forwardRef, type ForwardedRef } from 'react';
+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 }: HistoryItemProps) {
+export function HistoryItem({
+ item,
+ onDelete,
+ onDuplicate,
+ exportChat,
+ selectionMode = false,
+ isSelected = false,
+ onToggleSelection,
+}: HistoryItemProps) {
const { id: urlId } = useParams();
const isActiveChat = urlId === item.urlId;
@@ -24,13 +35,56 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
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
) => {
+ 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 (
+ {selectionMode && (
+
e.stopPropagation()}>
+
+
+ )}
+
{editing ? (
) : (
-
+
{currentDescription}
@@ -72,7 +129,10 @@ export function HistoryItem({ item, onDelete, onDuplicate, exportChat }: History
onDuplicate?.(item.id)}
+ onClick={(event) => {
+ event.preventDefault();
+ onDuplicate?.(item.id);
+ }}
/>
)}
-
- {
- event.preventDefault();
- onDelete?.(event);
- }}
- />
-
+
diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx
index 59c1fc22..953dfbdd 100644
--- a/app/components/sidebar/Menu.client.tsx
+++ b/app/components/sidebar/Menu.client.tsx
@@ -5,9 +5,9 @@ import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
import { ControlPanel } from '~/components/@settings/core/ControlPanel';
import { SettingsButton } from '~/components/ui/SettingsButton';
+import { Button } from '~/components/ui/Button';
import { db, deleteById, getAll, chatId, type ChatHistoryItem, useChatHistory } from '~/lib/persistence';
import { cubicEasingFn } from '~/utils/easings';
-import { logger } from '~/utils/logger';
import { HistoryItem } from './HistoryItem';
import { binDates } from './date-binning';
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
@@ -36,7 +36,10 @@ const menuVariants = {
},
} satisfies Variants;
-type DialogContent = { type: 'delete'; item: ChatHistoryItem } | null;
+type DialogContent =
+ | { type: 'delete'; item: ChatHistoryItem }
+ | { type: 'bulkDelete'; items: ChatHistoryItem[] }
+ | null;
function CurrentDateTime() {
const [dateTime, setDateTime] = useState(new Date());
@@ -51,7 +54,7 @@ function CurrentDateTime() {
return (
-
+
{dateTime.toLocaleDateString()}
{dateTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
@@ -68,6 +71,8 @@ export const Menu = () => {
const [dialogContent, setDialogContent] = useState
(null);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const profile = useStore(profileStore);
+ const [selectionMode, setSelectionMode] = useState(false);
+ const [selectedItems, setSelectedItems] = useState([]);
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
items: list,
@@ -83,35 +88,195 @@ export const Menu = () => {
}
}, []);
- const deleteItem = useCallback((event: React.UIEvent, item: ChatHistoryItem) => {
- event.preventDefault();
+ const deleteChat = useCallback(
+ async (id: string): Promise => {
+ if (!db) {
+ throw new Error('Database not available');
+ }
- if (db) {
- deleteById(db, item.id)
+ // Delete chat snapshot from localStorage
+ try {
+ const snapshotKey = `snapshot:${id}`;
+ localStorage.removeItem(snapshotKey);
+ console.log('Removed snapshot for chat:', id);
+ } catch (snapshotError) {
+ console.error(`Error deleting snapshot for chat ${id}:`, snapshotError);
+ }
+
+ // Delete the chat from the database
+ await deleteById(db, id);
+ console.log('Successfully deleted chat:', id);
+ },
+ [db],
+ );
+
+ const deleteItem = useCallback(
+ (event: React.UIEvent, item: ChatHistoryItem) => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ // Log the delete operation to help debugging
+ console.log('Attempting to delete chat:', { id: item.id, description: item.description });
+
+ deleteChat(item.id)
.then(() => {
+ toast.success('Chat deleted successfully', {
+ position: 'bottom-right',
+ autoClose: 3000,
+ });
+
+ // Always refresh the list
loadEntries();
if (chatId.get() === item.id) {
// hard page navigation to clear the stores
+ console.log('Navigating away from deleted chat');
window.location.pathname = '/';
}
})
.catch((error) => {
- toast.error('Failed to delete conversation');
- logger.error(error);
+ console.error('Failed to delete chat:', error);
+ toast.error('Failed to delete conversation', {
+ position: 'bottom-right',
+ autoClose: 3000,
+ });
+
+ // Still try to reload entries in case data has changed
+ loadEntries();
});
- }
- }, []);
+ },
+ [loadEntries, deleteChat],
+ );
+
+ const deleteSelectedItems = useCallback(
+ async (itemsToDeleteIds: string[]) => {
+ if (!db || itemsToDeleteIds.length === 0) {
+ console.log('Bulk delete skipped: No DB or no items to delete.');
+ return;
+ }
+
+ console.log(`Starting bulk delete for ${itemsToDeleteIds.length} chats`, itemsToDeleteIds);
+
+ let deletedCount = 0;
+ const errors: string[] = [];
+ const currentChatId = chatId.get();
+ let shouldNavigate = false;
+
+ // Process deletions sequentially using the shared deleteChat logic
+ for (const id of itemsToDeleteIds) {
+ try {
+ await deleteChat(id);
+ deletedCount++;
+
+ if (id === currentChatId) {
+ shouldNavigate = true;
+ }
+ } catch (error) {
+ console.error(`Error deleting chat ${id}:`, error);
+ errors.push(id);
+ }
+ }
+
+ // Show appropriate toast message
+ if (errors.length === 0) {
+ toast.success(`${deletedCount} chat${deletedCount === 1 ? '' : 's'} deleted successfully`);
+ } else {
+ toast.warning(`Deleted ${deletedCount} of ${itemsToDeleteIds.length} chats. ${errors.length} failed.`, {
+ autoClose: 5000,
+ });
+ }
+
+ // Reload the list after all deletions
+ await loadEntries();
+
+ // Clear selection state
+ setSelectedItems([]);
+ setSelectionMode(false);
+
+ // Navigate if needed
+ if (shouldNavigate) {
+ console.log('Navigating away from deleted chat');
+ window.location.pathname = '/';
+ }
+ },
+ [deleteChat, loadEntries, db],
+ );
const closeDialog = () => {
setDialogContent(null);
};
+ const toggleSelectionMode = () => {
+ setSelectionMode(!selectionMode);
+
+ if (selectionMode) {
+ // If turning selection mode OFF, clear selection
+ setSelectedItems([]);
+ }
+ };
+
+ const toggleItemSelection = useCallback((id: string) => {
+ setSelectedItems((prev) => {
+ const newSelectedItems = prev.includes(id) ? prev.filter((itemId) => itemId !== id) : [...prev, id];
+ console.log('Selected items updated:', newSelectedItems);
+
+ return newSelectedItems; // Return the new array
+ });
+ }, []); // No dependencies needed
+
+ const handleBulkDeleteClick = useCallback(() => {
+ if (selectedItems.length === 0) {
+ toast.info('Select at least one chat to delete');
+ return;
+ }
+
+ const selectedChats = list.filter((item) => selectedItems.includes(item.id));
+
+ if (selectedChats.length === 0) {
+ toast.error('Could not find selected chats');
+ return;
+ }
+
+ setDialogContent({ type: 'bulkDelete', items: selectedChats });
+ }, [selectedItems, list]); // Keep list dependency
+
+ const selectAll = useCallback(() => {
+ const allFilteredIds = filteredList.map((item) => item.id);
+ setSelectedItems((prev) => {
+ const allFilteredAreSelected = allFilteredIds.length > 0 && allFilteredIds.every((id) => prev.includes(id));
+
+ if (allFilteredAreSelected) {
+ // Deselect only the filtered items
+ const newSelectedItems = prev.filter((id) => !allFilteredIds.includes(id));
+ console.log('Deselecting all filtered items. New selection:', newSelectedItems);
+
+ return newSelectedItems;
+ } else {
+ // Select all filtered items, adding them to any existing selections
+ const newSelectedItems = [...new Set([...prev, ...allFilteredIds])];
+ console.log('Selecting all filtered items. New selection:', newSelectedItems);
+
+ return newSelectedItems;
+ }
+ });
+ }, [filteredList]); // Depends only on filteredList
+
useEffect(() => {
if (open) {
loadEntries();
}
- }, [open]);
+ }, [open, loadEntries]);
+
+ // Exit selection mode when sidebar is closed
+ useEffect(() => {
+ if (!open && selectionMode) {
+ /*
+ * Don't clear selection state anymore when sidebar closes
+ * This allows the selection to persist when reopening the sidebar
+ */
+ console.log('Sidebar closed, preserving selection state');
+ }
+ }, [open, selectionMode]);
useEffect(() => {
const enterThreshold = 40;
@@ -138,11 +303,6 @@ export const Menu = () => {
};
}, [isSettingsOpen]);
- const handleDeleteClick = (event: React.UIEvent, item: ChatHistoryItem) => {
- event.preventDefault();
- setDialogContent({ type: 'delete', item });
- };
-
const handleDuplicate = async (id: string) => {
await duplicateCurrentChat(id);
loadEntries(); // Reload the list after duplication
@@ -157,6 +317,11 @@ export const Menu = () => {
setIsSettingsOpen(false);
};
+ const setDialogContentWithLogging = useCallback((content: DialogContent) => {
+ console.log('Setting dialog content:', content);
+ setDialogContent(content);
+ }, []);
+
return (
<>
{
-
Your Chats
+
+
Your Chats
+ {selectionMode && (
+
+
+ {selectedItems.length === filteredList.length ? 'Deselect all' : 'Select all'}
+
+
+ Delete selected
+
+
+ )}
+
{filteredList.length === 0 && (
@@ -235,8 +431,16 @@ export const Menu = () => {
key={item.id}
item={item}
exportChat={exportChat}
- onDelete={(event) => handleDeleteClick(event, item)}
+ onDelete={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ console.log('Delete triggered for item:', item);
+ setDialogContentWithLogging({ type: 'delete', item });
+ }}
onDuplicate={() => handleDuplicate(item.id)}
+ selectionMode={selectionMode}
+ isSelected={selectedItems.includes(item.id)}
+ onToggleSelection={toggleItemSelection}
/>
))}
@@ -264,6 +468,7 @@ export const Menu = () => {
{
+ console.log('Dialog delete button clicked for item:', dialogContent.item);
deleteItem(event, dialogContent.item);
closeDialog();
}}
@@ -273,6 +478,49 @@ export const Menu = () => {
>
)}
+ {dialogContent?.type === 'bulkDelete' && (
+ <>
+
+
Delete Selected Chats?
+
+
+ You are about to delete {dialogContent.items.length}{' '}
+ {dialogContent.items.length === 1 ? 'chat' : 'chats'}:
+
+
+
+ {dialogContent.items.map((item) => (
+
+ {item.description}
+
+ ))}
+
+
+ Are you sure you want to delete these chats?
+
+
+
+
+ Cancel
+
+ {
+ /*
+ * Pass the current selectedItems to the delete function.
+ * This captures the state at the moment the user confirms.
+ */
+ const itemsToDeleteNow = [...selectedItems];
+ console.log('Bulk delete confirmed for', itemsToDeleteNow.length, 'items', itemsToDeleteNow);
+ deleteSelectedItems(itemsToDeleteNow);
+ closeDialog();
+ }}
+ >
+ Delete
+
+
+ >
+ )}
diff --git a/app/components/ui/Checkbox.tsx b/app/components/ui/Checkbox.tsx
index 7fa0d9fe..e21e9e25 100644
--- a/app/components/ui/Checkbox.tsx
+++ b/app/components/ui/Checkbox.tsx
@@ -10,13 +10,20 @@ const Checkbox = React.forwardRef<
-
-
+
+
));