mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Attachments: view images
This commit is contained in:
@@ -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)'} · </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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user