group select chats
This commit is contained in:
@@ -262,13 +262,17 @@ piRestRouter.delete('/pi/sessions', async (ctx: Context) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await ctx.req.json().catch(() => ({}));
|
const body = await ctx.req.json().catch(() => ({}));
|
||||||
|
const sessionIds = Array.isArray(body.sessionIds) ? (body.sessionIds as string[]) : undefined;
|
||||||
const contextFilter = body.context
|
const contextFilter = body.context
|
||||||
? { context: body.context as string, contextId: body.contextId as string | undefined }
|
? { context: body.context as string, contextId: body.contextId as string | undefined }
|
||||||
: undefined;
|
: undefined;
|
||||||
const userHome = getHomeDir(user.email);
|
const userHome = getHomeDir(user.email);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessions = await storage.listUserSessions(userHome, contextFilter);
|
const allSessions = await storage.listUserSessions(userHome, contextFilter);
|
||||||
|
const sessions = sessionIds
|
||||||
|
? allSessions.filter((s) => sessionIds.includes(s.id))
|
||||||
|
: allSessions;
|
||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
try {
|
try {
|
||||||
@@ -283,6 +287,7 @@ piRestRouter.delete('/pi/sessions', async (ctx: Context) => {
|
|||||||
deleted,
|
deleted,
|
||||||
total: sessions.length,
|
total: sessions.length,
|
||||||
context: contextFilter?.context,
|
context: contextFilter?.context,
|
||||||
|
sessionIds: sessionIds?.length,
|
||||||
});
|
});
|
||||||
return ctx.json({ success: true, deleted });
|
return ctx.json({ success: true, deleted });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useChatGroups } from 'state/useChatGroups';
|
|||||||
|
|
||||||
type CreateGroupDialogProps = {
|
type CreateGroupDialogProps = {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onCreateWithSessions?: (name: string, slug: string, description?: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function toSlug(name: string): string {
|
function toSlug(name: string): string {
|
||||||
@@ -13,7 +14,7 @@ function toSlug(name: string): string {
|
|||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CreateGroupDialog({ onClose }: CreateGroupDialogProps) {
|
export function CreateGroupDialog({ onClose, onCreateWithSessions }: CreateGroupDialogProps) {
|
||||||
const { createGroup } = useChatGroups();
|
const { createGroup } = useChatGroups();
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
@@ -38,7 +39,11 @@ export function CreateGroupDialog({ onClose }: CreateGroupDialogProps) {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createGroup(name.trim(), slug, description.trim() || undefined);
|
if (onCreateWithSessions) {
|
||||||
|
await onCreateWithSessions(name.trim(), slug, description.trim() || undefined);
|
||||||
|
} else {
|
||||||
|
await createGroup(name.trim(), slug, description.trim() || undefined);
|
||||||
|
}
|
||||||
onClose();
|
onClose();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
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 { usePanelChannel } from 'hooks/usePanelChannel';
|
||||||
import { useChatSessions } from 'state/useChatSessions';
|
import { useChatSessions } from 'state/useChatSessions';
|
||||||
import { useChatGroups } from 'state/useChatGroups';
|
import { useChatGroups } from 'state/useChatGroups';
|
||||||
@@ -20,12 +21,20 @@ export const SessionList = () => {
|
|||||||
: dashboardId && !dashboardId.startsWith('screens/')
|
: dashboardId && !dashboardId.startsWith('screens/')
|
||||||
? { context: 'dashboard' as const, contextId: dashboardId }
|
? { context: 'dashboard' as const, contextId: dashboardId }
|
||||||
: undefined;
|
: undefined;
|
||||||
const { sessions, deleteSession } = useChatSessions(contextFilter);
|
const { sessions, deleteSession, deleteSessions } = useChatSessions(contextFilter);
|
||||||
const { groups } = useChatGroups();
|
const { groups, moveSession, createGroup } = useChatGroups();
|
||||||
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
const [selected, setSelected] = usePanelChannel<SelectedSession>('chat:selected-session', null);
|
||||||
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
|
||||||
const [showCreateGroup, setShowCreateGroup] = useState(false);
|
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 scrolledRef = useRef(false);
|
||||||
const selectedRef = useCallback(
|
const selectedRef = useCallback(
|
||||||
(node: HTMLDivElement | null) => {
|
(node: HTMLDivElement | null) => {
|
||||||
@@ -41,18 +50,99 @@ export const SessionList = () => {
|
|||||||
scrolledRef.current = false;
|
scrolledRef.current = false;
|
||||||
}, [selected?.id]);
|
}, [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) => {
|
const toggleGroup = (slug: string) => {
|
||||||
setCollapsed(prev => {
|
setCollapsed((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
next.has(slug) ? next.delete(slug) : next.add(slug);
|
next.has(slug) ? next.delete(slug) : next.add(slug);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const ungrouped = sessions.filter(s => !s.groupSlug);
|
const toggleGroupSelection = (groupSessions: typeof sessions) => {
|
||||||
const grouped = groups.map(g => ({
|
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,
|
...g,
|
||||||
sessions: sessions.filter(s => s.groupSlug === g.slug)
|
sessions: sessions.filter((s) => s.groupSlug === g.slug),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const handleSelect = (session: (typeof sessions)[number]) => {
|
const handleSelect = (session: (typeof sessions)[number]) => {
|
||||||
@@ -69,19 +159,31 @@ export const SessionList = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderSession = (session: (typeof sessions)[number]) => {
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={session.id}
|
key={session.id}
|
||||||
ref={isSelected ? selectedRef : undefined}
|
ref={isActive ? selectedRef : undefined}
|
||||||
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
|
className={`group flex items-center rounded-lg border transition-colors cursor-pointer ${
|
||||||
isSelected
|
selectionMode && isChecked
|
||||||
? 'border-duck-teal/30 bg-duck-teal/5 dark:bg-duck-teal/10'
|
? 'border-duck-teal/40 bg-duck-teal/10 dark:bg-duck-teal/15'
|
||||||
: 'border-duck-dark/10 dark:border-foreground/10 bg-background/80 hover:bg-background/90'
|
: 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
|
<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"
|
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" />
|
<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">
|
<div className="text-xs text-duck-teal/70 dark:text-duck-teal/60 truncate">
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!session.model.includes('/')) return session.model;
|
if (!session.model.includes('/')) return session.model;
|
||||||
|
|
||||||
const [provider, modelId] = session.model.split('/') as [string, string];
|
const [provider, modelId] = session.model.split('/') as [string, string];
|
||||||
|
|
||||||
// Handle local providers - show friendly name
|
// Handle local providers - show friendly name
|
||||||
@@ -112,7 +214,7 @@ export const SessionList = () => {
|
|||||||
const friendlyName = getProviderDisplayName(provider);
|
const friendlyName = getProviderDisplayName(provider);
|
||||||
return `${friendlyName} - ${modelId}`;
|
return `${friendlyName} - ${modelId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Regular providers - just replace / with -
|
// Regular providers - just replace / with -
|
||||||
return session.model.replace('/', ' - ');
|
return session.model.replace('/', ' - ');
|
||||||
})()}
|
})()}
|
||||||
@@ -120,9 +222,11 @@ export const SessionList = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div className="shrink-0 mr-2">
|
{!selectionMode && (
|
||||||
<SessionContextMenu session={session} onDelete={handleDelete} />
|
<div className="shrink-0 mr-2">
|
||||||
</div>
|
<SessionContextMenu session={session} onDelete={handleDelete} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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">
|
<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>
|
<h2 className="text-sm font-medium text-duck-dark/70 dark:text-foreground/70">Sessions</h2>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
{selectionMode ? (
|
||||||
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"
|
<button
|
||||||
>
|
onClick={selectedIds.size === sessions.length ? deselectAll : selectAll}
|
||||||
<FolderPlus className="h-3.5 w-3.5" />
|
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"
|
||||||
New Group
|
>
|
||||||
</button>
|
{selectedIds.size === sessions.length ? 'Deselect All' : 'Select All'}
|
||||||
<button
|
</button>
|
||||||
onClick={() => {
|
<button
|
||||||
setSelected({ id: `new:${Date.now()}` });
|
onClick={exitSelectionMode}
|
||||||
navigate('/chat/new', { replace: 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"
|
||||||
}}
|
>
|
||||||
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"
|
Cancel
|
||||||
>
|
</button>
|
||||||
<Plus className="h-3.5 w-3.5" />
|
</>
|
||||||
New Chat
|
) : (
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -164,6 +296,14 @@ export const SessionList = () => {
|
|||||||
{/* Ungrouped sessions */}
|
{/* Ungrouped sessions */}
|
||||||
{ungrouped.length > 0 && (
|
{ungrouped.length > 0 && (
|
||||||
<div className="space-y-1.5">
|
<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)}
|
{ungrouped.map(renderSession)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -172,6 +312,15 @@ export const SessionList = () => {
|
|||||||
{grouped.map((group) => (
|
{grouped.map((group) => (
|
||||||
<div key={group.slug} className="space-y-1.5">
|
<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">
|
<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">
|
<div onClick={() => toggleGroup(group.slug)} className="flex items-center gap-2 flex-1 min-w-0">
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
className={`h-4 w-4 transition-transform ${!collapsed.has(group.slug) ? 'rotate-90' : ''}`}
|
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="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>
|
<span className="text-xs text-duck-dark/40 dark:text-foreground/40">({group.sessionCount})</span>
|
||||||
</div>
|
</div>
|
||||||
<GroupContextMenu group={group} />
|
{!selectionMode && <GroupContextMenu group={group} />}
|
||||||
</div>
|
</div>
|
||||||
{!collapsed.has(group.slug) && (
|
{!collapsed.has(group.slug) && (
|
||||||
<div className="ml-6 space-y-1.5">
|
<div className="ml-6 space-y-1.5">
|
||||||
@@ -191,7 +340,106 @@ export const SessionList = () => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</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)} />}
|
{showCreateGroup && <CreateGroupDialog onClose={() => setShowCreateGroup(false)} />}
|
||||||
|
{showCreateGroupForMove && (
|
||||||
|
<CreateGroupDialog onClose={() => setShowCreateGroupForMove(false)} onCreateWithSessions={handleCreateGroupAndMove} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,19 @@ export function useChatSessions(filter?: ChatSessionsFilter) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteSessions(sessionIds: string[]) {
|
||||||
|
await client.delete('/pi/sessions', {
|
||||||
|
sessionIds,
|
||||||
|
...(filter?.context ? { context: filter.context } : {}),
|
||||||
|
...(filter?.contextId ? { contextId: filter.contextId } : {}),
|
||||||
|
});
|
||||||
|
const idSet = new Set(sessionIds);
|
||||||
|
queryClient.setQueryData<SessionEntry[]>(
|
||||||
|
['PI_SESSIONS', filter?.context, filter?.contextId],
|
||||||
|
(prev) => prev?.filter((s) => !idSet.has(s.id)) ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function clearSessions() {
|
async function clearSessions() {
|
||||||
await client.delete('/pi/sessions', {
|
await client.delete('/pi/sessions', {
|
||||||
...(filter?.context ? { context: filter.context } : {}),
|
...(filter?.context ? { context: filter.context } : {}),
|
||||||
@@ -68,6 +81,7 @@ export function useChatSessions(filter?: ChatSessionsFilter) {
|
|||||||
saveMessages,
|
saveMessages,
|
||||||
renameSession,
|
renameSession,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
|
deleteSessions,
|
||||||
clearSessions,
|
clearSessions,
|
||||||
searchSessions,
|
searchSessions,
|
||||||
invalidate,
|
invalidate,
|
||||||
|
|||||||
Reference in New Issue
Block a user