MP: Image parts support deletion

This commit is contained in:
Enrico Ros
2024-06-17 19:48:37 -07:00
parent fb4c05f698
commit 25740ae13c
4 changed files with 66 additions and 14 deletions
@@ -127,6 +127,10 @@ export function ChatMessageList(props: {
props.conversationHandler?.messagesDelete([messageId]);
}, [props.conversationHandler]);
const handleMessageDeleteFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
props.conversationHandler?.messageFragmentDelete(messageId, fragmentId, false, true);
}, [props.conversationHandler]);
const handleMessageReplaceFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => {
props.conversationHandler?.messageFragmentReplace(messageId, fragmentId, newFragment, false);
}, [props.conversationHandler]);
@@ -282,6 +286,7 @@ export function ChatMessageList(props: {
onMessageBeam={handleMessageBeam}
onMessageBranch={handleMessageBranch}
onMessageDelete={handleMessageDelete}
onMessageFragmentDelete={handleMessageDeleteFragment}
onMessageFragmentReplace={handleMessageReplaceFragment}
onMessageToggleUserFlag={handleMessageToggleUserFlag}
onMessageTruncate={handleMessageTruncate}
@@ -175,6 +175,7 @@ export function ChatMessage(props: {
onMessageBeam?: (messageId: string) => Promise<void>,
onMessageBranch?: (messageId: string) => void,
onMessageDelete?: (messageId: string) => 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,
@@ -239,7 +240,12 @@ export function ChatMessage(props: {
// const textDiffs = useSanityTextDiffs(messageText, props.diffPreviousText, showDiff);
const { onMessageFragmentReplace } = props;
const { onMessageFragmentDelete, onMessageFragmentReplace } = props;
const handleFragmentDelete = React.useCallback((fragmentId: DMessageFragmentId) => {
setIsEditing(false);
onMessageFragmentDelete?.(messageId, fragmentId);
}, [messageId, onMessageFragmentDelete]);
const handleFragmentReplace = React.useCallback((fragmentId: DMessageFragmentId, newContent: DMessageContentFragment) => {
setIsEditing(false);
@@ -616,6 +622,7 @@ export function ChatMessage(props: {
imageRefPart={fragment.part}
fragmentId={fragment.fId}
contentScaling={contentScaling}
onFragmentDelete={messageFragments.length > 1 ? handleFragmentDelete : undefined}
onFragmentReplace={handleFragmentReplace}
/>
);
@@ -40,7 +40,8 @@ function ContentPartImageRefDBlob(props: {
imageWidth?: number,
imageHeight?: number,
onOpenInNewTab: () => void
onReplaceFragment: (newFragment: DMessageContentFragment) => void,
onDeleteFragment?: () => void,
onReplaceFragment?: (newFragment: DMessageContentFragment) => void,
scaledImageSx?: SxProps,
}) {
@@ -58,10 +59,10 @@ function ContentPartImageRefDBlob(props: {
enabled: false,
queryKey: ['regen-image-asset', props.dataRefDBlobAssetId, recreationPrompt],
queryFn: async ({ signal }) => {
if (signal?.aborted || !recreationPrompt) return;
if (signal?.aborted || !recreationPrompt || !props.onReplaceFragment) return;
const newImageFragments = await t2iGenerateImageContentFragments(null, recreationPrompt, 1, 'global', 'app-chat');
if (newImageFragments.length === 1)
props.onReplaceFragment(newImageFragments[0]);
props.onReplaceFragment?.(newImageFragments[0]);
},
});
@@ -116,7 +117,8 @@ function ContentPartImageRefDBlob(props: {
infoText={altText}
description={overlayText}
onOpenInNewTab={props.onOpenInNewTab}
onImageRegenerate={(!!recreationPrompt && !isRegenerating) ? handleImageRegenerate : undefined}
onImageDelete={props.onDeleteFragment}
onImageRegenerate={(!!recreationPrompt && !isRegenerating && !!props.onReplaceFragment) ? handleImageRegenerate : undefined}
className={isRegenerating ? 'agi-border-4' : undefined}
scaledImageSx={props.scaledImageSx}
/>
@@ -139,14 +141,19 @@ export function ContentPartImageRef(props: {
imageRefPart: DMessageImageRefPart,
fragmentId: DMessageFragmentId,
contentScaling: ContentScaling,
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
}) {
// derived state
const { fragmentId, imageRefPart, onFragmentReplace } = props;
const { fragmentId, imageRefPart, onFragmentDelete, onFragmentReplace } = props;
const { dataRef } = imageRefPart;
// event handlers
const handleDeleteFragment = React.useCallback(() => {
onFragmentDelete?.(fragmentId);
}, [fragmentId, onFragmentDelete]);
const handleReplaceFragment = React.useCallback((newImageFragment: DMessageContentFragment) => {
onFragmentReplace?.(fragmentId, newImageFragment);
}, [fragmentId, onFragmentReplace]);
@@ -174,7 +181,8 @@ export function ContentPartImageRef(props: {
imageWidth={imageRefPart.width}
imageHeight={imageRefPart.height}
onOpenInNewTab={handleOpenInNewTab}
onReplaceFragment={handleReplaceFragment}
onDeleteFragment={onFragmentDelete ? handleDeleteFragment : undefined}
onReplaceFragment={onFragmentReplace ? handleReplaceFragment : undefined}
scaledImageSx={scaledImageSx}
/>
) : dataRef.reftype === 'url' ? (
+39 -7
View File
@@ -3,6 +3,8 @@ import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Alert, Box, IconButton, Sheet } from '@mui/joy';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ReplayIcon from '@mui/icons-material/Replay';
@@ -76,6 +78,7 @@ export const RenderImageURL = (props: {
description?: React.ReactNode,
infoText?: string,
onOpenInNewTab?: (e: React.MouseEvent) => void,
onImageDelete?: () => void,
onImageRegenerate?: () => void,
scaledImageSx?: SxProps,
className?: string,
@@ -84,6 +87,7 @@ export const RenderImageURL = (props: {
// state
const [infoOpen, setInfoOpen] = React.useState(false);
const [loadingTimeout, setLoadingTimeout] = React.useState(false);
const [deleteArmed, setDeleteArmed] = React.useState(false);
const [regenArmed, setRegenArmed] = React.useState(false);
const [showDalleAlert, setShowDalleAlert] = React.useState(true);
@@ -94,24 +98,33 @@ export const RenderImageURL = (props: {
}, []);
// handlers
const { onImageRegenerate, onOpenInNewTab } = props;
const { onImageDelete, onImageRegenerate, onOpenInNewTab } = props;
const handleToggleInfoOpen = React.useCallback(() => {
setDeleteArmed(false);
setRegenArmed(false);
setInfoOpen(open => !open);
}, []);
const handleOpenInNewTab = React.useCallback((e: React.MouseEvent) => {
setDeleteArmed(false);
setRegenArmed(false);
onOpenInNewTab?.(e);
}, [onOpenInNewTab]);
const handleToggleDeleteArmed = React.useCallback(() => {
setRegenArmed(false);
setDeleteArmed(armed => !armed);
}, []);
const handleImageRegenerate = React.useCallback(() => {
setDeleteArmed(false);
setRegenArmed(false);
onImageRegenerate?.();
}, [onImageRegenerate]);
const handleToggleRegenArmed = React.useCallback(() => {
setDeleteArmed(false);
setRegenArmed(armed => !armed);
}, []);
@@ -235,16 +248,35 @@ export const RenderImageURL = (props: {
</GoodTooltip>
)}
{/* Regenerate [armed, arming] buttons */}
{regenArmed && (
<GoodTooltip title='Confirm Regeneration'>
<OverlayButton variant='soft' color='success' onClick={handleImageRegenerate} sx={{ gridRow: '2', gridColumn: '1' }}>
<ReplayIcon />
{/* Deletion */}
{!!onImageDelete && !regenArmed && (
<GoodTooltip title={deleteArmed ? 'Cancel Deletion' : 'Delete Image'}>
<OverlayButton variant={deleteArmed ? 'solid' : 'soft'} onClick={handleToggleDeleteArmed} sx={{ gridRow: '2', gridColumn: '1' }}>
{deleteArmed ? <CloseRoundedIcon /> : <DeleteOutlineIcon />}
</OverlayButton>
</GoodTooltip>
)}
{!!onImageRegenerate && (
{deleteArmed && !regenArmed && (
<GoodTooltip title='Confirm Deletion'>
<OverlayButton variant='soft' color='danger' onClick={onImageDelete} sx={{ gridRow: '2', gridColumn: '2' }}>
<DeleteForeverIcon sx={{ color: 'danger.solidBg' }} />
</OverlayButton>
</GoodTooltip>
)}
{/* Regenerate [armed, arming] buttons */}
{regenArmed && !deleteArmed && (
<GoodTooltip title='Confirm Regeneration'>
<OverlayButton variant='soft' color='success' onClick={handleImageRegenerate} sx={{ gridRow: '2', gridColumn: '1' }}>
<ReplayIcon sx={{ color: 'success.solidBg' }} />
</OverlayButton>
</GoodTooltip>
)}
{!!onImageRegenerate && !deleteArmed && (
<GoodTooltip title={regenArmed ? 'Cancel Regeneration' : 'Draw again with the current drawing configuration'}>
<OverlayButton variant={regenArmed ? 'solid' : 'soft'} onClick={handleToggleRegenArmed} sx={{ gridRow: '2', gridColumn: '2' }}>
{regenArmed