diff --git a/src/apps/chat/components/message/fragments-content/BlockPartImageRef.tsx b/src/apps/chat/components/message/fragments-content/BlockPartImageRef.tsx index be50d0b8b..11f9d9bb2 100644 --- a/src/apps/chat/components/message/fragments-content/BlockPartImageRef.tsx +++ b/src/apps/chat/components/message/fragments-content/BlockPartImageRef.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import type { SxProps } from '@mui/joy/styles/types'; -import { Box } from '@mui/joy'; import { BlocksContainer } from '~/modules/blocks/BlocksContainers'; import { RenderImageRefDBlob } from '~/modules/blocks/image/RenderImageRefDBlob'; @@ -78,17 +77,15 @@ export function BlockPartImageRef(props: { scaledImageSx={scaledImageSx} variant='content-part' /> - ) : ( - - ContentPartImageRef: unknown reftype - - )} + ) : 'BlockPartImageRef: unknown reftype'} {/* Image viewer modal */} {!props.disableViewer && viewingImageRefPart && ( setViewingImageRefPart(null)} + onDeleteFragment={onFragmentDelete ? handleDeleteFragment : undefined} + onReplaceFragment={onFragmentReplace ? handleReplaceFragment : undefined} /> )} diff --git a/src/apps/chat/components/message/fragments-content/ViewImageRefPartModal.tsx b/src/apps/chat/components/message/fragments-content/ViewImageRefPartModal.tsx index 3c2878eeb..86e72a3db 100644 --- a/src/apps/chat/components/message/fragments-content/ViewImageRefPartModal.tsx +++ b/src/apps/chat/components/message/fragments-content/ViewImageRefPartModal.tsx @@ -4,17 +4,18 @@ import type { SxProps } from '@mui/joy/styles/types'; import { Box, Button } from '@mui/joy'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import { RenderImageRefDBlob } from '~/modules/blocks/image/RenderImageRefDBlob'; +import { RenderImageURL } from '~/modules/blocks/image/RenderImageURL'; + import { getImageAsset } from '~/common/stores/blob/dblobs-portability'; -import type { DMessageImageRefPart } from '~/common/stores/chat/chat.fragments'; +import type { DMessageContentFragment, DMessageImageRefPart } from '~/common/stores/chat/chat.fragments'; +import { AppBreadcrumbs } from '~/common/components/AppBreadcrumbs'; import { GoodModal } from '~/common/components/modals/GoodModal'; import { convert_Base64WithMimeType_To_Blob } from '~/common/util/blobUtils'; import { downloadBlob } from '~/common/util/downloadUtils'; import { useIsMobile } from '~/common/components/useMatchMedia'; -import { BlockPartImageRef } from './BlockPartImageRef'; -import { AppBreadcrumbs } from '~/common/components/AppBreadcrumbs'; - const imageViewerModalSx: SxProps = { maxWidth: '90vw', @@ -28,10 +29,11 @@ const imageViewerContainerSx: SxProps = { maxHeight: '80vh', overflow: 'auto', - // pre-compensate the Block > Render Items 1.5 margin - m: -1.5, + // pre-compensate the RenderImageRefDBlob > Sheet's 1.5 (BlocksContainer-alike) margin + mx: -1.5, + // add some margin to unclip the Sheet's shadow '& > div': { - pt: 1.5, + mb: 0.5, }, }; @@ -39,6 +41,8 @@ const imageViewerContainerSx: SxProps = { export function ViewImageRefPartModal(props: { imageRefPart: DMessageImageRefPart, onClose: () => void, + onDeleteFragment?: () => void, + onReplaceFragment?: (newFragment: DMessageContentFragment) => void, }) { // state @@ -49,7 +53,7 @@ export function ViewImageRefPartModal(props: { const isMobile = useIsMobile(); // derived state - const { dataRef, altText } = props.imageRefPart; + const { dataRef, altText, width, height } = props.imageRefPart; const isDBlob = dataRef.reftype === 'dblob'; // handlers @@ -133,11 +137,27 @@ export function ViewImageRefPartModal(props: { sx={imageViewerModalSx} > - + {dataRef.reftype === 'dblob' ? ( + + ) : dataRef.reftype === 'url' ? ( + + ) : 'ViewImageRefPartModal: unknown reftype'} ); diff --git a/src/modules/blocks/image/RenderImageURL.tsx b/src/modules/blocks/image/RenderImageURL.tsx index b95f088e4..c439ddcfc 100644 --- a/src/modules/blocks/image/RenderImageURL.tsx +++ b/src/modules/blocks/image/RenderImageURL.tsx @@ -1,19 +1,15 @@ import * as React from 'react'; import type { SxProps } from '@mui/joy/styles/types'; -import { Alert, Box, IconButton, Sheet } from '@mui/joy'; +import { Box, Sheet } from '@mui/joy'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline'; import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined'; import ReplayIcon from '@mui/icons-material/Replay'; -import WarningRoundedIcon from '@mui/icons-material/WarningRounded'; -import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap'; - -import { Link } from '~/common/components/Link'; import type { RenderBlockInputs } from '../blocks.types'; -import { OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsTopRightSx, StyledOverlayButton } from '../OverlayButton'; +import { OverlayButton, overlayButtonsActiveSx, overlayButtonsClassName, overlayButtonsTopRightSx } from '../OverlayButton'; /// Heuristics to parse Markdown images (as URLs) /// @@ -79,7 +75,6 @@ export const RenderImageURL = (props: { const [loadingTimeout, setLoadingTimeout] = React.useState(false); const [deleteArmed, setDeleteArmed] = React.useState(false); const [regenArmed, setRegenArmed] = React.useState(false); - const [showDalleAlert, setShowDalleAlert] = React.useState(true); // Effect React.useEffect(() => { @@ -124,177 +119,150 @@ export const RenderImageURL = (props: { setRegenArmed(armed => !armed); }, [handleImageRegenerate, regenArmed]); + const handleImageClick = React.useCallback((e: React.MouseEvent) => { + if (onViewImage) { + e.stopPropagation(); + handleViewImage(e); + } else if (props.imageURL?.startsWith('http')) { + e.stopPropagation(); + window.open(props.imageURL, '_blank', 'noopener,noreferrer'); + } + }, [onViewImage, handleViewImage, props.imageURL]); + // derived state const isCard = props.variant === 'attachment-card'; + const isImageClickable = !props.disabled && (!!onViewImage || (!!props.imageURL && props.imageURL.startsWith('http'))); + + // Only show regeneration in modal context (when not showing a viewer button) + const showRegenerate = !!onImageRegenerate && !onViewImage; const isOnButton = props.variant === 'attachment-button'; - const isTempDalleUrl = props.imageURL?.startsWith('https://oaidalle') || false; return ( - + .${overlayButtonsClassName}`]: overlayButtonsActiveSx, + '&:hover .overlay-text': overlayButtonsActiveSx, - // resizeable image - '& picture': { display: 'flex', justifyContent: 'center' }, - '& img': { maxWidth: '100%', maxHeight: '100%', filter: props.disabled ? 'grayscale(100%)' : undefined }, - [`&:hover > .${overlayButtonsClassName}`]: overlayButtonsActiveSx, - '&:hover .overlay-text': overlayButtonsActiveSx, + // layout + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', - // layout - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', + // this shall apply font scaling and maybe margins, not much + ...props.scaledImageSx, + }} + > - // this shall apply font scaling and maybe margins, not much - ...props.scaledImageSx, - }} - > + {/* Image and Overlay - clickable to view/maximize */} + - {/* Image and Overlay */} - - - {/* Image / Loading Indicator */} - {props.imageURL ? ( - - {props.expandableText - - ) : ( - - {loadingTimeout ? 'Image Missing' : 'Loading...'} - - )} - - {/* [overlay] Description */} - {!!props.overlayText && ( - - {props.overlayText} - - )} - - - {/* Bottom Expander: information */} - {!!props.expandableText && infoOpen && ( - - {props.expandableText} + {/* Image / Loading Indicator */} + {props.imageURL ? ( + + {props.expandableText + + ) : ( + + {loadingTimeout ? 'Image Missing' : 'Loading...'} )} - {/* [overlay] Buttons (RenderImage) */} - {!props.disabled && + {/* [overlay] Description */} + {!!props.overlayText && ( + + {props.overlayText} + + )} + - {!!props.expandableText && ( - - - - )} - - {!!props.imageURL && ( - props.onViewImage ? ( - - - - ) : props.imageURL.startsWith('http') ? ( - - - - ) : - )} - - - {/* Deletion */} - - {deleteArmed && !regenArmed && ( - - - - )} - - {!!onImageDelete && !regenArmed && ( - - {deleteArmed ? : } - - )} - - {!!onImageRegenerate && !deleteArmed && ( - - {regenArmed - ? - : - } - - )} - - {/* Regenerate [armed, arming] buttons */} - {regenArmed && !deleteArmed && ( - - - - )} - - } - - - - {/* (Remove in 2025) Dalle Warning notice */} - {isTempDalleUrl && showDalleAlert && ( - } - endDecorator={ - setShowDalleAlert(on => !on)} sx={{ my: -0.5 }}> - - - } - sx={{ - mx: 0.5, - ...props.scaledImageSx, - }} - > -
- Please Save Locally ยท OpenAI will delete this image link from their servers one hour after creation. -
-
+ {/* Bottom Expander: information */} + {!!props.expandableText && infoOpen && ( + + {props.expandableText} + )} -
+ {/* [overlay] Buttons (RenderImage) */} + {!props.disabled && + + {/* Info toggle */} + {!!props.expandableText && ( + + + + )} + + {/* Delete toggle/cancel */} + {!!onImageDelete && !regenArmed && ( + + {deleteArmed ? : } + + )} + + {/* Delete confirm (armed) */} + {deleteArmed && !regenArmed && ( + + + + )} + + {/* Regenerate toggle/cancel - only in modal context (click image to view inline) */} + {showRegenerate && !deleteArmed && ( + + {regenArmed ? : } + + )} + + {/* Regenerate confirm (armed) */} + {regenArmed && !deleteArmed && ( + + + + )} + + } + ); };