Skip to content

Commit 571a04c

Browse files
committed
Rename/Auto-Name conversations and New UI Conversation Item. Fixes #222, Fixes #297.
1 parent 216dae9 commit 571a04c

File tree

1 file changed

+188
-114
lines changed

1 file changed

+188
-114
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
import * as React from 'react';
22

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';
55
import CloseIcon from '@mui/icons-material/Close';
6+
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
67
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
8+
import EditIcon from '@mui/icons-material/Edit';
79

810
import { SystemPurposeId, SystemPurposes } from '../../../../data';
911

12+
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
13+
1014
import { DConversationId, useChatStore } from '~/common/state/store-chats';
1115
import { InlineTextarea } from '~/common/components/InlineTextarea';
1216

1317

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+
});
1523

1624

1725
export const ChatDrawerItemMemo = React.memo(ChatNavigationItem);
@@ -44,152 +52,218 @@ function ChatNavigationItem(props: {
4452
const { conversationId, isActive, title, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
4553
const isNew = messageCount === 0;
4654

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;
5058
React.useEffect(() => {
51-
if (deleteArmed && !isActive)
59+
if (shallClose)
5260
setDeleteArmed(false);
53-
}, [deleteArmed, isActive]);
61+
}, [shallClose]);
62+
5463

64+
// Activate
5565

5666
const handleConversationActivate = () => props.onConversationActivate(conversationId, true);
5767

58-
const handleTitleEdit = () => setIsEditingTitle(true);
5968

60-
const handleTitleEdited = (text: string) => {
69+
// Title Edit
70+
71+
const handleTitleEditBegin = React.useCallback(() => setIsEditingTitle(true), []);
72+
73+
const handleTitleEditCancel = React.useCallback(() => {
6174
setIsEditingTitle(false);
62-
useChatStore.getState().setUserTitle(conversationId, text.trim());
63-
};
75+
}, []);
6476

65-
const handleTitleEditCancel = () => {
77+
const handleTitleEditChange = React.useCallback((text: string) => {
6678
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
6888

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), []);
7690

77-
const handleDeleteButtonHide = () => setDeleteArmed(false);
91+
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);
7892

79-
const handleConversationDelete = (event: React.MouseEvent) => {
93+
const handleConversationDelete = React.useCallback((event: React.MouseEvent) => {
8094
if (deleteArmed) {
8195
setDeleteArmed(false);
8296
event.stopPropagation();
8397
props.onConversationDelete(conversationId);
8498
}
85-
};
99+
}, [conversationId, deleteArmed, props]);
86100

87101

88102
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
89-
const buttonSx: SxProps = isActive ? { color: 'white' } : {};
90103

91104
const progress = props.bottomBarBasis ? 100 * (searchFrequency ?? messageCount) / props.bottomBarBasis : 0;
92105

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}
97180
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+
},
103191
}}
104192
>
105193

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)' }}>
133195

196+
{/* title row */}
197+
<Box sx={{ display: 'flex', gap: 'var(--ListItem-gap)', minHeight: '2.25rem', alignItems: 'center' }}>
134198

135-
{/* Text */}
136-
{!isEditingTitle ? (
199+
{titleRowComponent}
137200

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>
145202

146-
) : (
203+
{/* buttons row */}
204+
<Box sx={{ display: 'flex', gap: 'var(--ListItem-gap)', minHeight: '2.25rem', alignItems: 'center' }}>
147205

148-
<InlineTextarea initialText={title} onEdit={handleTitleEdited} onCancel={handleTitleEditCancel} sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }} />
206+
<ListItemDecorator />
149207

150-
)}
208+
<Tooltip title='Rename Chat'>
209+
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditBegin}>
210+
<EditIcon />
211+
</FadeInButton>
212+
</Tooltip>
151213

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+
</>}
161241

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>
168242
</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>;
195269
}

0 commit comments

Comments
 (0)