group select chats
This commit is contained in:
@@ -4,6 +4,7 @@ import { useChatGroups } from 'state/useChatGroups';
|
||||
|
||||
type CreateGroupDialogProps = {
|
||||
onClose: () => void;
|
||||
onCreateWithSessions?: (name: string, slug: string, description?: string) => Promise<void>;
|
||||
};
|
||||
|
||||
function toSlug(name: string): string {
|
||||
@@ -13,7 +14,7 @@ function toSlug(name: string): string {
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
export function CreateGroupDialog({ onClose }: CreateGroupDialogProps) {
|
||||
export function CreateGroupDialog({ onClose, onCreateWithSessions }: CreateGroupDialogProps) {
|
||||
const { createGroup } = useChatGroups();
|
||||
const [name, setName] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
@@ -38,7 +39,11 @@ export function CreateGroupDialog({ onClose }: CreateGroupDialogProps) {
|
||||
setError(null);
|
||||
|
||||
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();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to create group');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
await client.delete('/pi/sessions', {
|
||||
...(filter?.context ? { context: filter.context } : {}),
|
||||
@@ -68,6 +81,7 @@ export function useChatSessions(filter?: ChatSessionsFilter) {
|
||||
saveMessages,
|
||||
renameSession,
|
||||
deleteSession,
|
||||
deleteSessions,
|
||||
clearSessions,
|
||||
searchSessions,
|
||||
invalidate,
|
||||
|
||||
Reference in New Issue
Block a user