diff --git a/src/apps/chat/components/message/ChatMessage.tsx b/src/apps/chat/components/message/ChatMessage.tsx index c08213d18..eb7e0d21a 100644 --- a/src/apps/chat/components/message/ChatMessage.tsx +++ b/src/apps/chat/components/message/ChatMessage.tsx @@ -624,7 +624,9 @@ export function ChatMessage(props: { ); diff --git a/src/apps/chat/components/message/ContentPartImageRef.tsx b/src/apps/chat/components/message/ContentPartImageRef.tsx index 09977030e..53501838b 100644 --- a/src/apps/chat/components/message/ContentPartImageRef.tsx +++ b/src/apps/chat/components/message/ContentPartImageRef.tsx @@ -9,7 +9,7 @@ import { RenderImageURL } from '~/modules/blocks/RenderImageURL'; import { blocksRendererSx } from '~/modules/blocks/BlocksRenderer'; import { useDBAsset } from '~/modules/dblobs/dblobs.hooks'; -import type { DMessageImagePart } from '~/common/stores/chat/chat.message'; +import type { DMessageContentFragment, DMessageImageRefPart } from '~/common/stores/chat/chat.message'; import { ContentScaling, themeScalingMap } from '~/common/app.theme'; import { showImageDataRefInNewTab } from '~/common/stores/chat/chat.dblobs'; @@ -20,6 +20,7 @@ function ContentPartImageDBlob(props: { imageAltText?: string, imageWidth?: number, imageHeight?: number, + onImageReplace: (newImageFragment: DMessageContentFragment) => void, onOpenInNewTab: () => void scaledImageSx?: SxProps, }) { @@ -27,6 +28,27 @@ function ContentPartImageDBlob(props: { // external state from the DB const [imageItem] = useDBAsset(props.dataRefDBlobAssetId); + // handlers + + const { label: imageItemLabel, origin: imageItemOrigin, metadata: imageItemMetadata } = imageItem || {}; + const recreationPrompt = ((imageItemOrigin?.ot === 'generated') ? imageItemOrigin.prompt : undefined) || imageItemLabel || props.imageAltText; + const recreationWidth = imageItemMetadata?.width || props.imageWidth; + const recreationHeight = imageItemMetadata?.height || props.imageHeight; + + const handleImageRegenerate = React.useCallback(() => { + // TODO: ... t2iGenerateImagesOrThrow() + console.log('ContentPartImageDBlob: handleImageRegenerate: notImplemented', imageItem, recreationPrompt, recreationWidth, recreationHeight); + + // props.onImageReplace( createImageContentFragment() + // { + // type: 'image', + // dataRef: { reftype: 'dblob', dblobAssetId: props.dataRefDBlobAssetId }, + // altText: props.imageAltText, + // width: props.imageWidth, + // height: props.imageHeight, + // }); + }, [imageItem, recreationPrompt, recreationWidth, recreationHeight]); + // memo the description and overlay text const { dataUrl, altText, overlayText } = React.useMemo(() => { if (!imageItem?.data) @@ -78,6 +100,7 @@ function ContentPartImageDBlob(props: { infoText={altText} description={overlayText} onOpenInNewTab={props.onOpenInNewTab} + onImageRegenerate={(!!recreationPrompt) ? handleImageRegenerate : undefined} scaledImageSx={props.scaledImageSx} /> ); @@ -85,16 +108,25 @@ function ContentPartImageDBlob(props: { export function ContentPartImageRef(props: { - imageRefPart: DMessageImagePart, + imageRefPart: DMessageImageRefPart, + fragmentIndex: number, contentScaling: ContentScaling, + onFragmentEdit?: (fragmentIndex: number, newFragment: DMessageContentFragment) => void, }) { // derived state - const imagePart = props.imageRefPart; - const { dataRef } = imagePart; + const { fragmentIndex, imageRefPart, onFragmentEdit } = props; + const { dataRef } = imageRefPart; // event handlers - const handleOpenInNewTab = React.useCallback(() => showImageDataRefInNewTab(dataRef), [dataRef]); + const handleImageReplace = React.useCallback((newImageFragment: DMessageContentFragment) => { + onFragmentEdit?.(fragmentIndex, newImageFragment); + }, [onFragmentEdit, fragmentIndex]); + + const handleOpenInNewTab = React.useCallback(() => { + void showImageDataRefInNewTab(dataRef); // fire/forget + }, [dataRef]); + // memo the scaled image style const scaledImageSx = React.useMemo((): SxProps => ( @@ -112,16 +144,17 @@ export function ContentPartImageRef(props: { ) : dataRef.reftype === 'url' ? ( ) : ( diff --git a/src/common/stores/chat/chat.message.ts b/src/common/stores/chat/chat.message.ts index f9ab1af3c..934c9ad30 100644 --- a/src/common/stores/chat/chat.message.ts +++ b/src/common/stores/chat/chat.message.ts @@ -52,14 +52,14 @@ export type DMessageFragment = // expected a list of one or more per message, of similar or different types export type DMessageContentFragment = { ft: 'content', - part: DMessageTextPart | DMessageImagePart | DMessageToolCallPart | DMessageToolResponsePart; + part: DMessageTextPart | DMessageImageRefPart | DMessageToolCallPart | DMessageToolResponsePart; } // displayed at the bottom of the message, zero or more export type DMessageAttachmentFragment = { ft: 'attachment', title: string; - part: DMessageTextPart | DMessageImagePart; + part: DMessageTextPart | DMessageImageRefPart; } // up to 1 per message, containing the Rays and Merges that would be used to restore the Beam state - could be volatile (omitted at save) @@ -74,7 +74,7 @@ export type DMessageAttachmentFragment = { // - small and efficient (larger objects need to only be referred to) export type DMessageTextPart = { pt: 'text', text: string }; -export type DMessageImagePart = { pt: 'image_ref', dataRef: DMessageDataRef, altText?: string, width?: number, height?: number }; +export type DMessageImageRefPart = { pt: 'image_ref', dataRef: DMessageDataRef, altText?: string, width?: number, height?: number }; type DMessageToolCallPart = { pt: 'tool_call', function: string, args: Record }; type DMessageToolResponsePart = { pt: 'tool_response', function: string, response: Record }; // type DMessageErrorPart = { pt: 'error', error: string }; @@ -186,7 +186,7 @@ function createDMessageTextPart(text: string): DMessageTextPart { return { pt: 'text', text }; } -function createDMessageImagePart(dataRef: DMessageDataRef, altText?: string, width?: number, height?: number): DMessageImagePart { +function createDMessageImagePart(dataRef: DMessageDataRef, altText?: string, width?: number, height?: number): DMessageImageRefPart { return { pt: 'image_ref', dataRef, altText, width, height }; } diff --git a/src/modules/blocks/BlocksRenderer.tsx b/src/modules/blocks/BlocksRenderer.tsx index 3b6985cf2..c6c80d04f 100644 --- a/src/modules/blocks/BlocksRenderer.tsx +++ b/src/modules/blocks/BlocksRenderer.tsx @@ -194,7 +194,7 @@ export const BlocksRenderer = React.forwardRef : block.type === 'imageb' - ? + ? : block.type === 'diffb' ? : (props.renderTextAsMarkdown && !fromSystem && !isUserCommand) diff --git a/src/modules/blocks/RenderImageURL.tsx b/src/modules/blocks/RenderImageURL.tsx index 1b7b9a69a..81d9de220 100644 --- a/src/modules/blocks/RenderImageURL.tsx +++ b/src/modules/blocks/RenderImageURL.tsx @@ -49,6 +49,7 @@ const prodiaUrlRegex = /^(https?:\/\/images\.prodia\.\S+)$/i; /** * Legacy heuristic for detecting images from "images.prodia." URLs. + * @deprecated Remove in mid 2024 */ export function heuristicLegacyImageBlocks(fullText: string): ImageBlock[] | null { @@ -75,7 +76,7 @@ export const RenderImageURL = (props: { description?: React.ReactNode, infoText?: string, onOpenInNewTab?: (e: React.MouseEvent) => void, - onRunAgain?: (e: React.MouseEvent) => void, + onImageRegenerate?: (e: React.MouseEvent) => void, scaledImageSx?: SxProps, }) => { @@ -179,10 +180,15 @@ export const RenderImageURL = (props: { )} {/* (overlay) Image Buttons */} - - {!!props.onRunAgain && ( + + {!!props.onImageRegenerate && ( - +