@@ -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 = / M a c | i P h o n e | i P a d | i P o d / i. test ( platform )
43+ || ( / M a c O S / i. test ( userAgent ) && ! / W i n d o w s | L i n u x | A n d r o i d / i. test ( userAgent ) )
44+ return isMac ? '⌘' : 'Ctrl'
45+ }
46+
3247function 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