diff --git a/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx b/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx index 7dfdd903fe..a9919708d0 100644 --- a/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx +++ b/studio/frontend/src/components/assistant-ui/tool-ui-image-generation.tsx @@ -10,7 +10,7 @@ import { DownloadIcon, ImageIcon, PencilIcon } from "lucide-react"; import type { CSSProperties, MouseEvent } from "react"; import { memo, useCallback, useEffect, useRef, useState } from "react"; import { useGeneratedImageOverlay } from "./generated-image-overlay-context"; -import { Image, downloadImagePart } from "./image"; +import { downloadImagePart } from "./image"; import { ToolFallbackContent, ToolFallbackRoot, @@ -64,6 +64,8 @@ type GeneratedImagePart = { }; const CAPTION_COLLAPSED_LINES = 4; +const INLINE_IMAGE_MAX_WIDTH = 520; +const INLINE_IMAGE_MAX_HEIGHT = 620; const extensionForMime = (mime: string): string => { switch (mime.toLowerCase()) { @@ -100,6 +102,31 @@ const formatGeneratedImageLabel = (prompt: string): string => { : `Generated image: ${prompt}`; }; +const parseImageSize = ( + size?: string, +): { width: number; height: number } | null => { + const match = size?.match(/^(\d+)x(\d+)$/i); + if (!match) return null; + const width = Number(match[1]); + const height = Number(match[2]); + return width > 0 && height > 0 ? { width, height } : null; +}; + +const getInlineImageFrameWidth = ({ + width, + height, +}: { + width: number; + height: number; +}): number => + Math.round( + Math.min( + width, + INLINE_IMAGE_MAX_WIDTH, + (INLINE_IMAGE_MAX_HEIGHT * width) / height, + ), + ); + const loadingDots = Array.from({ length: 64 }, (_, index) => { const row = Math.floor(index / 8); const col = index % 8; @@ -150,6 +177,16 @@ const ImageGenerationToolUIImpl: ToolCallMessagePartComponent = ({ typeof result === "object" && typeof (result as ImageGenerationResult).image_b64 === "string"; const imageResult = isImageResult ? (result as ImageGenerationResult) : null; + const imageDimensions = parseImageSize(imageResult?.size); + const imageFrameStyle: CSSProperties = { + width: imageDimensions + ? getInlineImageFrameWidth(imageDimensions) + : INLINE_IMAGE_MAX_WIDTH, + maxWidth: "100%", + }; + const imageBoxStyle: CSSProperties | undefined = imageDimensions + ? { aspectRatio: `${imageDimensions.width} / ${imageDimensions.height}` } + : undefined; const mime = imageResult?.image_mime || "image/png"; const imageSrc = imageResult?.image_b64 ? `data:${mime};base64,${imageResult.image_b64}` @@ -202,8 +239,7 @@ const ImageGenerationToolUIImpl: ToolCallMessagePartComponent = ({ const computedStyle = window.getComputedStyle(captionElement); const lineHeight = Number.parseFloat(computedStyle.lineHeight); const collapsedHeight = - (Number.isFinite(lineHeight) ? lineHeight : 20) * - CAPTION_COLLAPSED_LINES; + (Number.isFinite(lineHeight) ? lineHeight : 20) * CAPTION_COLLAPSED_LINES; const hasOverflow = captionElement.scrollHeight > collapsedHeight + 1; setPromptOverflow((current) => current?.prompt === captionPrompt && current.canExpand === hasOverflow @@ -289,26 +325,34 @@ const ImageGenerationToolUIImpl: ToolCallMessagePartComponent = ({ /> {imagePart ? ( -
-
- -
+
+
@@ -335,7 +379,7 @@ const ImageGenerationToolUIImpl: ToolCallMessagePartComponent = ({
{captionPrompt ? ( -
+