Skip to content

Commit ef9b36d

Browse files
committed
fix(chat): 优化会话列表快捷删除交互
Closes #56
1 parent 8ea9735 commit ef9b36d

13 files changed

Lines changed: 436 additions & 33 deletions

File tree

src/components/chat/ChatSidebar.tsx

Lines changed: 125 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@ import {
2929
type DragOverEvent,
3030
} from '@dnd-kit/core'
3131

32+
type DeleteShortcutEvent = Pick<React.MouseEvent<HTMLElement>, 'ctrlKey' | 'metaKey'>
33+
34+
function isDirectDeleteEvent(event?: DeleteShortcutEvent): boolean {
35+
return Boolean(event?.ctrlKey || event?.metaKey)
36+
}
37+
38+
function getDirectDeleteShortcutLabel(): string {
39+
if (typeof navigator === 'undefined') return 'Ctrl'
40+
const platform = navigator.platform || ''
41+
const userAgent = navigator.userAgent || ''
42+
const isMac = /Mac|iPhone|iPad|iPod/i.test(platform)
43+
|| (/Mac OS/i.test(userAgent) && !/Windows|Linux|Android/i.test(userAgent))
44+
return isMac ? '⌘' : 'Ctrl'
45+
}
46+
3247
function getDateGroup(timestamp: number): string {
3348
const now = new Date()
3449
const date = new Date(timestamp * 1000)
@@ -229,6 +244,23 @@ export function ChatSidebar() {
229244
const [categoryModalOpen, setCategoryModalOpen] = useState(false)
230245
const [editingCategory, setEditingCategory] = useState<ConversationCategory | null>(null)
231246
const [expandedParentIds, setExpandedParentIds] = useState<Set<string>>(new Set())
247+
const [directDeleteMode, setDirectDeleteMode] = useState(false)
248+
249+
useEffect(() => {
250+
const updateFromKeyboard = (event: KeyboardEvent) => {
251+
setDirectDeleteMode(event.ctrlKey || event.metaKey)
252+
}
253+
const reset = () => setDirectDeleteMode(false)
254+
255+
window.addEventListener('keydown', updateFromKeyboard)
256+
window.addEventListener('keyup', updateFromKeyboard)
257+
window.addEventListener('blur', reset)
258+
return () => {
259+
window.removeEventListener('keydown', updateFromKeyboard)
260+
window.removeEventListener('keyup', updateFromKeyboard)
261+
window.removeEventListener('blur', reset)
262+
}
263+
}, [])
232264

233265
// Auto-expand parent when active conversation is a child
234266
useEffect(() => {
@@ -524,7 +556,42 @@ export function ChatSidebar() {
524556
return icon
525557
}, [streamingConversationId, token.colorPrimary, token.colorPrimaryBg, token.colorBgContainer])
526558

527-
const conversationItems: ConversationItemType[] = useMemo(
559+
const directDeleteShortcutLabel = useMemo(() => getDirectDeleteShortcutLabel(), [])
560+
const directDeleteHint = t('chat.directDeleteHint', { shortcut: directDeleteShortcutLabel })
561+
562+
const handleDelete = useCallback(
563+
(
564+
item: Pick<ConversationItemType, 'key'>,
565+
event?: DeleteShortcutEvent,
566+
afterDelete?: () => void | Promise<void>,
567+
) => {
568+
const id = String(item.key)
569+
const runDelete = async () => {
570+
await deleteConversation(id)
571+
await afterDelete?.()
572+
}
573+
574+
if (isDirectDeleteEvent(event)) {
575+
void runDelete()
576+
return
577+
}
578+
579+
modal.confirm({
580+
title: t('chat.deleteConfirm'),
581+
mask: { enabled: true, blur: true },
582+
okButtonProps: { danger: true },
583+
onOk: runDelete,
584+
})
585+
},
586+
[deleteConversation, t, modal],
587+
)
588+
589+
const syncDirectDeleteModeFromMouse = useCallback((event: DeleteShortcutEvent) => {
590+
const next = isDirectDeleteEvent(event)
591+
setDirectDeleteMode((current) => (current === next ? current : next))
592+
}, [])
593+
594+
const conversationItems: ConversationItemType[] = useMemo(
528595
() => {
529596
const items: ConversationItemType[] = []
530597

@@ -843,7 +910,8 @@ export function ChatSidebar() {
843910

844911
const handleRename = useCallback(
845912
(item: ConversationItemType) => {
846-
let newTitle = String(item.label ?? '')
913+
const conversation = conversations.find((c) => c.id === String(item.key))
914+
let newTitle = conversation?.title ?? (typeof item.label === 'string' ? item.label : '')
847915
modal.confirm({
848916
title: t('chat.rename'),
849917
mask: { enabled: true, blur: true },
@@ -862,19 +930,7 @@ export function ChatSidebar() {
862930
},
863931
})
864932
},
865-
[updateConversation, t, modal],
866-
)
867-
868-
const handleDelete = useCallback(
869-
(item: ConversationItemType) => {
870-
modal.confirm({
871-
title: t('chat.deleteConfirm'),
872-
mask: { enabled: true, blur: true },
873-
okButtonProps: { danger: true },
874-
onOk: () => deleteConversation(String(item.key)),
875-
})
876-
},
877-
[deleteConversation, t, modal],
933+
[conversations, updateConversation, t, modal],
878934
)
879935

880936
const buildExportChildren = useCallback(
@@ -972,7 +1028,29 @@ export function ChatSidebar() {
9721028
}
9731029
}
9741030
return {
975-
items: [
1031+
trigger: (_conversation: ConversationItemType, info: { originNode: React.ReactNode }) => {
1032+
if (!directDeleteMode) {
1033+
return <Tooltip title={directDeleteHint}>{info.originNode}</Tooltip>
1034+
}
1035+
return (
1036+
<Tooltip title={directDeleteHint}>
1037+
<Button
1038+
type="text"
1039+
danger
1040+
size="small"
1041+
aria-label={t('chat.delete')}
1042+
className="ant-conversations-menu-icon aqbot-chat-conversation-menu-delete"
1043+
icon={<Trash2 size={14} />}
1044+
onClick={(event) => {
1045+
event.preventDefault()
1046+
event.stopPropagation()
1047+
handleDelete(item, event)
1048+
}}
1049+
/>
1050+
</Tooltip>
1051+
)
1052+
},
1053+
items: directDeleteMode ? [] : [
9761054
{
9771055
key: 'pin',
9781056
label: isPinned ? t('chat.unpin') : t('chat.pin'),
@@ -984,11 +1062,11 @@ export function ChatSidebar() {
9841062
{
9851063
key: 'export',
9861064
label: (<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}><Share size={14} />{t('chat.export')}</span>),
987-
children: buildExportChildren(String(item.key), String(item.label ?? '')),
1065+
children: buildExportChildren(String(item.key), conv?.title ?? (typeof item.label === 'string' ? item.label : '')),
9881066
},
9891067
{ key: 'delete', label: t('chat.delete'), icon: <Trash2 size={14} />, danger: true },
9901068
],
991-
onClick: (menuInfo: { key: string }) => {
1069+
onClick: (menuInfo: { key: string; domEvent?: DeleteShortcutEvent }) => {
9921070
if (menuInfo.key.startsWith('move-to-cat:')) {
9931071
const catId = menuInfo.key.slice('move-to-cat:'.length)
9941072
void updateConversation(String(item.key), { category_id: catId })
@@ -1009,13 +1087,13 @@ export function ChatSidebar() {
10091087
handleRename(item)
10101088
break
10111089
case 'delete':
1012-
handleDelete(item)
1090+
handleDelete(item, menuInfo.domEvent)
10131091
break
10141092
}
10151093
},
10161094
}
10171095
},
1018-
[t, conversations, multiSelectMode, handleRename, handleDelete, togglePin, toggleArchive, buildExportChildren, categories, moveToCategoryMenuItems, updateConversation],
1096+
[t, conversations, multiSelectMode, handleRename, handleDelete, togglePin, toggleArchive, buildExportChildren, categories, moveToCategoryMenuItems, updateConversation, directDeleteMode, directDeleteHint],
10191097
)
10201098

10211099
const handleConversationClick = useCallback((key: string) => {
@@ -1063,7 +1141,7 @@ export function ChatSidebar() {
10631141
},
10641142
{ key: 'delete', label: t('chat.delete'), icon: <Trash2 size={14} />, danger: true },
10651143
],
1066-
onClick: (menuInfo: { key: string }) => {
1144+
onClick: (menuInfo: { key: string; domEvent?: DeleteShortcutEvent }) => {
10671145
if (menuInfo.key.startsWith('move-to-cat:')) {
10681146
const catId = menuInfo.key.slice('move-to-cat:'.length)
10691147
void updateConversation(conv.id, { category_id: catId })
@@ -1078,7 +1156,7 @@ export function ChatSidebar() {
10781156
case 'pin': togglePin(conv.id); break
10791157
case 'archive': toggleArchive(conv.id); break
10801158
case 'rename': handleRename(item); break
1081-
case 'delete': handleDelete(item); break
1159+
case 'delete': handleDelete(item, menuInfo.domEvent); break
10821160
}
10831161
},
10841162
}
@@ -1264,23 +1342,16 @@ export function ChatSidebar() {
12641342
}}
12651343
/>
12661344
</Tooltip>
1267-
<Tooltip title={t('chat.delete')}>
1345+
<Tooltip title={directDeleteHint}>
12681346
<Button
12691347
type="text"
12701348
size="small"
12711349
danger
1350+
aria-label={t('chat.delete')}
12721351
icon={<Trash2 size={14} />}
12731352
onClick={(e) => {
12741353
e.stopPropagation()
1275-
modal.confirm({
1276-
title: t('chat.deleteConfirm'),
1277-
mask: { enabled: true, blur: true },
1278-
okButtonProps: { danger: true },
1279-
onOk: async () => {
1280-
await deleteConversation(conv.id)
1281-
await fetchArchivedConversations()
1282-
},
1283-
})
1354+
handleDelete({ key: conv.id }, e, fetchArchivedConversations)
12841355
}}
12851356
/>
12861357
</Tooltip>
@@ -1302,7 +1373,9 @@ export function ChatSidebar() {
13021373
onOpenChange={(open) => { if (!open) setRightClickedConvId(null) }}
13031374
>
13041375
<div className="flex-1 overflow-y-auto">
1305-
<div onContextMenu={(e) => {
1376+
<div
1377+
onMouseMove={syncDirectDeleteModeFromMouse}
1378+
onContextMenu={(e) => {
13061379
if (multiSelectMode) { e.preventDefault(); e.stopPropagation(); return }
13071380
const listItem = (e.target as HTMLElement).closest('[data-conv-id]') as HTMLElement
13081381
if (!listItem) { e.preventDefault(); e.stopPropagation(); return }
@@ -1317,6 +1390,25 @@ export function ChatSidebar() {
13171390
.ant-conversations .ant-conversations-item-active .ant-conversations-label {
13181391
color: ${token.colorPrimary} !important;
13191392
}
1393+
.aqbot-chat-conversation-menu-delete {
1394+
width: 22px;
1395+
height: 22px;
1396+
min-width: 22px;
1397+
padding: 0;
1398+
display: inline-flex;
1399+
align-items: center;
1400+
justify-content: center;
1401+
}
1402+
.ant-conversations .ant-conversations-item-active .aqbot-chat-conversation-menu-delete {
1403+
opacity: 0;
1404+
}
1405+
.ant-conversations .ant-conversations-item:hover .aqbot-chat-conversation-menu-delete,
1406+
.aqbot-chat-conversation-menu-delete:focus-visible {
1407+
opacity: 0.85;
1408+
}
1409+
.aqbot-chat-conversation-menu-delete:hover {
1410+
opacity: 1 !important;
1411+
}
13201412
.ant-conversations .ant-conversations-group-label {
13211413
flex: 1;
13221414
overflow: hidden;

0 commit comments

Comments
 (0)