mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Chat: option to regenerate images in-place
This commit is contained in:
@@ -624,7 +624,9 @@ export function ChatMessage(props: {
|
||||
<ContentPartImageRef
|
||||
key={'image-part-' + fragmentIndex}
|
||||
imageRefPart={fragment.part}
|
||||
fragmentIndex={fragmentIndex}
|
||||
contentScaling={contentScaling}
|
||||
onFragmentEdit={handleFragmentEdit}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -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<DBlobImageAsset>(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: {
|
||||
<ContentPartImageDBlob
|
||||
dataRefDBlobAssetId={dataRef.dblobAssetId}
|
||||
dataRefMimeType={dataRef.mimeType}
|
||||
imageAltText={imagePart.altText}
|
||||
imageWidth={imagePart.width}
|
||||
imageHeight={imagePart.height}
|
||||
imageAltText={imageRefPart.altText}
|
||||
imageWidth={imageRefPart.width}
|
||||
imageHeight={imageRefPart.height}
|
||||
onImageReplace={handleImageReplace}
|
||||
onOpenInNewTab={handleOpenInNewTab}
|
||||
scaledImageSx={scaledImageSx}
|
||||
/>
|
||||
) : dataRef.reftype === 'url' ? (
|
||||
<RenderImageURL
|
||||
imageURL={dataRef.url}
|
||||
infoText={imagePart.altText}
|
||||
infoText={imageRefPart.altText}
|
||||
scaledImageSx={scaledImageSx}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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<string, any> };
|
||||
type DMessageToolResponsePart = { pt: 'tool_response', function: string, response: Record<string, any> };
|
||||
// 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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export const BlocksRenderer = React.forwardRef<HTMLDivElement, BlocksRendererPro
|
||||
: block.type === 'codeb'
|
||||
? <RenderCodeMemoOrNot key={'code-' + index} codeBlock={block} fitScreen={props.fitScreen} initialShowHTML={props.showUnsafeHtml} noCopyButton={props.specialDiagramMode} optimizeLightweight={!optimizeSubBlockWithMemo} sx={scaledCodeSx} />
|
||||
: block.type === 'imageb'
|
||||
? <RenderImageURL key={'image-' + index} imageURL={block.url} infoText={block.alt} onRunAgain={/*props.isBottom ? props.onImageRegenerate :*/ undefined} scaledImageSx={scaledImageSx} />
|
||||
? <RenderImageURL key={'image-' + index} imageURL={block.url} infoText={block.alt} onImageRegenerate={/*props.isBottom ? props.onImageRegenerate :*/ undefined} scaledImageSx={scaledImageSx} />
|
||||
: block.type === 'diffb'
|
||||
? <RenderTextDiff key={'text-diff-' + index} diffBlock={block} sx={scaledTypographySx} />
|
||||
: (props.renderTextAsMarkdown && !fromSystem && !isUserCommand)
|
||||
|
||||
@@ -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 */}
|
||||
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, pt: 0.5, px: 0.5, gap: 0.5 }}>
|
||||
{!!props.onRunAgain && (
|
||||
<Box className='overlay-buttons' sx={{
|
||||
...overlayButtonsSx,
|
||||
p: 0.5,
|
||||
display: 'grid',
|
||||
gap: 0.5,
|
||||
}}>
|
||||
{!!props.onImageRegenerate && (
|
||||
<GoodTooltip title='Draw again'>
|
||||
<OverlayButton variant='outlined' onClick={props.onRunAgain}>
|
||||
<OverlayButton variant='outlined' onClick={props.onImageRegenerate} sx={{ gridRow: '2', gridColumn: '2' }}>
|
||||
<ReplayIcon />
|
||||
</OverlayButton>
|
||||
</GoodTooltip>
|
||||
|
||||
Reference in New Issue
Block a user