mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-01-29 22:56:36 +08:00
feat: add batch delete for conversations in chat(web) (#12584)
Resolves #12572 ## What problem does this PR solve? The conversation list in chat sessions previously only supported deleting conversations one by one. This was inefficient when users needed to clean up multiple conversations. This PR adds batch delete functionality to improve user experience. ## Type of change - [x] New Feature (non-breaking change which adds functionality) ## Specific changes - Add selection mode with checkboxes for conversation list - Add batch delete functionality with custom icons - Add internationalization support (en/zh) - Use existing removeConversation API which supports batch deletion ## UI modification status - Default: Show [+] and [batch delete icon] - Selection mode: Show checkboxes, keep [+] and [select all icon] - Items selected: Show [return icon] and [red trash icon]" ### Repair Comparison **1.Before Repair** <img width="982" height="1221" alt="image" src="https://github.com/user-attachments/assets/8a80f7c0-7da6-41ec-9d1a-ac887ede96ba" /> **2.After Repair** <img width="1273" height="919" alt="新增批量删除效果图" src="https://github.com/user-attachments/assets/e179bdf3-3779-4bd5-84b6-8e24780a22ea" /> --- Co-authored-by: Gongzi --------- Co-authored-by: Liu An <asiro@qq.com>
This commit is contained in:
@ -1,16 +1,25 @@
|
||||
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
|
||||
import { MoreButton } from '@/components/more-button';
|
||||
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { SearchInput } from '@/components/ui/input';
|
||||
import { useSetModalState } from '@/hooks/common-hooks';
|
||||
import {
|
||||
useFetchDialog,
|
||||
useGetChatSearchParams,
|
||||
useRemoveConversation,
|
||||
} from '@/hooks/use-chat-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { PanelLeftClose, PanelRightClose, Plus } from 'lucide-react';
|
||||
import { useCallback } from 'react';
|
||||
import {
|
||||
Check,
|
||||
PanelLeftClose,
|
||||
PanelRightClose,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useHandleClickConversationCard } from '../hooks/use-click-card';
|
||||
import { useSelectDerivedConversationList } from '../hooks/use-select-conversation-list';
|
||||
@ -35,12 +44,71 @@ export function Sessions({
|
||||
} = useSelectDerivedConversationList();
|
||||
const { data } = useFetchDialog();
|
||||
const { visible, switchVisible } = useSetModalState(true);
|
||||
const { removeConversation } = useRemoveConversation();
|
||||
|
||||
// Selection mode state
|
||||
const [selectionMode, setSelectionMode] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Toggle selection mode (click batch delete icon)
|
||||
const toggleSelectionMode = useCallback(() => {
|
||||
setSelectionMode(true);
|
||||
setSelectedIds(new Set());
|
||||
}, []);
|
||||
|
||||
// Exit selection mode (click return icon)
|
||||
const exitSelectionMode = useCallback(() => {
|
||||
setSelectionMode(false);
|
||||
setSelectedIds(new Set());
|
||||
}, []);
|
||||
|
||||
// Toggle single item selection
|
||||
const toggleSelection = useCallback((id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Toggle select all
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
setSelectedIds((prev) => {
|
||||
if (prev.size === conversationList.length) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(conversationList.map((x) => x.id));
|
||||
});
|
||||
}, [conversationList]);
|
||||
|
||||
// Batch delete
|
||||
const handleBatchDelete = useCallback(async () => {
|
||||
if (selectedIds.size > 0) {
|
||||
await removeConversation(Array.from(selectedIds));
|
||||
exitSelectionMode();
|
||||
}
|
||||
}, [selectedIds, removeConversation, exitSelectionMode]);
|
||||
|
||||
const selectedCount = useMemo(() => selectedIds.size, [selectedIds]);
|
||||
const allSelected = useMemo(
|
||||
() =>
|
||||
selectedCount === conversationList.length && conversationList.length > 0,
|
||||
[selectedCount, conversationList.length],
|
||||
);
|
||||
|
||||
const handleCardClick = useCallback(
|
||||
(conversationId: string, isNew: boolean) => () => {
|
||||
handleConversationCardClick(conversationId, isNew);
|
||||
if (selectionMode) {
|
||||
toggleSelection(conversationId);
|
||||
} else {
|
||||
handleConversationCardClick(conversationId, isNew);
|
||||
}
|
||||
},
|
||||
[handleConversationCardClick],
|
||||
[handleConversationCardClick, selectionMode, toggleSelection],
|
||||
);
|
||||
|
||||
const { conversationId } = useGetChatSearchParams();
|
||||
@ -55,7 +123,7 @@ export function Sessions({
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="p-6 w-[296px] flex flex-col">
|
||||
<section className="p-6 w-[296px] flex flex-col">
|
||||
<section className="flex items-center text-base justify-between gap-2">
|
||||
<div className="flex gap-3 items-center min-w-0">
|
||||
<RAGFlowAvatar
|
||||
@ -77,9 +145,62 @@ export function Sessions({
|
||||
{conversationList.length}
|
||||
</span>
|
||||
</div>
|
||||
<Button variant={'ghost'} onClick={addTemporaryConversation}>
|
||||
<Plus></Plus>
|
||||
</Button>
|
||||
{selectionMode && selectedCount > 0 ? (
|
||||
// Selection mode with items selected: show return and delete
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={exitSelectionMode}
|
||||
>
|
||||
<img src="/return2.png" alt="返回" className="h-4 w-4" />
|
||||
</Button>
|
||||
<ConfirmDeleteDialog
|
||||
onOk={handleBatchDelete}
|
||||
title={t('chat.batchDeleteSessions')}
|
||||
content={t('chat.deleteSelectedConfirm', {
|
||||
count: selectedCount,
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6 text-state-error"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</ConfirmDeleteDialog>
|
||||
</div>
|
||||
) : (
|
||||
// Default or selection mode without selection: show plus and batch delete
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={addTemporaryConversation}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-6"
|
||||
onClick={selectionMode ? toggleSelectAll : toggleSelectionMode}
|
||||
>
|
||||
{selectionMode && allSelected ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<img
|
||||
src="/batch_delete2.png"
|
||||
alt="批量删除"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="pb-4">
|
||||
<SearchInput
|
||||
@ -92,18 +213,34 @@ export function Sessions({
|
||||
<Card
|
||||
key={x.id}
|
||||
onClick={handleCardClick(x.id, x.is_new)}
|
||||
className={cn('cursor-pointer bg-transparent', {
|
||||
'bg-bg-card': conversationId === x.id,
|
||||
className={cn('cursor-pointer bg-transparent relative', {
|
||||
'bg-bg-card': conversationId === x.id && !selectionMode,
|
||||
})}
|
||||
>
|
||||
<CardContent className="px-3 py-2 flex justify-between items-center group gap-1">
|
||||
<div className="truncate">{x.name}</div>
|
||||
<ConversationDropdown
|
||||
conversation={x}
|
||||
removeTemporaryConversation={removeTemporaryConversation}
|
||||
>
|
||||
<MoreButton></MoreButton>
|
||||
</ConversationDropdown>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{selectionMode && (
|
||||
<span
|
||||
className="flex-shrink-0"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedIds.has(x.id)}
|
||||
onCheckedChange={() => toggleSelection(x.id)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<div className="truncate">{x.name}</div>
|
||||
</div>
|
||||
{!selectionMode && (
|
||||
<ConversationDropdown
|
||||
conversation={x}
|
||||
removeTemporaryConversation={removeTemporaryConversation}
|
||||
>
|
||||
<MoreButton></MoreButton>
|
||||
</ConversationDropdown>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user