1
1
import * as React from 'react' ;
2
2
3
- import { Avatar , Box , IconButton , ListItemButton , ListItemDecorator , Typography } from '@mui/joy' ;
4
- import { SxProps } from '@mui/joy/styles/types ' ;
3
+ import { Avatar , Box , IconButton , ListItem , ListItemButton , ListItemDecorator , Sheet , styled , Tooltip , Typography } from '@mui/joy' ;
4
+ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh ' ;
5
5
import CloseIcon from '@mui/icons-material/Close' ;
6
+ import DeleteForeverIcon from '@mui/icons-material/DeleteForever' ;
6
7
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline' ;
8
+ import EditIcon from '@mui/icons-material/Edit' ;
7
9
8
10
import { SystemPurposeId , SystemPurposes } from '../../../../data' ;
9
11
12
+ import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle' ;
13
+
10
14
import { DConversationId , useChatStore } from '~/common/state/store-chats' ;
11
15
import { InlineTextarea } from '~/common/components/InlineTextarea' ;
12
16
13
17
14
- const DEBUG_CONVERSATION_IDs = false ;
18
+ const FadeInButton = styled ( IconButton ) ( {
19
+ opacity : 0.5 ,
20
+ transition : 'opacity 0.2s' ,
21
+ '&:hover' : { opacity : 1 } ,
22
+ } ) ;
15
23
16
24
17
25
export const ChatDrawerItemMemo = React . memo ( ChatNavigationItem ) ;
@@ -44,152 +52,218 @@ function ChatNavigationItem(props: {
44
52
const { conversationId, isActive, title, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props . item ;
45
53
const isNew = messageCount === 0 ;
46
54
47
- // auto-close the arming menu when clicking away
48
- // NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening
49
- // because the isActive prop is not yet updated
55
+
56
+ // [effect] auto-disarm when inactive
57
+ const shallClose = deleteArmed && ! isActive ;
50
58
React . useEffect ( ( ) => {
51
- if ( deleteArmed && ! isActive )
59
+ if ( shallClose )
52
60
setDeleteArmed ( false ) ;
53
- } , [ deleteArmed , isActive ] ) ;
61
+ } , [ shallClose ] ) ;
62
+
54
63
64
+ // Activate
55
65
56
66
const handleConversationActivate = ( ) => props . onConversationActivate ( conversationId , true ) ;
57
67
58
- const handleTitleEdit = ( ) => setIsEditingTitle ( true ) ;
59
68
60
- const handleTitleEdited = ( text : string ) => {
69
+ // Title Edit
70
+
71
+ const handleTitleEditBegin = React . useCallback ( ( ) => setIsEditingTitle ( true ) , [ ] ) ;
72
+
73
+ const handleTitleEditCancel = React . useCallback ( ( ) => {
61
74
setIsEditingTitle ( false ) ;
62
- useChatStore . getState ( ) . setUserTitle ( conversationId , text . trim ( ) ) ;
63
- } ;
75
+ } , [ ] ) ;
64
76
65
- const handleTitleEditCancel = ( ) => {
77
+ const handleTitleEditChange = React . useCallback ( ( text : string ) => {
66
78
setIsEditingTitle ( false ) ;
67
- } ;
79
+ useChatStore . getState ( ) . setUserTitle ( conversationId , text . trim ( ) ) ;
80
+ } , [ conversationId ] ) ;
81
+
82
+ const handleTitleEditAuto = React . useCallback ( ( ) => {
83
+ conversationAutoTitle ( conversationId , true ) ;
84
+ } , [ conversationId ] ) ;
85
+
86
+
87
+ // Delete
68
88
69
- const handleDeleteButtonShow = ( event : React . MouseEvent ) => {
70
- event . stopPropagation ( ) ;
71
- if ( ! isActive )
72
- props . onConversationActivate ( conversationId , false ) ;
73
- else
74
- setDeleteArmed ( true ) ;
75
- } ;
89
+ const handleDeleteButtonShow = React . useCallback ( ( ) => setDeleteArmed ( true ) , [ ] ) ;
76
90
77
- const handleDeleteButtonHide = ( ) => setDeleteArmed ( false ) ;
91
+ const handleDeleteButtonHide = React . useCallback ( ( ) => setDeleteArmed ( false ) , [ ] ) ;
78
92
79
- const handleConversationDelete = ( event : React . MouseEvent ) => {
93
+ const handleConversationDelete = React . useCallback ( ( event : React . MouseEvent ) => {
80
94
if ( deleteArmed ) {
81
95
setDeleteArmed ( false ) ;
82
96
event . stopPropagation ( ) ;
83
97
props . onConversationDelete ( conversationId ) ;
84
98
}
85
- } ;
99
+ } , [ conversationId , deleteArmed , props ] ) ;
86
100
87
101
88
102
const textSymbol = SystemPurposes [ systemPurposeId ] ?. symbol || '❓' ;
89
- const buttonSx : SxProps = isActive ? { color : 'white' } : { } ;
90
103
91
104
const progress = props . bottomBarBasis ? 100 * ( searchFrequency ?? messageCount ) / props . bottomBarBasis : 0 ;
92
105
93
- return (
94
- < ListItemButton
95
- variant = { isActive ? 'soft' : 'plain' } color = 'neutral'
96
- onClick = { ! isActive ? handleConversationActivate : event => event . preventDefault ( ) }
106
+
107
+ const titleRowComponent = React . useMemo ( ( ) => < >
108
+
109
+ { /* Symbol, if globally enabled */ }
110
+ { props . showSymbols && < ListItemDecorator >
111
+ { assistantTyping
112
+ ? (
113
+ < Avatar
114
+ alt = 'typing' variant = 'plain'
115
+ src = 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
116
+ sx = { {
117
+ width : '1.5rem' ,
118
+ height : '1.5rem' ,
119
+ borderRadius : 'var(--joy-radius-sm)' ,
120
+ } }
121
+ />
122
+ ) : (
123
+ < Typography >
124
+ { isNew ? '' : textSymbol }
125
+ </ Typography >
126
+ ) }
127
+ </ ListItemDecorator > }
128
+
129
+ { /* Title */ }
130
+ { ! isEditingTitle ? (
131
+ < Typography
132
+ // level={isActive ? 'title-md' : 'body-md'}
133
+ onDoubleClick = { handleTitleEditBegin }
134
+ sx = { {
135
+ color : isActive ? 'text.primary' : 'text.secondary' ,
136
+ flex : 1 ,
137
+ } }
138
+ >
139
+ { title . trim ( ) ? title : 'Chat' } { assistantTyping && '...' }
140
+ </ Typography >
141
+ ) : (
142
+ < InlineTextarea
143
+ invertedColors
144
+ initialText = { title }
145
+ onEdit = { handleTitleEditChange }
146
+ onCancel = { handleTitleEditCancel }
147
+ sx = { {
148
+ flexGrow : 1 ,
149
+ ml : - 1.5 , mr : - 0.5 ,
150
+ } }
151
+ />
152
+ ) }
153
+
154
+ { /* Display search frequency if it exists and is greater than 0 */ }
155
+ { searchFrequency && searchFrequency > 0 && (
156
+ < Box sx = { { ml : 1 } } >
157
+ < Typography level = 'body-sm' >
158
+ { searchFrequency }
159
+ </ Typography >
160
+ </ Box >
161
+ ) }
162
+
163
+ </ > , [ assistantTyping , handleTitleEditBegin , handleTitleEditCancel , handleTitleEditChange , isActive , isEditingTitle , isNew , props . showSymbols , searchFrequency , textSymbol , title ] ) ;
164
+
165
+ const progressBarFixedComponent = React . useMemo ( ( ) =>
166
+ progress > 0 && (
167
+ < Box sx = { {
168
+ backgroundColor : 'neutral.softBg' ,
169
+ position : 'absolute' , left : 0 , bottom : 0 , width : progress + '%' , height : 4 ,
170
+ } } />
171
+ ) , [ progress ] ) ;
172
+
173
+
174
+ return isActive ?
175
+
176
+ // Active Conversation
177
+ < Sheet
178
+ variant = { isActive ? 'solid' : 'plain' } color = 'neutral'
179
+ invertedColors = { isActive }
97
180
sx = { {
98
- // py: 0,
99
- position : 'relative' ,
100
- border : 'none' , // note, there's a default border of 1px and invisible.. hmm
101
- cursor : 'pointer' ,
102
- '&:hover > button' : { opacity : 1 } ,
181
+ // common
182
+ '--ListItem-minHeight' : '2.75rem' ,
183
+ position : 'relative' , // for the progress bar
184
+ border : 'none' , // there's a default border of 1px and invisible.. hmm
185
+ // style
186
+ borderRadius : 'md' ,
187
+ mx : '0.25rem' ,
188
+ '&:hover > button' : {
189
+ opacity : 1 , // fade in buttons when hovering, but by default wash them out a bit
190
+ } ,
103
191
} }
104
192
>
105
193
106
- { /* Optional progress bar, underlay */ }
107
- { progress > 0 && (
108
- < Box sx = { {
109
- backgroundColor : 'neutral.softBg' ,
110
- position : 'absolute' , left : 0 , bottom : 0 , width : progress + '%' , height : 4 ,
111
- } } />
112
- ) }
113
-
114
- { /* Icon */ }
115
- { props . showSymbols && < ListItemDecorator >
116
- { assistantTyping
117
- ? (
118
- < Avatar
119
- alt = 'typing' variant = 'plain'
120
- src = 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
121
- sx = { {
122
- width : '1.5rem' ,
123
- height : '1.5rem' ,
124
- borderRadius : 'var(--joy-radius-sm)' ,
125
- } }
126
- />
127
- ) : (
128
- < Typography >
129
- { isNew ? '' : textSymbol }
130
- </ Typography >
131
- ) }
132
- </ ListItemDecorator > }
194
+ < ListItem sx = { { border : 'none' , display : 'grid' , gap : 0 , px : 'calc(var(--ListItem-paddingX) - 0.25rem)' } } >
133
195
196
+ { /* title row */ }
197
+ < Box sx = { { display : 'flex' , gap : 'var(--ListItem-gap)' , minHeight : '2.25rem' , alignItems : 'center' } } >
134
198
135
- { /* Text */ }
136
- { ! isEditingTitle ? (
199
+ { titleRowComponent }
137
200
138
- < Typography
139
- level = { isActive ? 'title-md' : 'body-md' }
140
- onDoubleClick = { handleTitleEdit }
141
- sx = { { flex : 1 } }
142
- >
143
- { DEBUG_CONVERSATION_IDs ? conversationId . slice ( 0 , 10 ) : ( title . trim ( ) ? title : 'Chat' ) } { assistantTyping && '...' }
144
- </ Typography >
201
+ </ Box >
145
202
146
- ) : (
203
+ { /* buttons row */ }
204
+ < Box sx = { { display : 'flex' , gap : 'var(--ListItem-gap)' , minHeight : '2.25rem' , alignItems : 'center' } } >
147
205
148
- < InlineTextarea initialText = { title } onEdit = { handleTitleEdited } onCancel = { handleTitleEditCancel } sx = { { ml : - 1.5 , mr : - 0.5 , flexGrow : 1 } } />
206
+ < ListItemDecorator />
149
207
150
- ) }
208
+ < Tooltip title = 'Rename Chat' >
209
+ < FadeInButton size = 'sm' disabled = { isEditingTitle } onClick = { handleTitleEditBegin } >
210
+ < EditIcon />
211
+ </ FadeInButton >
212
+ </ Tooltip >
151
213
152
- { /* // TODO: Commented code */ }
153
- { /* Edit */ }
154
- { /*<IconButton*/ }
155
- { /* onClick={() => props.onEditTitle(props.conversationId)}*/ }
156
- { /* sx={{*/ }
157
- { /* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/ }
158
- { /* }}>*/ }
159
- { /* <EditIcon />*/ }
160
- { /*</IconButton>*/ }
214
+ { ! isNew && (
215
+ < Tooltip title = 'Auto-title Chat' >
216
+ < FadeInButton size = 'sm' disabled = { isEditingTitle } onClick = { handleTitleEditAuto } >
217
+ < AutoFixHighIcon />
218
+ </ FadeInButton >
219
+ </ Tooltip >
220
+ ) }
221
+
222
+ { /* --> */ }
223
+ < Box sx = { { flex : 1 } } />
224
+
225
+ { /* Delete Button(s) */ }
226
+ { ! props . isLonely && ! searchFrequency && < >
227
+ { deleteArmed && (
228
+ < Tooltip title = 'Confirm Deletion' >
229
+ < FadeInButton key = 'btn-del' variant = 'solid' color = 'success' size = 'sm' onClick = { handleConversationDelete } sx = { { opacity : 1 } } >
230
+ < DeleteForeverIcon sx = { { color : 'danger.solidBg' } } />
231
+ </ FadeInButton >
232
+ </ Tooltip >
233
+ ) }
234
+
235
+ < Tooltip title = { deleteArmed ? 'Cancel' : 'Delete?' } >
236
+ < FadeInButton key = 'btn-arm' size = 'sm' onClick = { deleteArmed ? handleDeleteButtonHide : handleDeleteButtonShow } sx = { deleteArmed ? { opacity : 1 } : { } } >
237
+ { deleteArmed ? < CloseIcon /> : < DeleteOutlineIcon /> }
238
+ </ FadeInButton >
239
+ </ Tooltip >
240
+ </ > }
161
241
162
- { /* Display search frequency if it exists and is greater than 0 */ }
163
- { searchFrequency && searchFrequency > 0 && (
164
- < Box sx = { { ml : 1 } } >
165
- < Typography level = 'body-sm' >
166
- { searchFrequency }
167
- </ Typography >
168
242
</ Box >
169
- ) }
170
-
171
- { /* Delete Arming */ }
172
- { ! props . isLonely && ! deleteArmed && ! searchFrequency && (
173
- < IconButton
174
- variant = { isActive ? 'solid' : 'outlined' }
175
- size = 'sm'
176
- sx = { { opacity : { xs : 1 , sm : 0 } , transition : 'opacity 0.2s' , ... buttonSx } }
177
- onClick = { handleDeleteButtonShow }
178
- >
179
- < DeleteOutlineIcon />
180
- </ IconButton >
181
- ) }
182
-
183
- { /* Delete / Cancel buttons */ }
184
- { ! props . isLonely && deleteArmed && ! searchFrequency && < >
185
- < IconButton size = 'sm' variant = 'solid' color = 'danger' sx = { buttonSx } onClick = { handleConversationDelete } >
186
- < DeleteOutlineIcon />
187
- </ IconButton >
188
- < IconButton size = 'sm' variant = 'solid' color = 'neutral' sx = { buttonSx } onClick = { handleDeleteButtonHide } >
189
- < CloseIcon />
190
- </ IconButton >
191
- </ > }
192
-
193
- </ ListItemButton >
194
- ) ;
243
+
244
+ </ ListItem >
245
+
246
+ { /* Optional progress bar, underlay */ }
247
+ { progressBarFixedComponent }
248
+
249
+ </ Sheet >
250
+
251
+ :
252
+
253
+ // Inactive Conversation - click to activate
254
+ < ListItemButton
255
+ onClick = { handleConversationActivate }
256
+ sx = { {
257
+ '--ListItem-minHeight' : '2.75rem' ,
258
+ position : 'relative' , // for the progress bar
259
+ border : 'none' , // there's a default border of 1px and invisible.. hmm
260
+ } }
261
+ >
262
+
263
+ { titleRowComponent }
264
+
265
+ { /* Optional progress bar, underlay */ }
266
+ { progressBarFixedComponent }
267
+
268
+ </ ListItemButton > ;
195
269
}
0 commit comments