Panes: Edit Title, close, close others

This commit is contained in:
Enrico Ros
2025-03-03 12:35:02 -08:00
parent 9a61e04293
commit 41a5f9a775
3 changed files with 143 additions and 31 deletions
+1
View File
@@ -665,6 +665,7 @@ export function AppChat() {
{isMultiPane && !isZenMode && (
<PaneTitleOverlay
paneIdx={idx}
conversationId={_paneConversationId}
isFocused={_paneIsFocused}
/>
+126 -28
View File
@@ -1,50 +1,148 @@
import * as React from 'react';
import { Sheet } from '@mui/joy';
import { Box, IconButton, Sheet } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { useConversationTitle } from '~/common/stores/chat/hooks/useConversationTitle';
import { chatPanesActions } from './panes/store-panes-manager';
const _style = {
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 10,
p: '1px 1rem 4px',
fontSize: 'sm',
fontWeight: 'md',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
// boxShadow: 'xs',
// border: '1px solid',
// borderColor: 'background.popup',
borderTop: 'none',
maxWidth: '78%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
const _styles = {
tileBar: {
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 10,
padding: '0 0.125rem 0.125rem',
fontSize: 'sm',
fontWeight: 'md',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
// boxShadow: 'xs',
// border: '1px solid',
// borderColor: 'background.popup',
borderTop: 'none',
maxWidth: '78%',
display: 'flex',
alignItems: 'center',
gap: 1,
} as const,
title: {
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
} as const,
toolButton: {
'--IconButton-size': '1.5rem',
backgroundColor: 'transparent',
} as const,
toolIconLg: {
fontSize: 'lg',
} as const,
} as const;
export function PaneTitleOverlay(props: { conversationId: DConversationId | null, isFocused: boolean }) {
export function PaneTitleOverlay(props: {
paneIdx: number,
conversationId: DConversationId | null,
isFocused: boolean,
}) {
// state
const [editingTitle, setEditingTitle] = React.useState(false);
// external state
const title = useConversationTitle(props.conversationId);
if (!title || title?.length < 3)
return null;
const { title, setUserTitle } = useConversationTitle(props.conversationId);
// if (!title || title?.length < 3)
// return null;
// close tabs handlers
const handleCloseThis = React.useCallback(() => {
chatPanesActions().removePane(props.paneIdx);
}, [props.paneIdx]);
const handleCloseOthers = React.useCallback(() => {
chatPanesActions().removeOtherPanes(props.paneIdx);
}, [props.paneIdx]);
// title handles
const handleTitleEditBegin = React.useCallback(() => {
setEditingTitle(true);
}, []);
const handleTitleEditChange = React.useCallback((newTitle: string) => {
setUserTitle(newTitle);
setEditingTitle(false);
}, [setUserTitle]);
const handleTitleEditEnd = React.useCallback(() => {
setEditingTitle(false);
}, []);
// don't render if not focused
// if (!props.isFocused)
// return null;
const hasTitle = title && title.length > 0;
const color = props.isFocused ? 'primary' : 'neutral';
const variantO = props.isFocused ? 'solid' : 'outlined';
const variantP = props.isFocused ? 'solid' : 'plain';
return (
<Sheet
color={props.isFocused ? 'primary' : 'neutral'}
variant={props.isFocused ? 'solid' : 'outlined'}
sx={_style}
color={color}
variant={variantO}
sx={_styles.tileBar}
>
{title}
{/* Close Others*/}
{/*<TooltipOutlined title='Close Other Tabs'>*/}
{!editingTitle && <IconButton title='Close Other Tabs' size='sm' color={color} variant={variantP} onClick={handleCloseOthers} sx={_styles.toolButton}>
<OpenInFullIcon />
</IconButton>}
{/*</TooltipOutlined>*/}
{/* Title */}
{editingTitle ? (
<InlineTextarea
initialText={title || ''}
placeholder='Chat title...'
invertedColors
centerText
onEdit={handleTitleEditChange}
onCancel={handleTitleEditEnd}
sx={{
// flexGrow: 1,
// minWidth: 120,
mx: { md: 1 },
}}
/>
) : hasTitle ? (
<Box sx={_styles.title} onDoubleClick={handleTitleEditBegin}>
{title}
</Box>
) : !!props.conversationId && (
<IconButton title='Edit Chat Title' size='sm' color={color} variant={variantP} onClick={handleTitleEditBegin} sx={_styles.toolButton}>
<EditRoundedIcon />
</IconButton>
)}
{/* Close This */}
{/*<TooltipOutlined title='Close'>*/}
{!editingTitle && <IconButton title='Close Tab' size='sm' color={color} variant={variantP} onClick={handleCloseThis} sx={_styles.toolButton}>
<ClearIcon sx={_styles.toolIconLg} />
</IconButton>}
{/*</TooltipOutlined>*/}
</Sheet>
);
}
@@ -1,12 +1,25 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { conversationTitle, DConversationId } from '../chat.conversation';
import { useChatStore } from '../store-chats';
export function useConversationTitle(conversationId: DConversationId | null) {
return useChatStore(useShallow(({ conversations }) => {
export function useConversationTitle(conversationId: DConversationId | null, fallbackTitle?: string) {
// react to the title
const { title, setUserTitle: storeSetUserTitle } = useChatStore(useShallow(({ conversations, setUserTitle }) => {
const conversation = conversationId ? conversations.find(_c => _c.id === conversationId) : null;
return conversation ? conversationTitle(conversation) : null;
return {
title: conversation ? conversationTitle(conversation, fallbackTitle) : null,
setUserTitle,
};
}));
// closure to set the title
const setUserTitle = React.useCallback((newTitle: string) => {
conversationId && storeSetUserTitle(conversationId, newTitle);
}, [conversationId, storeSetUserTitle]);
return { title, setUserTitle };
}