|
|
|
@@ -1,6 +1,7 @@
|
|
|
|
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router';
|
|
|
|
|
import { Plus, MessageSquare, Folder, ChevronRight, FolderPlus } from 'lucide-react';
|
|
|
|
|
import { Plus, MessageSquare, Folder, ChevronRight, FolderPlus, CheckSquare, Trash2, FolderInput } from 'lucide-react';
|
|
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import { usePanelChannel } from 'hooks/usePanelChannel';
|
|
|
|
|
import { useChatSessions } from 'state/useChatSessions';
|
|
|
|
|
import { useChatGroups } from 'state/useChatGroups';
|
|
|
|
@@ -20,12 +21,20 @@ export const SessionList = () => {
|
|
|
|
|
: dashboardId && !dashboardId.startsWith('screens/')
|
|
|
|
|
? { context: 'dashboard' as const, contextId: dashboardId }
|
|
|
|
|
: undefined;
|
|
|
|
|
const { sessions, deleteSession } = useChatSessions(contextFilter);
|
|
|
|
|
const { groups } = useChatGroups();
|
|
|
|
|
const { sessions, deleteSession, deleteSessions } = useChatSessions(contextFilter);
|
|
|
|
|
const { groups, moveSession, createGroup } = useChatGroups();
|
|
|
|
|
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
|
|
|
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
|
|
|
|
const [showCreateGroup, setShowCreateGroup] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Selection mode state
|
|
|
|
|
const [selectionMode, setSelectionMode] = useState(false);
|
|
|
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
|
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
|
|
|
const [showMoveMenu, setShowMoveMenu] = useState(false);
|
|
|
|
|
const [showCreateGroupForMove, setShowCreateGroupForMove] = useState(false);
|
|
|
|
|
const [isBulkDeleting, setIsBulkDeleting] = useState(false);
|
|
|
|
|
|
|
|
|
|
const scrolledRef = useRef(false);
|
|
|
|
|
const selectedRef = useCallback(
|
|
|
|
|
(node: HTMLDivElement | null) => {
|
|
|
|
@@ -41,18 +50,99 @@ export const SessionList = () => {
|
|
|
|
|
scrolledRef.current = false;
|
|
|
|
|
}, [selected?.id]);
|
|
|
|
|
|
|
|
|
|
// Exit selection mode on Escape
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!selectionMode) return;
|
|
|
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
|
|
|
if (e.key === 'Escape') {
|
|
|
|
|
setSelectionMode(false);
|
|
|
|
|
setSelectedIds(new Set());
|
|
|
|
|
setShowMoveMenu(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
|
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
|
|
|
}, [selectionMode]);
|
|
|
|
|
|
|
|
|
|
const exitSelectionMode = () => {
|
|
|
|
|
setSelectionMode(false);
|
|
|
|
|
setSelectedIds(new Set());
|
|
|
|
|
setShowMoveMenu(false);
|
|
|
|
|
setShowDeleteConfirm(false);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleSelection = (id: string) => {
|
|
|
|
|
setSelectedIds((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
next.has(id) ? next.delete(id) : next.add(id);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const toggleGroup = (slug: string) => {
|
|
|
|
|
setCollapsed(prev => {
|
|
|
|
|
setCollapsed((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
next.has(slug) ? next.delete(slug) : next.add(slug);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ungrouped = sessions.filter(s => !s.groupSlug);
|
|
|
|
|
const grouped = groups.map(g => ({
|
|
|
|
|
const toggleGroupSelection = (groupSessions: typeof sessions) => {
|
|
|
|
|
const ids = groupSessions.map((s) => s.id);
|
|
|
|
|
const allSelected = ids.every((id) => selectedIds.has(id));
|
|
|
|
|
setSelectedIds((prev) => {
|
|
|
|
|
const next = new Set(prev);
|
|
|
|
|
if (allSelected) {
|
|
|
|
|
ids.forEach((id) => next.delete(id));
|
|
|
|
|
} else {
|
|
|
|
|
ids.forEach((id) => next.add(id));
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const selectAll = () => {
|
|
|
|
|
setSelectedIds(new Set(sessions.map((s) => s.id)));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deselectAll = () => {
|
|
|
|
|
setSelectedIds(new Set());
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBulkDelete = async () => {
|
|
|
|
|
setIsBulkDeleting(true);
|
|
|
|
|
try {
|
|
|
|
|
const ids = [...selectedIds];
|
|
|
|
|
// Clear selected panel if deleting the active session
|
|
|
|
|
if (selected && selectedIds.has(selected.id)) {
|
|
|
|
|
setSelected(null);
|
|
|
|
|
navigate('/chat', { replace: true });
|
|
|
|
|
}
|
|
|
|
|
await deleteSessions(ids);
|
|
|
|
|
exitSelectionMode();
|
|
|
|
|
} finally {
|
|
|
|
|
setIsBulkDeleting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleBulkMove = async (groupSlug: string | null) => {
|
|
|
|
|
const ids = [...selectedIds];
|
|
|
|
|
await Promise.all(ids.map((id) => moveSession(id, groupSlug)));
|
|
|
|
|
setShowMoveMenu(false);
|
|
|
|
|
exitSelectionMode();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateGroupAndMove = async (name: string, slug: string, description?: string) => {
|
|
|
|
|
const ids = [...selectedIds];
|
|
|
|
|
await createGroup(name, slug, description, ids);
|
|
|
|
|
setShowCreateGroupForMove(false);
|
|
|
|
|
exitSelectionMode();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const ungrouped = sessions.filter((s) => !s.groupSlug);
|
|
|
|
|
const grouped = groups.map((g) => ({
|
|
|
|
|
...g,
|
|
|
|
|
sessions: sessions.filter(s => s.groupSlug === g.slug)
|
|
|
|
|
sessions: sessions.filter((s) => s.groupSlug === g.slug),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const handleSelect = (session: (typeof sessions)[number]) => {
|
|
|
|
@@ -69,19 +159,31 @@ export const SessionList = () => {
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderSession = (session: (typeof sessions)[number]) => {
|
|
|
|
|
const isSelected = selected?.id === session.id;
|
|
|
|
|
const isActive = selected?.id === session.id;
|
|
|
|
|
const isChecked = selectedIds.has(session.id);
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={session.id}
|
|
|
|
|
ref={isSelected ? selectedRef : undefined}
|
|
|
|
|
ref={isActive ? selectedRef : undefined}
|
|
|
|
|
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
|
|
|
|
|
isSelected
|
|
|
|
|
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
|
|
|
|
|
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
|
|
|
|
|
selectionMode && isChecked
|
|
|
|
|
? 'border-duck-teal/40 bg-duck-teal/10 dark:bg-duck-teal/15'
|
|
|
|
|
: isActive
|
|
|
|
|
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
|
|
|
|
|
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{selectionMode && (
|
|
|
|
|
<div className="shrink-0 pl-3" onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={isChecked}
|
|
|
|
|
onCheckedChange={() => toggleSelection(session.id)}
|
|
|
|
|
className="border-duck-dark/30 dark:border-foreground/30 data-[state=checked]:bg-duck-teal data-[state=checked]:border-duck-teal"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleSelect(session)}
|
|
|
|
|
onClick={() => (selectionMode ? toggleSelection(session.id) : handleSelect(session))}
|
|
|
|
|
className="flex-1 flex items-center gap-3 px-4 py-3 min-w-0 text-left cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
|
|
|
@@ -104,7 +206,7 @@ export const SessionList = () => {
|
|
|
|
|
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
|
|
|
|
|
{(() => {
|
|
|
|
|
if (!session.model.includes('/')) return session.model;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const [provider, modelId] = session.model.split('/') as [string, string];
|
|
|
|
|
|
|
|
|
|
// Handle local providers - show friendly name
|
|
|
|
@@ -112,7 +214,7 @@ export const SessionList = () => {
|
|
|
|
|
const friendlyName = getProviderDisplayName(provider);
|
|
|
|
|
return `${friendlyName} - ${modelId}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Regular providers - just replace / with -
|
|
|
|
|
return session.model.replace('/', ' - ');
|
|
|
|
|
})()}
|
|
|
|
@@ -120,9 +222,11 @@ export const SessionList = () => {
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
<div className="shrink-0 mr-2">
|
|
|
|
|
<SessionContextMenu session={session} onDelete={handleDelete} />
|
|
|
|
|
</div>
|
|
|
|
|
{!selectionMode && (
|
|
|
|
|
<div className="shrink-0 mr-2">
|
|
|
|
|
<SessionContextMenu session={session} onDelete={handleDelete} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
@@ -133,23 +237,51 @@ export const SessionList = () => {
|
|
|
|
|
<div className="shrink-0 flex items-center justify-between px-4 py-2 border-b border-duck-dark/10 dark:border-foreground/10 bg-background/60">
|
|
|
|
|
<h2 className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Sessions</h2>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCreateGroup(true)}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<FolderPlus className="h-3.5 w-3.5" />
|
|
|
|
|
New Group
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelected({ id: `new:${Date.now()}` });
|
|
|
|
|
navigate('/chat/new', { replace: true });
|
|
|
|
|
}}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5" />
|
|
|
|
|
New Chat
|
|
|
|
|
</button>
|
|
|
|
|
{selectionMode ? (
|
|
|
|
|
<>
|
|
|
|
|
<button
|
|
|
|
|
onClick={selectedIds.size === sessions.length ? deselectAll : selectAll}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
{selectedIds.size === sessions.length ? 'Deselect All' : 'Select All'}
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={exitSelectionMode}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
{sessions.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setSelectionMode(true)}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<CheckSquare className="h-3.5 w-3.5" />
|
|
|
|
|
Select
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowCreateGroup(true)}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<FolderPlus className="h-3.5 w-3.5" />
|
|
|
|
|
New Group
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelected({ id: `new:${Date.now()}` });
|
|
|
|
|
navigate('/chat/new', { replace: true });
|
|
|
|
|
}}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-teal hover:bg-duck-teal/90 text-duck-yellow cursor-pointer h-7 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="h-3.5 w-3.5" />
|
|
|
|
|
New Chat
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -164,6 +296,14 @@ export const SessionList = () => {
|
|
|
|
|
{/* Ungrouped sessions */}
|
|
|
|
|
{ungrouped.length > 0 && (
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
{selectionMode && ungrouped.length > 1 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => toggleGroupSelection(ungrouped)}
|
|
|
|
|
className="text-xs text-duck-dark/50 dark:text-foreground/50 hover:text-duck-dark/70 dark:hover:text-foreground/70 px-3 py-1"
|
|
|
|
|
>
|
|
|
|
|
{ungrouped.every((s) => selectedIds.has(s.id)) ? 'Deselect ungrouped' : 'Select ungrouped'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{ungrouped.map(renderSession)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
@@ -172,6 +312,15 @@ export const SessionList = () => {
|
|
|
|
|
{grouped.map((group) => (
|
|
|
|
|
<div key={group.slug} className="space-y-1.5">
|
|
|
|
|
<div className="flex items-center gap-2 px-3 py-2 cursor-pointer rounded-md hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors">
|
|
|
|
|
{selectionMode && group.sessions.length > 0 && (
|
|
|
|
|
<div onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={group.sessions.length > 0 && group.sessions.every((s) => selectedIds.has(s.id))}
|
|
|
|
|
onCheckedChange={() => toggleGroupSelection(group.sessions)}
|
|
|
|
|
className="border-duck-dark/30 dark:border-foreground/30 data-[state=checked]:bg-duck-teal data-[state=checked]:border-duck-teal"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div onClick={() => toggleGroup(group.slug)} className="flex items-center gap-2 flex-1 min-w-0">
|
|
|
|
|
<ChevronRight
|
|
|
|
|
className={`h-4 w-4 transition-transform ${!collapsed.has(group.slug) ? 'rotate-90' : ''}`}
|
|
|
|
@@ -180,7 +329,7 @@ export const SessionList = () => {
|
|
|
|
|
<span className="font-medium text-sm text-duck-dark/70 dark:text-foreground/70">{group.name}</span>
|
|
|
|
|
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">({group.sessionCount})</span>
|
|
|
|
|
</div>
|
|
|
|
|
<GroupContextMenu group={group} />
|
|
|
|
|
{!selectionMode && <GroupContextMenu group={group} />}
|
|
|
|
|
</div>
|
|
|
|
|
{!collapsed.has(group.slug) && (
|
|
|
|
|
<div className="ml-6 space-y-1.5">
|
|
|
|
@@ -191,7 +340,106 @@ export const SessionList = () => {
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Floating action bar */}
|
|
|
|
|
{selectionMode && selectedIds.size > 0 && (
|
|
|
|
|
<div className="shrink-0 flex items-center justify-between px-4 py-3 border-t border-duck-dark/10 dark:border-foreground/10 bg-background/95 backdrop-blur-sm">
|
|
|
|
|
<span className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">
|
|
|
|
|
{selectedIds.size} selected
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex gap-2 relative">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowDeleteConfirm(true)}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-red-500/10 hover:bg-red-500/20 text-red-600 dark:text-red-400 cursor-pointer h-8 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowMoveMenu(!showMoveMenu)}
|
|
|
|
|
className="flex items-center gap-1.5 rounded-md bg-duck-dark/10 hover:bg-duck-dark/20 dark:bg-foreground/10 dark:hover:bg-foreground/20 text-duck-dark dark:text-foreground cursor-pointer h-8 px-3 text-xs font-medium"
|
|
|
|
|
>
|
|
|
|
|
<FolderInput className="h-3.5 w-3.5" />
|
|
|
|
|
Move to Group
|
|
|
|
|
</button>
|
|
|
|
|
{showMoveMenu && (
|
|
|
|
|
<>
|
|
|
|
|
<div className="fixed inset-0 z-40" onClick={() => setShowMoveMenu(false)} />
|
|
|
|
|
<div className="absolute right-0 bottom-full mb-1 z-50 min-w-[180px] bg-background border border-duck-dark/10 dark:border-foreground/10 rounded-md shadow-lg overflow-hidden">
|
|
|
|
|
<div className="px-3 py-2 text-xs font-medium text-duck-dark/50 dark:text-foreground/50 border-b border-duck-dark/10 dark:border-foreground/10">
|
|
|
|
|
Move to:
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleBulkMove(null)}
|
|
|
|
|
className="w-full px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
Ungrouped
|
|
|
|
|
</button>
|
|
|
|
|
{groups.map((group) => (
|
|
|
|
|
<button
|
|
|
|
|
key={group.slug}
|
|
|
|
|
onClick={() => handleBulkMove(group.slug)}
|
|
|
|
|
className="w-full px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left"
|
|
|
|
|
>
|
|
|
|
|
{group.name}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
<div className="border-t border-duck-dark/10 dark:border-foreground/10">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setShowMoveMenu(false);
|
|
|
|
|
setShowCreateGroupForMove(true);
|
|
|
|
|
}}
|
|
|
|
|
className="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-duck-dark/5 dark:hover:bg-foreground/5 transition-colors text-left text-duck-teal"
|
|
|
|
|
>
|
|
|
|
|
<FolderPlus className="h-3.5 w-3.5" />
|
|
|
|
|
New Group
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Delete confirmation dialog */}
|
|
|
|
|
{showDeleteConfirm && (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) setShowDeleteConfirm(false);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div className="bg-background border border-duck-dark/10 dark:border-foreground/10 rounded-lg shadow-xl w-full max-w-sm mx-4 p-6">
|
|
|
|
|
<h3 className="text-lg font-semibold text-duck-dark dark:text-foreground mb-2">Delete Sessions</h3>
|
|
|
|
|
<p className="text-sm text-duck-dark/60 dark:text-foreground/60 mb-6">
|
|
|
|
|
Are you sure you want to delete {selectedIds.size} session{selectedIds.size > 1 ? 's' : ''}? This action
|
|
|
|
|
cannot be undone.
|
|
|
|
|
</p>
|
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowDeleteConfirm(false)}
|
|
|
|
|
disabled={isBulkDeleting}
|
|
|
|
|
className="px-4 py-2 text-sm font-medium text-duck-dark/70 dark:text-foreground/70 hover:bg-duck-dark/10 dark:hover:bg-foreground/10 rounded-md transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleBulkDelete}
|
|
|
|
|
disabled={isBulkDeleting}
|
|
|
|
|
className="px-4 py-2 text-sm font-medium bg-red-500 hover:bg-red-600 text-white rounded-md transition-colors disabled:opacity-50"
|
|
|
|
|
>
|
|
|
|
|
{isBulkDeleting ? 'Deleting...' : 'Delete'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{showCreateGroup && <CreateGroupDialog onClose={() => setShowCreateGroup(false)} />}
|
|
|
|
|
{showCreateGroupForMove && (
|
|
|
|
|
<CreateGroupDialog onClose={() => setShowCreateGroupForMove(false)} onCreateWithSessions={handleCreateGroupAndMove} />
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|