MP: improve text part addition

This commit is contained in:
Enrico Ros
2024-06-20 23:34:42 -07:00
parent 3802123147
commit 2207405ebc
4 changed files with 67 additions and 33 deletions
@@ -127,6 +127,10 @@ export function ChatMessageList(props: {
props.conversationHandler?.messagesDelete([messageId]);
}, [props.conversationHandler]);
const handleMessageAppendFragment = React.useCallback((messageId: DMessageId, fragment: DMessageFragment) => {
props.conversationHandler?.messageFragmentAppend(messageId, fragment, false, false);
}, [props.conversationHandler]);
const handleMessageDeleteFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
props.conversationHandler?.messageFragmentDelete(messageId, fragmentId, false, true);
}, [props.conversationHandler]);
@@ -286,6 +290,7 @@ export function ChatMessageList(props: {
onMessageBeam={handleMessageBeam}
onMessageBranch={handleMessageBranch}
onMessageDelete={handleMessageDelete}
onMessageFragmentAppend={handleMessageAppendFragment}
onMessageFragmentDelete={handleMessageDeleteFragment}
onMessageFragmentReplace={handleMessageReplaceFragment}
onMessageToggleUserFlag={handleMessageToggleUserFlag}
@@ -3,7 +3,7 @@ import { useShallow } from 'zustand/react/shallow';
import TimeAgo from 'react-timeago';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, ButtonGroup, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
import { Box, Button, ButtonGroup, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
import { ClickAwayListener, Popper } from '@mui/base';
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
@@ -33,10 +33,10 @@ import { copyToClipboard } from '~/common/util/clipboardUtils';
import { prettyBaseModel } from '~/common/util/modelUtils';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { AttachmentFragments } from './fragments-attachment-text/TextAttachmentFragments';
import { ContentFragments } from './fragments-content/ContentFragments';
import { ImageAttachmentFragments } from './fragments-attachment-image/ImageAttachmentFragments';
import { ReplyToBubble } from './ReplyToBubble';
import { TextAttachmentFragments } from './fragments-attachment-text/TextAttachmentFragments';
import { avatarIconSx, makeMessageAvatar, messageBackground, personaColumnSx } from './messageUtils';
import { useChatShowTextDiff } from '../../store-app-chat';
@@ -79,14 +79,15 @@ export function ChatMessage(props: {
onMessageBeam?: (messageId: string) => Promise<void>,
onMessageBranch?: (messageId: string) => void,
onMessageDelete?: (messageId: string) => void,
onMessageFragmentAppend?: (messageId: DMessageId, fragment: DMessageFragment) => void
onMessageFragmentDelete?: (messageId: DMessageId, fragmentId: DMessageFragmentId) => void,
onMessageFragmentReplace?: (messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => void,
onMessageToggleUserFlag?: (messageId: string, flag: DMessageUserFlag) => void,
onMessageTruncate?: (messageId: string) => void,
onReplyTo?: (messageId: string, selectedText: string) => void,
onTextDiagram?: (messageId: string, text: string) => Promise<void>
onTextImagine?: (text: string) => Promise<void>
onTextSpeak?: (text: string) => Promise<void>
onTextDiagram?: (messageId: string, text: string) => Promise<void>,
onTextImagine?: (text: string) => Promise<void>,
onTextSpeak?: (text: string) => Promise<void>,
sx?: SxProps,
}) {
@@ -125,7 +126,7 @@ export function ChatMessage(props: {
} = props.message;
// split the fragments: image attachments are first, then content fragments, then other attachment fragments
const [contentFragments, imageAttachments, otherAttachments] = classifyMessageFragments(messageFragments);
const [contentFragments, imageAttachments, nonImageAttachments] = classifyMessageFragments(messageFragments);
const isUserStarred = messageHasUserFlag(props.message, 'starred');
@@ -143,7 +144,11 @@ export function ChatMessage(props: {
// const textDiffs = useSanityTextDiffs(messageText, props.diffPreviousText, showDiff);
const { onMessageFragmentDelete, onMessageFragmentReplace } = props;
const { onMessageFragmentAppend, onMessageFragmentDelete, onMessageFragmentReplace } = props;
const handleFragmentNew = React.useCallback(() => {
onMessageFragmentAppend?.(messageId, createTextContentFragment(''));
}, [messageId, onMessageFragmentAppend]);
const handleFragmentDelete = React.useCallback((fragmentId: DMessageFragmentId) => {
onMessageFragmentDelete?.(messageId, fragmentId);
@@ -535,11 +540,22 @@ export function ChatMessage(props: {
<ImageAttachmentFragments
imageAttachments={imageAttachments}
contentScaling={contentScaling}
messageRole={messageRole}
isMobile={props.isMobile}
onFragmentDelete={handleFragmentDelete}
/>
)}
{/* If editing and there's no content, have a button to create a new TextContentFragment */}
{isEditingText && !contentFragments.length && (
<Button variant='plain' color='neutral' onClick={handleFragmentNew} sx={{
ml: fromAssistant ? undefined : 'auto',
mr: fromAssistant ? 'auto' : undefined,
}}>
Add text
</Button>
)}
{/* Content Fragments (iterating all to preserve the index) */}
<ContentFragments
fragments={contentFragments}
@@ -567,13 +583,15 @@ export function ChatMessage(props: {
/>
{/* Attachment Fragments */}
{/*{hasAttachments && (*/}
<AttachmentFragments
attachmentFragments={otherAttachments}
messageRole={messageRole}
contentScaling={contentScaling}
/>
{/*)}*/}
{nonImageAttachments.length >= 1 && (
<TextAttachmentFragments
textFragments={nonImageAttachments}
messageRole={messageRole}
contentScaling={contentScaling}
isMobile={props.isMobile}
onFragmentDelete={handleFragmentDelete}
/>
)}
{/* Reply-To Bubble */}
{!!messageMetadata?.inReplyToText && (
@@ -3,7 +3,7 @@ import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box } from '@mui/joy';
import type { DMessageAttachmentFragment, DMessageFragmentId } from '~/common/stores/chat/chat.message';
import type { DMessageAttachmentFragment, DMessageFragmentId, DMessageRole } from '~/common/stores/chat/chat.message';
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
import { ContentPartImageRefDBlob, showImageDataRefInNewTab } from '../fragments-content/ContentPartImageRef';
@@ -23,11 +23,8 @@ const layoutSx: SxProps = {
// layout
display: 'flex',
flexWrap: 'wrap',
// alignItems: 'center',
justifyContent: 'flex-end',
// display: 'grid',
// gridTemplateColumns: 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',
// alignItems: 'center', // commented to keep them to the top
// justifyContent: 'flex-end', // commented as we do it dynamically
gap: { xs: 0.5, md: 1 },
};
@@ -48,8 +45,6 @@ const imageSheetPatchSx: SxProps = {
// override the style in RenderImageURL
maxWidth: CARD_MAX_WIDTH, // very important to keep the aspect ratio
maxHeight: CARD_MAX_HEIGHT, // very important to keep the aspect ratio
// style
// width: '100%',
// height: '100%',
// objectFit: 'cover',
@@ -58,18 +53,23 @@ const imageSheetPatchSx: SxProps = {
/**
* Shows image attachments in a Grid (responsive), similarly to
* Shows image attachments in a flexbox that wraps the images (overflowing by rows)
* Also see `TextAttachmentFragments` for the text version, and 'ContentFragments'.
*/
export function ImageAttachmentFragments(props: {
imageAttachments: DMessageAttachmentFragment[],
contentScaling: ContentScaling,
messageRole: DMessageRole,
isMobile?: boolean,
onFragmentDelete: (fragmentId: DMessageFragmentId) => void,
}) {
const layoutSxMemo = React.useMemo((): SxProps => ({
...layoutSx,
justifyContent: props.messageRole === 'assistant' ? 'flex-start' : 'flex-end',
}), [props.messageRole]);
// memo the scaled image style
const scaledImageCardSx = React.useMemo((): SxProps => ({
const cardStyleSxMemo = React.useMemo((): SxProps => ({
fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined,
lineHeight: themeScalingMap[props.contentScaling]?.blockLineHeight ?? 1.75,
...imageSheetPatchSx,
@@ -77,7 +77,7 @@ export function ImageAttachmentFragments(props: {
return (
<Box aria-label={`${props.imageAttachments.length} image(s)`} sx={layoutSx}>
<Box aria-label={`${props.imageAttachments.length} image(s)`} sx={layoutSxMemo}>
{/* render each image attachment */}
{props.imageAttachments.map(attachmentFragment => {
@@ -99,7 +99,7 @@ export function ImageAttachmentFragments(props: {
imageHeight={imageRefPart.height}
onOpenInNewTab={() => showImageDataRefInNewTab(dataRef)}
onDeleteFragment={() => props.onFragmentDelete(attachmentFragment.fId)}
scaledImageSx={scaledImageCardSx}
scaledImageSx={cardStyleSxMemo}
variant='attachment-card'
/>
);
@@ -1,28 +1,38 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box } from '@mui/joy';
import type { ContentScaling } from '~/common/app.theme';
import type { DMessageAttachmentFragment, DMessageRole } from '~/common/stores/chat/chat.message';
import type { DMessageAttachmentFragment, DMessageFragmentId, DMessageRole } from '~/common/stores/chat/chat.message';
import { ContentPartPlaceholder } from '../fragments-content/ContentPartPlaceholder';
const layoutSx: SxProps = {};
/**
* Displays a list of 'cards' which are buttons with a mutually exclusive active state.
* When one is active, there is a content part just right under (with the collapse mechanism in case it's a user role).
* If one is clicked the content part (use ContentFragments with a single Fragment) is displayed.
*/
export function AttachmentFragments(props: {
attachmentFragments: DMessageAttachmentFragment[],
export function TextAttachmentFragments(props: {
textFragments: DMessageAttachmentFragment[],
messageRole: DMessageRole,
contentScaling: ContentScaling,
isMobile?: boolean,
onFragmentDelete: (fragmentId: DMessageFragmentId) => void,
}) {
if (!props.attachmentFragments.length)
if (!props.textFragments.length)
return null;
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{props.attachmentFragments.map((fragment, attachmentNumber) => (
<Box aria-label={`${props.textFragments.length} image(s)`} sx={layoutSx}>
{/* render each text attachment */}
{props.textFragments.map((fragment, attachmentNumber) => (
<ContentPartPlaceholder
key={'attachment-part-' + attachmentNumber}
placeholderText={`Attachment Placeholder: ${fragment.part.pt}`}
@@ -30,6 +40,7 @@ export function AttachmentFragments(props: {
contentScaling={props.contentScaling}
/>
))}
</Box>
);
}