diff --git a/frontend/src/ui/components/CanvasAgentTimeline/index.css b/frontend/src/ui/components/CanvasAgentTimeline/index.css index e97b73a..cd7d870 100644 --- a/frontend/src/ui/components/CanvasAgentTimeline/index.css +++ b/frontend/src/ui/components/CanvasAgentTimeline/index.css @@ -299,9 +299,11 @@ } .moteva-generated-image { + position: relative; width: min(100%, 300px); max-width: 100%; display: block; + aspect-ratio: 1; overflow: hidden; padding: 0; border-radius: 10px; @@ -317,11 +319,57 @@ .moteva-generated-image-preview { width: 100%; - aspect-ratio: 1; + height: 100%; display: block; - background-repeat: no-repeat; - background-position: center; - background-size: contain; + object-fit: contain; +} + +.moteva-generated-download-scrim { + position: absolute; + right: 0; + bottom: 0; + left: 0; + height: 48px; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.32) 0%, rgba(0, 0, 0, 0) 100%); + opacity: 0; + pointer-events: none; + transition: opacity 180ms ease; +} + +.moteva-generated-download { + position: absolute; + right: 2px; + bottom: 2px; + z-index: 1; + width: 34px; + height: 34px; + display: grid; + place-items: center; + padding: 0; + color: #fff; + background: transparent; + opacity: 0; + transform: translateY(4px); + transition: + opacity 180ms ease, + transform 180ms ease; +} + +.moteva-generated-download svg { + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35)); + stroke-width: 2.4; +} + +.moteva-generated-image:hover .moteva-generated-download-scrim, +.moteva-generated-image:focus-within .moteva-generated-download-scrim, +.moteva-generated-image:hover .moteva-generated-download, +.moteva-generated-image:focus-within .moteva-generated-download { + opacity: 1; +} + +.moteva-generated-image:hover .moteva-generated-download, +.moteva-generated-image:focus-within .moteva-generated-download { + transform: translateY(0); } .moteva-response-card .markdown-content { diff --git a/frontend/src/ui/components/CanvasAgentTimeline/index.tsx b/frontend/src/ui/components/CanvasAgentTimeline/index.tsx index e258800..61d6af2 100644 --- a/frontend/src/ui/components/CanvasAgentTimeline/index.tsx +++ b/frontend/src/ui/components/CanvasAgentTimeline/index.tsx @@ -1,7 +1,7 @@ "use client"; import { Fragment, useEffect, useRef, useState, type MouseEvent as ReactMouseEvent } from "react"; -import { Check, Copy, Eye, Globe2, Heart, Loader2 } from "lucide-react"; +import { Check, Copy, Download, Eye, Globe2, Heart, Loader2 } from "lucide-react"; import type { AgentMessage, CanvasNode } from "@/domain/design"; import { useI18n } from "@/i18n/i18n"; import { MarkdownContent } from "@/ui/components/MarkdownContent"; @@ -143,12 +143,13 @@ function isImageGenerationMessage(message: AgentMessage) { } function MotevaGeneratedResponse({ message, nodes = [], artifacts = [] }: { message?: AgentMessage; nodes?: CanvasNode[]; artifacts?: AgentImageArtifact[] }) { + const { t } = useI18n(); const items = artifacts.length > 0 - ? artifacts.map((artifact) => ({ id: artifact.id, title: artifact.title, url: artifact.url })) + ? artifacts.map((artifact) => ({ id: artifact.id, title: artifact.title, url: artifact.url, width: artifact.width, height: artifact.height })) : nodes .filter((node) => isImageUrl(node.content)) - .map((node) => ({ id: node.id, title: node.title, url: node.content })); + .map((node) => ({ id: node.id, title: node.title, url: node.content, width: node.width, height: node.height })); const title = items[0]?.title || message?.title || "Generated image"; const createdAt = message?.createdAt || new Date().toISOString(); const content = message ? cleanMessageContent(message.content) : ""; @@ -165,9 +166,28 @@ function MotevaGeneratedResponse({ message, nodes = [], artifacts = [] }: { mess {items.length > 0 && (