Attachments: view images

This commit is contained in:
Enrico Ros
2024-09-26 22:16:08 -07:00
parent 23cf01d4b4
commit fc8c984cd4
4 changed files with 70 additions and 14 deletions
@@ -15,10 +15,8 @@ import ReadMoreIcon from '@mui/icons-material/ReadMore';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { showImageDataRefInNewTab } from '~/modules/blocks/image/RenderImageRefDBlob';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { DMessageAttachmentFragment, DMessageDataRef, isDocPart, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { DMessageAttachmentFragment, DMessageImageRefPart, isDocPart, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { LiveFileIcon } from '~/common/livefile/liveFile.icons';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { showImageDataURLInNewTab } from '~/common/util/imageUtils';
@@ -50,8 +48,9 @@ export function LLMAttachmentMenu(props: {
menuAnchor: HTMLAnchorElement,
isPositionFirst: boolean,
isPositionLast: boolean,
onDraftAction: (attachmentDraftId: AttachmentDraftId, actionId: LLMAttachmentDraftsAction) => void,
onClose: () => void,
onDraftAction: (attachmentDraftId: AttachmentDraftId, actionId: LLMAttachmentDraftsAction) => void,
onViewImageRefPart: (imageRefPart: DMessageImageRefPart) => void
}) {
// state
@@ -97,7 +96,7 @@ export function LLMAttachmentMenu(props: {
// operations
const { attachmentDraftsStoreApi, onDraftAction, onClose } = props;
const { attachmentDraftsStoreApi, onClose, onDraftAction, onViewImageRefPart } = props;
const handleMoveUp = React.useCallback(() => {
attachmentDraftsStoreApi.getState().moveAttachmentDraft(draftId, -1);
@@ -128,11 +127,11 @@ export function LLMAttachmentMenu(props: {
copyToClipboard(text, 'Attachment Text');
}, []);
const handleViewMessageDataRef = React.useCallback((event: React.MouseEvent, dataRef: DMessageDataRef) => {
const handleViewImageRefPart = React.useCallback((event: React.MouseEvent, imageRefPart: DMessageImageRefPart) => {
event.preventDefault();
event.stopPropagation();
void showImageDataRefInNewTab(dataRef); // fire/forget
}, []);
onViewImageRefPart(imageRefPart);
}, [onViewImageRefPart]);
const canHaveDetails = !!draftInput && !isConverting;
@@ -327,7 +326,7 @@ export function LLMAttachmentMenu(props: {
return (
<Typography key={index} level='body-sm' sx={{ color: 'text.primary' }} startDecorator={<ReadMoreIcon sx={indicatorSx} />}>
<span>{mime /*.replace('image/', 'img: ')*/} · {resolution} · {part.dataRef.reftype === 'dblob' ? (part.dataRef.bytesSize?.toLocaleString() || 'no size') : '(remote)'} ·&nbsp;</span>
<Chip size='sm' color='success' variant='outlined' startDecorator={<VisibilityIcon />} onClick={(event) => handleViewMessageDataRef(event, part.dataRef)}>
<Chip size='sm' color='success' variant='outlined' startDecorator={<VisibilityIcon />} onClick={(event) => handleViewImageRefPart(event, part)}>
see
</Chip>
{isOutputMultiple && <Chip size='sm' color='danger' variant='outlined' startDecorator={<DeleteForeverIcon />} onClick={(event) => handleDeleteOutputFragment(event, index)}>
@@ -15,6 +15,9 @@ import { useOverlayComponents } from '~/common/layout/overlays/useOverlayCompone
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts-slice';
import type { DMessageImageRefPart } from '~/common/stores/chat/chat.fragments';
import { ImageRefPartModal } from '../../message/fragments-content/ImageRefPartModal';
import type { LLMAttachmentDraft } from './useLLMAttachmentDrafts';
import { LLMAttachmentButtonMemo } from './LLMAttachmentButton';
@@ -40,6 +43,7 @@ export function LLMAttachmentsList(props: {
const { showPromisedOverlay } = useOverlayComponents();
const [draftMenu, setDraftMenu] = React.useState<{ anchor: HTMLAnchorElement, attachmentDraftId: AttachmentDraftId } | null>(null);
const [overallMenuAnchor, setOverallMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
const [viewerImageRefPart, setViewerImageRefPart] = React.useState<DMessageImageRefPart | null>(null);
// derived state
@@ -106,6 +110,14 @@ export function LLMAttachmentsList(props: {
onAttachmentDraftsAction(attachmentDraftId, actionId);
}, [handleDraftMenuHide, onAttachmentDraftsAction]);
const handleViewImageRefPart = React.useCallback((imageRefPart: DMessageImageRefPart) => {
setViewerImageRefPart(imageRefPart);
}, []);
const handleCloseImageViewer = React.useCallback(() => {
setViewerImageRefPart(null);
}, []);
// no components without attachments
if (!hasAttachments)
@@ -153,7 +165,10 @@ export function LLMAttachmentsList(props: {
</Box>
{/* LLM Attachment Draft Menu */}
{/* Optional Modal to view the Image */}
{viewerImageRefPart && <ImageRefPartModal imageRefPart={viewerImageRefPart} onClose={handleCloseImageViewer} />}
{/* Single LLM Attachment Draft Menu */}
{!!itemMenuAnchor && !!itemMenuAttachmentDraft && !!props.attachmentDraftsStoreApi && (
<LLMAttachmentMenu
attachmentDraftsStoreApi={props.attachmentDraftsStoreApi}
@@ -161,8 +176,9 @@ export function LLMAttachmentsList(props: {
menuAnchor={itemMenuAnchor}
isPositionFirst={itemMenuIndex === 0}
isPositionLast={itemMenuIndex === llmAttachmentDrafts.length - 1}
onDraftAction={handleDraftAction}
onClose={handleDraftMenuHide}
onDraftAction={handleDraftAction}
onViewImageRefPart={handleViewImageRefPart}
/>
)}
@@ -13,7 +13,7 @@ import { ContentScaling, themeScalingMap } from '~/common/app.theme';
export function BlockPartImageRef(props: {
imageRefPart: DMessageImageRefPart,
fragmentId: DMessageFragmentId,
fragmentId?: DMessageFragmentId,
contentScaling: ContentScaling,
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
@@ -25,11 +25,13 @@ export function BlockPartImageRef(props: {
// event handlers
const handleDeleteFragment = React.useCallback(() => {
onFragmentDelete?.(fragmentId);
if (fragmentId && onFragmentDelete)
onFragmentDelete(fragmentId);
}, [fragmentId, onFragmentDelete]);
const handleReplaceFragment = React.useCallback((newImageFragment: DMessageContentFragment) => {
onFragmentReplace?.(fragmentId, newImageFragment);
if (fragmentId && onFragmentReplace)
onFragmentReplace(fragmentId, newImageFragment);
}, [fragmentId, onFragmentReplace]);
const handleOpenInNewTab = React.useCallback(() => {
@@ -0,0 +1,39 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box } from '@mui/joy';
import type { DMessageImageRefPart } from '~/common/stores/chat/chat.fragments';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { BlockPartImageRef } from './BlockPartImageRef';
const imageViewerModalSx: SxProps = {
maxWidth: '90vw',
};
const imageViewerContainerSx: SxProps = {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
maxHeight: '80vh',
overflow: 'auto',
};
export function ImageRefPartModal(props: { imageRefPart: DMessageImageRefPart, onClose: () => void }) {
return (
<GoodModal
open
onClose={props.onClose}
title='Attachment Image Viewer'
noTitleBar={false}
sx={imageViewerModalSx}
>
<Box sx={imageViewerContainerSx}>
<BlockPartImageRef imageRefPart={props.imageRefPart} contentScaling='sm' />
</Box>
</GoodModal>
);
}