diff --git a/src/apps/chat/components/ChatMessageList.tsx b/src/apps/chat/components/ChatMessageList.tsx index aaea02d91..4fdc95eb4 100644 --- a/src/apps/chat/components/ChatMessageList.tsx +++ b/src/apps/chat/components/ChatMessageList.tsx @@ -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} diff --git a/src/apps/chat/components/message/ChatMessage.tsx b/src/apps/chat/components/message/ChatMessage.tsx index 394c063cc..0625b3015 100644 --- a/src/apps/chat/components/message/ChatMessage.tsx +++ b/src/apps/chat/components/message/ChatMessage.tsx @@ -175,6 +175,7 @@ export function ChatMessage(props: { onMessageBeam?: (messageId: string) => Promise, 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} /> ); diff --git a/src/apps/chat/components/message/ContentPartImageRef.tsx b/src/apps/chat/components/message/ContentPartImageRef.tsx index e56c5d9e8..8ec5c4eea 100644 --- a/src/apps/chat/components/message/ContentPartImageRef.tsx +++ b/src/apps/chat/components/message/ContentPartImageRef.tsx @@ -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' ? ( diff --git a/src/modules/blocks/RenderImageURL.tsx b/src/modules/blocks/RenderImageURL.tsx index 53ae2c736..bb17a1ef7 100644 --- a/src/modules/blocks/RenderImageURL.tsx +++ b/src/modules/blocks/RenderImageURL.tsx @@ -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: { )} - {/* Regenerate [armed, arming] buttons */} - {regenArmed && ( - - - + + {/* Deletion */} + + {!!onImageDelete && !regenArmed && ( + + + {deleteArmed ? : } )} - {!!onImageRegenerate && ( + {deleteArmed && !regenArmed && ( + + + + + + )} + + {/* Regenerate [armed, arming] buttons */} + {regenArmed && !deleteArmed && ( + + + + + + )} + + {!!onImageRegenerate && !deleteArmed && ( {regenArmed