diff --git a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx index cae3fcaeb..10f9e6443 100644 --- a/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx +++ b/src/apps/chat/components/composer/llmattachments/LLMAttachmentMenu.tsx @@ -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'; diff --git a/src/apps/chat/components/message/fragments-attachment-image/ImageAttachmentFragments.tsx b/src/apps/chat/components/message/fragments-attachment-image/ImageAttachmentFragments.tsx index 33386f441..eeff19066 100644 --- a/src/apps/chat/components/message/fragments-attachment-image/ImageAttachmentFragments.tsx +++ b/src/apps/chat/components/message/fragments-attachment-image/ImageAttachmentFragments.tsx @@ -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', diff --git a/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx b/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx index 01052f311..47ea1b127 100644 --- a/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx +++ b/src/apps/chat/components/message/fragments-content/ContentPartImageRef.tsx @@ -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(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 = - {/*" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "*/} - - {imageItem.origin.source} · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension} - - - {imageItem.origin.media}{imageItem.origin.fileName ? ' · ' + imageItem.origin.fileName : ''} - - {!!overlayDate && - - } - ; - break; - - case 'generated': - overlayText = - " {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} " - - AI Image · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension} - - - {Object.entries(imageItem.origin.parameters).reduce((acc, [key, value]) => { - acc.push(`${key}: ${value}`); - return acc; - }, [] as string[]).join(', ')} - - {!!overlayDate && - - } - ; - 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 ( - - ); -} - - -function ContentPartImageRefURL(props: { dataRefUrl: string, imageAltText?: string, scaledImageSx?: SxProps, }) { - return ( - - ); -} +import { PartImageRefDBlob, showImageDataRefInNewTab } from './PartImageRefDBlob'; export function ContentPartImageRef(props: { @@ -209,15 +62,16 @@ export function ContentPartImageRef(props: { partVariant='content-part' /> ) : dataRef.reftype === 'url' ? ( - ) : ( - ContentPartImageRef: unknown reftype + + ContentPartImageRef: unknown reftype + )} ); diff --git a/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx b/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx new file mode 100644 index 000000000..efb353a82 --- /dev/null +++ b/src/apps/chat/components/message/fragments-content/PartImageRefDBlob.tsx @@ -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(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 = + {/*" {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} "*/} + + {imageItem.origin.source} · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension} + + + {imageItem.origin.media}{imageItem.origin.fileName ? ' · ' + imageItem.origin.fileName : ''} + + {!!overlayDate && + + } + ; + break; + + case 'generated': + overlayText = + " {imageItem.label.length > 120 ? imageItem.label.slice(0, 120 - 3) + '...' : imageItem.label} " + + AI Image · {imageItem.metadata?.width || props.imageWidth}x{imageItem.metadata?.height || props.imageHeight} · {extension} + + + {Object.entries(imageItem.origin.parameters).reduce((acc, [key, value]) => { + acc.push(`${key}: ${value}`); + return acc; + }, [] as string[]).join(', ')} + + {!!overlayDate && + + } + ; + 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 ( + + ); +}