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: +

+
+
    +
  1. + Go to Cloudflare Pages dashboard → Your project → Settings → Environment variables +
  2. +
  3. + Add both of these secrets (Production environment): +
      +
    • + GITHUB_ACCESS_TOKEN{' '} + (server-side API calls) +
    • +
    • + VITE_GITHUB_ACCESS_TOKEN{' '} + (client-side access) +
    • +
    +
  4. +
  5. + Add VITE_GITHUB_TOKEN_TYPE if + using fine-grained tokens +
  6. +
  7. Deploy a fresh build after adding these variables
  8. +
+
+ +
}> 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 && ( +
+ + +
+ )} +
{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< - - + + ));