Skip to content

Commit

Permalink
Rename/Auto-Name conversations and New UI Conversation Item. Fixes #222
Browse files Browse the repository at this point in the history
…, Fixes #297.
  • Loading branch information
enricoros committed Jan 16, 2024
1 parent 216dae9 commit 571a04c
Showing 1 changed file with 188 additions and 114 deletions.
302 changes: 188 additions & 114 deletions src/apps/chat/components/applayout/ChatNavigationItem.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import * as React from 'react';

import { Avatar, Box, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import { Avatar, Box, IconButton, ListItem, ListItemButton, ListItemDecorator, Sheet, styled, Tooltip, Typography } from '@mui/joy';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import CloseIcon from '@mui/icons-material/Close';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import EditIcon from '@mui/icons-material/Edit';

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

import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';

import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { InlineTextarea } from '~/common/components/InlineTextarea';


const DEBUG_CONVERSATION_IDs = false;
const FadeInButton = styled(IconButton)({
opacity: 0.5,
transition: 'opacity 0.2s',
'&:hover': { opacity: 1 },
});


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

// auto-close the arming menu when clicking away
// NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening
// because the isActive prop is not yet updated

// [effect] auto-disarm when inactive
const shallClose = deleteArmed && !isActive;
React.useEffect(() => {
if (deleteArmed && !isActive)
if (shallClose)
setDeleteArmed(false);
}, [deleteArmed, isActive]);
}, [shallClose]);


// Activate

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

const handleTitleEdit = () => setIsEditingTitle(true);

const handleTitleEdited = (text: string) => {
// Title Edit

const handleTitleEditBegin = React.useCallback(() => setIsEditingTitle(true), []);

const handleTitleEditCancel = React.useCallback(() => {
setIsEditingTitle(false);
useChatStore.getState().setUserTitle(conversationId, text.trim());
};
}, []);

const handleTitleEditCancel = () => {
const handleTitleEditChange = React.useCallback((text: string) => {
setIsEditingTitle(false);
};
useChatStore.getState().setUserTitle(conversationId, text.trim());
}, [conversationId]);

const handleTitleEditAuto = React.useCallback(() => {
conversationAutoTitle(conversationId, true);
}, [conversationId]);


// Delete

const handleDeleteButtonShow = (event: React.MouseEvent) => {
event.stopPropagation();
if (!isActive)
props.onConversationActivate(conversationId, false);
else
setDeleteArmed(true);
};
const handleDeleteButtonShow = React.useCallback(() => setDeleteArmed(true), []);

const handleDeleteButtonHide = () => setDeleteArmed(false);
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);

const handleConversationDelete = (event: React.MouseEvent) => {
const handleConversationDelete = React.useCallback((event: React.MouseEvent) => {
if (deleteArmed) {
setDeleteArmed(false);
event.stopPropagation();
props.onConversationDelete(conversationId);
}
};
}, [conversationId, deleteArmed, props]);


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

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

return (
<ListItemButton
variant={isActive ? 'soft' : 'plain'} color='neutral'
onClick={!isActive ? handleConversationActivate : event => event.preventDefault()}

const titleRowComponent = React.useMemo(() => <>

{/* Symbol, if globally enabled */}
{props.showSymbols && <ListItemDecorator>
{assistantTyping
? (
<Avatar
alt='typing' variant='plain'
src='https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
sx={{
width: '1.5rem',
height: '1.5rem',
borderRadius: 'var(--joy-radius-sm)',
}}
/>
) : (
<Typography>
{isNew ? '' : textSymbol}
</Typography>
)}
</ListItemDecorator>}

{/* Title */}
{!isEditingTitle ? (
<Typography
// level={isActive ? 'title-md' : 'body-md'}
onDoubleClick={handleTitleEditBegin}
sx={{
color: isActive ? 'text.primary' : 'text.secondary',
flex: 1,
}}
>
{title.trim() ? title : 'Chat'}{assistantTyping && '...'}
</Typography>
) : (
<InlineTextarea
invertedColors
initialText={title}
onEdit={handleTitleEditChange}
onCancel={handleTitleEditCancel}
sx={{
flexGrow: 1,
ml: -1.5, mr: -0.5,
}}
/>
)}

{/* Display search frequency if it exists and is greater than 0 */}
{searchFrequency && searchFrequency > 0 && (
<Box sx={{ ml: 1 }}>
<Typography level='body-sm'>
{searchFrequency}
</Typography>
</Box>
)}

</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title]);

const progressBarFixedComponent = React.useMemo(() =>
progress > 0 && (
<Box sx={{
backgroundColor: 'neutral.softBg',
position: 'absolute', left: 0, bottom: 0, width: progress + '%', height: 4,
}} />
), [progress]);


return isActive ?

