mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-11 14:10:15 -07:00
PartImageRefDBlob: extract
This commit is contained in:
@@ -11,7 +11,7 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.message';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
|
||||
import { showImageDataRefInNewTab } from '../../message/fragments-content/ContentPartImageRef';
|
||||
import { showImageDataRefInNewTab } from '../../message/fragments-content/PartImageRefDBlob';
|
||||
|
||||
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
|
||||
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts-slice';
|
||||
|
||||
+2
-1
@@ -6,7 +6,7 @@ import { Box } from '@mui/joy';
|
||||
import type { DMessageAttachmentFragment, DMessageFragmentId, DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
|
||||
import { PartImageRefDBlob, showImageDataRefInNewTab } from '../fragments-content/ContentPartImageRef';
|
||||
import { PartImageRefDBlob, showImageDataRefInNewTab } from '../fragments-content/PartImageRefDBlob';
|
||||
|
||||
|
||||
// configuration
|
||||
@@ -34,6 +34,7 @@ const imageSheetPatchSx: SxProps = {
|
||||
minWidth: CARD_MIN_SQR,
|
||||
minHeight: CARD_MIN_SQR,
|
||||
boxShadow: 'sm',
|
||||
// border: 'none',
|
||||
|
||||
// style
|
||||
// backgroundColor: 'background.popup',
|
||||
|
||||
@@ -1,162 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box } from '@mui/joy';
|
||||
|
||||
import type { DBlobAssetId, DBlobImageAsset } from '~/modules/dblobs/dblobs.types';
|
||||
import { RenderImageURL } from '~/modules/blocks/RenderImageURL';
|
||||
import { blocksRendererSx } from '~/modules/blocks/BlocksRenderer';
|
||||
import { getImageAssetAsBlobURL } from '~/modules/dblobs/dblobs.images';
|
||||
import { t2iGenerateImageContentFragments } from '~/modules/t2i/t2i.client';
|
||||
import { useDBAsset } from '~/modules/dblobs/dblobs.hooks';
|
||||
|
||||
import type { DMessageContentFragment, DMessageDataRef, DMessageFragmentId, DMessageImageRefPart } from '~/common/stores/chat/chat.message';
|
||||
import type { DMessageContentFragment, DMessageFragmentId, DMessageImageRefPart } from '~/common/stores/chat/chat.message';
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
|
||||
|
||||
/**
|
||||
* Opens am image data ref in a new tab (fetches and shows it)
|
||||
*/
|
||||
export async function showImageDataRefInNewTab(dataRef: DMessageDataRef) {
|
||||
let imageUrl: string | null = null;
|
||||
if (dataRef.reftype === 'url')
|
||||
imageUrl = dataRef.url;
|
||||
else if (dataRef.reftype === 'dblob')
|
||||
imageUrl = await getImageAssetAsBlobURL(dataRef.dblobAssetId);
|
||||
if (imageUrl && typeof window !== 'undefined') {
|
||||
window.open(imageUrl, '_blank', 'noopener,noreferrer');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export function PartImageRefDBlob(props: {
|
||||
dataRefDBlobAssetId: DBlobAssetId,
|
||||
dataRefMimeType: string,
|
||||
imageAltText?: string,
|
||||
imageWidth?: number,
|
||||
imageHeight?: number,
|
||||
onOpenInNewTab: () => void
|
||||
onDeleteFragment?: () => void,
|
||||
onReplaceFragment?: (newFragment: DMessageContentFragment) => void,
|
||||
scaledImageSx?: SxProps,
|
||||
partVariant: 'content-part' | 'attachment-card',
|
||||
}) {
|
||||
|
||||
// external state from the DB
|
||||
const [imageItem] = useDBAsset<DBlobImageAsset>(props.dataRefDBlobAssetId);
|
||||
|
||||
|
||||
// hook: async image regeneration
|
||||
|
||||
const { label: imageItemLabel, origin: imageItemOrigin } = imageItem || {};
|
||||
// const _recreationWidth = imageItemMetadata?.width || props.imageWidth;
|
||||
// const _recreationHeight = imageItemMetadata?.height || props.imageHeight;
|
||||
const recreationPrompt = ((imageItemOrigin?.ot === 'generated') ? imageItemOrigin.prompt : undefined) || imageItemLabel || props.imageAltText;
|
||||
|
||||
const { isFetching: isRegenerating, refetch: handleImageRegenerate } = useQuery({
|
||||
enabled: false,
|
||||
queryKey: ['regen-image-asset', props.dataRefDBlobAssetId, recreationPrompt],
|
||||
queryFn: async ({ signal }) => {
|
||||
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]);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// memo the description and overlay text
|
||||
const { dataUrlMemo, altText, overlayText } = React.useMemo(() => {
|
||||
// if no image data, return null
|
||||
if (!imageItem?.data) {
|
||||
return {
|
||||
dataUrlMemo: null,
|
||||
};
|
||||
}
|
||||
|
||||
// [attachment card] only return the data
|
||||
if (props.partVariant === 'attachment-card') {
|
||||
return {
|
||||
dataUrlMemo: `data:${imageItem.data.mimeType};base64,${imageItem.data.base64}`,
|
||||
};
|
||||
}
|
||||
|
||||
let overlayText: React.ReactNode = null;
|
||||
const extension = (imageItem.data.mimeType || props.dataRefMimeType || '').replace('image/', '');
|
||||
const overlayDate = imageItem.updatedAt || imageItem.createdAt || undefined;
|
||||
|
||||
switch (imageItem.origin.ot) {
|
||||
case 'user':
|
||||
overlayText = <Box sx={{ fontSize: '0.875em' }}>
|
||||
{/*" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "*/}
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{imageItem.origin.source} · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension}
|
||||
</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{imageItem.origin.media}{imageItem.origin.fileName ? ' · ' + imageItem.origin.fileName : ''}
|
||||
</Box>
|
||||
{!!overlayDate && <Box sx={{ opacity: 0.5 }}>
|
||||
<TimeAgo date={overlayDate} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
break;
|
||||
|
||||
case 'generated':
|
||||
overlayText = <Box sx={{ fontSize: '0.875em' }}>
|
||||
" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
AI Image · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension}
|
||||
</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{Object.entries(imageItem.origin.parameters).reduce((acc, [key, value]) => {
|
||||
acc.push(`${key}: ${value}`);
|
||||
return acc;
|
||||
}, [] as string[]).join(', ')}
|
||||
</Box>
|
||||
{!!overlayDate && <Box sx={{ opacity: 0.5 }}>
|
||||
<TimeAgo date={overlayDate} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
dataUrlMemo: `data:${imageItem.data.mimeType};base64,${imageItem.data.base64}`,
|
||||
altText: props.imageAltText || imageItem.metadata?.description || imageItem.label || '',
|
||||
overlayText: overlayText,
|
||||
};
|
||||
}, [imageItem, props.dataRefMimeType, props.imageAltText, props.imageHeight, props.imageWidth, props.partVariant]);
|
||||
|
||||
return (
|
||||
<RenderImageURL
|
||||
imageURL={dataUrlMemo}
|
||||
infoText={altText}
|
||||
description={overlayText}
|
||||
onOpenInNewTab={props.onOpenInNewTab}
|
||||
onImageDelete={props.onDeleteFragment}
|
||||
onImageRegenerate={(!!recreationPrompt && !isRegenerating && !!props.onReplaceFragment) ? handleImageRegenerate : undefined}
|
||||
className={isRegenerating ? 'agi-border-4' : undefined}
|
||||
scaledImageSx={props.scaledImageSx}
|
||||
variant={props.partVariant}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function ContentPartImageRefURL(props: { dataRefUrl: string, imageAltText?: string, scaledImageSx?: SxProps, }) {
|
||||
return (
|
||||
<RenderImageURL
|
||||
imageURL={props.dataRefUrl}
|
||||
infoText={props.imageAltText}
|
||||
scaledImageSx={props.scaledImageSx}
|
||||
variant='content-part'
|
||||
/>
|
||||
);
|
||||
}
|
||||
import { PartImageRefDBlob, showImageDataRefInNewTab } from './PartImageRefDBlob';
|
||||
|
||||
|
||||
export function ContentPartImageRef(props: {
|
||||
@@ -209,15 +62,16 @@ export function ContentPartImageRef(props: {
|
||||
partVariant='content-part'
|
||||
/>
|
||||
) : dataRef.reftype === 'url' ? (
|
||||
<ContentPartImageRefURL
|
||||
dataRefUrl={dataRef.url}
|
||||
// ...
|
||||
imageAltText={imageRefPart.altText}
|
||||
// ...
|
||||
<RenderImageURL
|
||||
imageURL={dataRef.url}
|
||||
infoText={imageRefPart.altText}
|
||||
scaledImageSx={scaledImageSx}
|
||||
variant='content-part'
|
||||
/>
|
||||
) : (
|
||||
<Box>ContentPartImageRef: unknown reftype</Box>
|
||||
<Box>
|
||||
ContentPartImageRef: unknown reftype
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box } from '@mui/joy';
|
||||
|
||||
import type { DBlobAssetId, DBlobImageAsset } from '~/modules/dblobs/dblobs.types';
|
||||
import { RenderImageURL } from '~/modules/blocks/RenderImageURL';
|
||||
import { getImageAssetAsBlobURL } from '~/modules/dblobs/dblobs.images';
|
||||
import { t2iGenerateImageContentFragments } from '~/modules/t2i/t2i.client';
|
||||
import { useDBAsset } from '~/modules/dblobs/dblobs.hooks';
|
||||
|
||||
import type { DMessageContentFragment, DMessageDataRef } from '~/common/stores/chat/chat.message';
|
||||
|
||||
|
||||
/**
|
||||
* Opens am image data ref in a new tab (fetches and shows it)
|
||||
*/
|
||||
export async function showImageDataRefInNewTab(dataRef: DMessageDataRef) {
|
||||
let imageUrl: string | null = null;
|
||||
if (dataRef.reftype === 'url')
|
||||
imageUrl = dataRef.url;
|
||||
else if (dataRef.reftype === 'dblob')
|
||||
imageUrl = await getImageAssetAsBlobURL(dataRef.dblobAssetId);
|
||||
if (imageUrl && typeof window !== 'undefined') {
|
||||
window.open(imageUrl, '_blank', 'noopener,noreferrer');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
export function PartImageRefDBlob(props: {
|
||||
dataRefDBlobAssetId: DBlobAssetId,
|
||||
dataRefMimeType: string,
|
||||
imageAltText?: string,
|
||||
imageWidth?: number,
|
||||
imageHeight?: number,
|
||||
onOpenInNewTab: () => void
|
||||
onDeleteFragment?: () => void,
|
||||
onReplaceFragment?: (newFragment: DMessageContentFragment) => void,
|
||||
scaledImageSx?: SxProps,
|
||||
partVariant: 'content-part' | 'attachment-card',
|
||||
}) {
|
||||
|
||||
// external state from the DB
|
||||
const [imageItem] = useDBAsset<DBlobImageAsset>(props.dataRefDBlobAssetId);
|
||||
|
||||
|
||||
// hook: async image regeneration
|
||||
|
||||
const { label: imageItemLabel, origin: imageItemOrigin } = imageItem || {};
|
||||
// const _recreationWidth = imageItemMetadata?.width || props.imageWidth;
|
||||
// const _recreationHeight = imageItemMetadata?.height || props.imageHeight;
|
||||
const recreationPrompt = ((imageItemOrigin?.ot === 'generated') ? imageItemOrigin.prompt : undefined) || imageItemLabel || props.imageAltText;
|
||||
|
||||
const { isFetching: isRegenerating, refetch: handleImageRegenerate } = useQuery({
|
||||
enabled: false,
|
||||
queryKey: ['regen-image-asset', props.dataRefDBlobAssetId, recreationPrompt],
|
||||
queryFn: async ({ signal }) => {
|
||||
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]);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// memo the description and overlay text
|
||||
const { dataUrlMemo, altText, overlayText } = React.useMemo(() => {
|
||||
// if no image data, return null
|
||||
if (!imageItem?.data) {
|
||||
return {
|
||||
dataUrlMemo: null,
|
||||
};
|
||||
}
|
||||
|
||||
// [attachment card] only return the data
|
||||
if (props.partVariant === 'attachment-card') {
|
||||
return {
|
||||
dataUrlMemo: `data:${imageItem.data.mimeType};base64,${imageItem.data.base64}`,
|
||||
};
|
||||
}
|
||||
|
||||
let overlayText: React.ReactNode = null;
|
||||
const extension = (imageItem.data.mimeType || props.dataRefMimeType || '').replace('image/', '');
|
||||
const overlayDate = imageItem.updatedAt || imageItem.createdAt || undefined;
|
||||
|
||||
switch (imageItem.origin.ot) {
|
||||
case 'user':
|
||||
overlayText = <Box sx={{ fontSize: '0.875em' }}>
|
||||
{/*" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "*/}
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{imageItem.origin.source} · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension}
|
||||
</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{imageItem.origin.media}{imageItem.origin.fileName ? ' · ' + imageItem.origin.fileName : ''}
|
||||
</Box>
|
||||
{!!overlayDate && <Box sx={{ opacity: 0.5 }}>
|
||||
<TimeAgo date={overlayDate} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
break;
|
||||
|
||||
case 'generated':
|
||||
overlayText = <Box sx={{ fontSize: '0.875em' }}>
|
||||
" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
AI Image · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension}
|
||||
</Box>
|
||||
<Box sx={{ opacity: 0.8 }}>
|
||||
{Object.entries(imageItem.origin.parameters).reduce((acc, [key, value]) => {
|
||||
acc.push(`${key}: ${value}`);
|
||||
return acc;
|
||||
}, [] as string[]).join(', ')}
|
||||
</Box>
|
||||
{!!overlayDate && <Box sx={{ opacity: 0.5 }}>
|
||||
<TimeAgo date={overlayDate} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
dataUrlMemo: `data:${imageItem.data.mimeType};base64,${imageItem.data.base64}`,
|
||||
altText: props.imageAltText || imageItem.metadata?.description || imageItem.label || '',
|
||||
overlayText: overlayText,
|
||||
};
|
||||
}, [imageItem, props.dataRefMimeType, props.imageAltText, props.imageHeight, props.imageWidth, props.partVariant]);
|
||||
|
||||
return (
|
||||
<RenderImageURL
|
||||
imageURL={dataUrlMemo}
|
||||
infoText={altText}
|
||||
description={overlayText}
|
||||
onOpenInNewTab={props.onOpenInNewTab}
|
||||
onImageDelete={props.onDeleteFragment}
|
||||
onImageRegenerate={(!!recreationPrompt && !isRegenerating && !!props.onReplaceFragment) ? handleImageRegenerate : undefined}
|
||||
className={isRegenerating ? 'agi-border-4' : undefined}
|
||||
scaledImageSx={props.scaledImageSx}
|
||||
variant={props.partVariant}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user