mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Panes: Edit Title, close, close others
This commit is contained in:
@@ -665,6 +665,7 @@ export function AppChat() {
|
||||
|
||||
{isMultiPane && !isZenMode && (
|
||||
<PaneTitleOverlay
|
||||
paneIdx={idx}
|
||||
conversationId={_paneConversationId}
|
||||
isFocused={_paneIsFocused}
|
||||
/>
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user