// Active Conversation
<Sheet
variant={isActive ? 'solid' : 'plain'} color='neutral'
invertedColors={isActive}
sx={{
// py: 0,
position: 'relative',
border: 'none', // note, there's a default border of 1px and invisible.. hmm
cursor: 'pointer',
'&:hover > button': { opacity: 1 },
// common
'--ListItem-minHeight': '2.75rem',
position: 'relative', // for the progress bar
border: 'none', // there's a default border of 1px and invisible.. hmm
// style
borderRadius: 'md',
mx: '0.25rem',
'&:hover > button': {
opacity: 1, // fade in buttons when hovering, but by default wash them out a bit
},
}}
>

{/* Optional progress bar, underlay */}
{progress > 0 && (
<Box sx={{
backgroundColor: 'neutral.softBg',
position: 'absolute', left: 0, bottom: 0, width: progress + '%', height: 4,
}} />
)}

{/* Icon */}
{props.showSymbols && <ListItemDecorator>
{assistantTyping
? (
<Avatar
alt='typing' variant='plain'
src='https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
sx={{
width: '1.5rem',
height: '1.5rem',
borderRadius: 'var(--joy-radius-sm)',
}}
/>
) : (
<Typography>
{isNew ? '' : textSymbol}
</Typography>
)}
</ListItemDecorator>}
<ListItem sx={{ border: 'none', display: 'grid', gap: 0, px: 'calc(var(--ListItem-paddingX) - 0.25rem)' }}>

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

{/* Text */}
{!isEditingTitle ? (
{titleRowComponent}

<Typography
level={isActive ? 'title-md' : 'body-md'}
onDoubleClick={handleTitleEdit}
sx={{ flex: 1 }}
>
{DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : (title.trim() ? title : 'Chat')}{assistantTyping && '...'}
</Typography>
</Box>

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

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

)}
<Tooltip title='Rename Chat'>
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditBegin}>
<EditIcon />
</FadeInButton>
</Tooltip>

{/* // TODO: Commented code */}
{/* Edit */}
{/*<IconButton*/}
{/* onClick={() => props.onEditTitle(props.conversationId)}*/}
{/* sx={{*/}
{/* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/}
{/* }}>*/}
{/* <EditIcon />*/}
{/*</IconButton>*/}
{!isNew && (
<Tooltip title='Auto-title Chat'>
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditAuto}>
<AutoFixHighIcon />
</FadeInButton>
</Tooltip>
)}

{/* --> */}
<Box sx={{ flex: 1 }} />

{/* Delete Button(s) */}
{!props.isLonely && !searchFrequency && <>
{deleteArmed && (
<Tooltip title='Confirm Deletion'>
<FadeInButton key='btn-del' variant='solid' color='success' size='sm' onClick={handleConversationDelete} sx={{ opacity: 1 }}>
<DeleteForeverIcon sx={{ color: 'danger.solidBg' }} />
</FadeInButton>
</Tooltip>
)}

<Tooltip title={deleteArmed ? 'Cancel' : 'Delete?'}>
<FadeInButton key='btn-arm' size='sm' onClick={deleteArmed ? handleDeleteButtonHide : handleDeleteButtonShow} sx={deleteArmed ? { opacity: 1 } : {}}>
{deleteArmed ? <CloseIcon /> : <DeleteOutlineIcon />}
</FadeInButton>
</Tooltip>
</>}

{/* Display search frequency if it exists and is greater than 0 */}
{searchFrequency && searchFrequency > 0 && (
<Box sx={{ ml: 1 }}>
<Typography level='body-sm'>
{searchFrequency}
</Typography>
</Box>
)}

{/* Delete Arming */}
{!props.isLonely && !deleteArmed && !searchFrequency && (
<IconButton
variant={isActive ? 'solid' : 'outlined'}
size='sm'
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s', ...buttonSx }}
onClick={handleDeleteButtonShow}
>
<DeleteOutlineIcon />
</IconButton>
)}

{/* Delete / Cancel buttons */}
{!props.isLonely && deleteArmed && !searchFrequency && <>
<IconButton size='sm' variant='solid' color='danger' sx={buttonSx} onClick={handleConversationDelete}>
<DeleteOutlineIcon />
</IconButton>
<IconButton size='sm' variant='solid' color='neutral' sx={buttonSx} onClick={handleDeleteButtonHide}>
<CloseIcon />
</IconButton>
</>}

</ListItemButton>
);

</ListItem>

{/* Optional progress bar, underlay */}
{progressBarFixedComponent}

</Sheet>

:

// Inactive Conversation - click to activate
<ListItemButton
onClick={handleConversationActivate}
sx={{
'--ListItem-minHeight': '2.75rem',
position: 'relative', // for the progress bar
border: 'none', // there's a default border of 1px and invisible.. hmm
}}
>

{titleRowComponent}

{/* Optional progress bar, underlay */}
{progressBarFixedComponent}

</ListItemButton>;
}

0 comments on commit 571a04c

Please sign in to comment.