From 1721f53248f2b3d1c14b08257c0d3fd414312c1c Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 26 Mar 2026 17:38:32 +0800 Subject: [PATCH] feat agent scene result resize dialog --- .../agents/AgentGenerationWorkspaceDarkV2.tsx | 219 +++++++++++++++++- components/canvas/nodes/ImageNode.tsx | 2 +- components/image-annotator.tsx | 12 +- 3 files changed, 230 insertions(+), 3 deletions(-) diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index 54cce3592..8ab6e3bf0 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -49,6 +49,7 @@ import { import { ImageParamsBar, type ImageGenerationParams } from "@/components/image-params-bar"; import { AgentWorkflowShowcaseBanner } from "@/components/agents/AgentWorkflowShowcaseBanner"; import { AgentDetailReferenceStitchDialog } from "@/components/agents/AgentDetailReferenceStitchDialog"; +import { ImageAnnotator } from "@/components/image-annotator"; import { useCanvasStore } from "@/store/canvas"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { buildImageProxyUrl } from "@/lib/image-delivery"; @@ -143,6 +144,11 @@ interface GeneratedImageGroup { text: string; } +interface SceneResultToolbarTarget { + url: string; + index: number; +} + interface AgentModeReferenceSearchResponse { success?: boolean; data?: { @@ -936,6 +942,9 @@ export function AgentGenerationWorkspaceDarkV2({ const [activeResultModeType, setActiveResultModeType] = useState("scene_generation"); const [fullscreenPreview, setFullscreenPreview] = useState(null); + const [sceneResultToolbarTarget, setSceneResultToolbarTarget] = + useState(null); + const [sceneResizeEditorTargetUrl, setSceneResizeEditorTargetUrl] = useState(null); const [batchDownloadState, setBatchDownloadState] = useState(null); const [detailReplacementSearchState, setDetailReplacementSearchState] = useState<{ isLoading: boolean; @@ -2217,6 +2226,17 @@ export function AgentGenerationWorkspaceDarkV2({ } }, [activeResultModeUrls, selectedResultUrl]); + useEffect(() => { + if ( + sceneResultToolbarTarget?.url && + !normalizeResultCollectionUrls(mergedResultCollections.scene_generation).includes( + sceneResultToolbarTarget.url + ) + ) { + setSceneResultToolbarTarget(null); + } + }, [mergedResultCollections.scene_generation, sceneResultToolbarTarget]); + useEffect(() => { if (!selectedAgentPresetId || !hasAgentUploadSlots) { setIsAttachmentSlotExpanded(false); @@ -2569,6 +2589,64 @@ export function AgentGenerationWorkspaceDarkV2({ toast.message("已打开原图,可右键另存为"); } }, []); + const uploadBase64ResultToStorage = useCallback(async (base64Url: string) => { + const response = await fetch(base64Url); + const blob = await response.blob(); + const formData = new FormData(); + formData.append("file", blob, `agent-scene-resize-${Date.now()}.png`); + + const uploadResponse = await fetchUploadWithRetry(formData); + if (!uploadResponse.ok) { + throw new Error("上传失败"); + } + + const result = await uploadResponse.json().catch(() => ({})); + const uploadedUrl = String(result?.url || "").trim(); + if (!/^https?:\/\//i.test(uploadedUrl)) { + throw new Error("上传结果无效"); + } + return uploadedUrl; + }, []); + const handleSceneResultResizeComplete = useCallback( + async (resizedImageUrl: string) => { + if (!onResultPreviewUrlsChange) { + toast.error("当前项目未接入结果保存能力"); + return; + } + + let finalUrl = String(resizedImageUrl || "").trim(); + if (finalUrl.startsWith("data:")) { + try { + toast.info("正在上传尺寸调整结果..."); + finalUrl = await uploadBase64ResultToStorage(finalUrl); + } catch (error) { + console.error("[AgentGenerationWorkspaceDarkV2] 上传尺寸调整结果失败:", error); + toast.error("尺寸调整结果上传失败"); + return; + } + } + + if (!/^https?:\/\//i.test(finalUrl)) { + toast.error("尺寸调整结果地址无效"); + return; + } + + const nextSceneUrls = Array.from( + new Set([ + ...normalizeResultCollectionUrls(mergedResultCollections.scene_generation), + finalUrl, + ]) + ); + + onResultPreviewUrlsChange("scene_generation", nextSceneUrls); + setActiveResultModeType("scene_generation"); + setSelectedResultUrl(finalUrl); + setSceneResizeEditorTargetUrl(null); + setSceneResultToolbarTarget(null); + toast.success("尺寸调整结果已加入场景结果区"); + }, + [mergedResultCollections.scene_generation, onResultPreviewUrlsChange, uploadBase64ResultToStorage] + ); const handleDeleteResultImage = useCallback( (modeType: AgentGenerationResultModeType, imageUrl: string) => { const normalizedUrl = String(imageUrl || "").trim(); @@ -2582,11 +2660,23 @@ export function AgentGenerationWorkspaceDarkV2({ if (fullscreenPreview === normalizedUrl) { setFullscreenPreview(null); } + if (sceneResultToolbarTarget?.url === normalizedUrl) { + setSceneResultToolbarTarget(null); + } + if (sceneResizeEditorTargetUrl === normalizedUrl) { + setSceneResizeEditorTargetUrl(null); + } onDeleteResultImage(modeType, normalizedUrl); toast.success("结果图已删除"); }, - [fullscreenPreview, onDeleteResultImage, selectedResultUrl] + [ + fullscreenPreview, + onDeleteResultImage, + sceneResizeEditorTargetUrl, + sceneResultToolbarTarget, + selectedResultUrl, + ] ); const handleToolStepChange = useCallback( @@ -5381,6 +5471,7 @@ export function AgentGenerationWorkspaceDarkV2({
{gridResultCards.map((card) => { const isActive = card.url && displayedResultUrl === card.url; + const isSceneResultCard = activeResultModeType === "scene_generation"; return (
{ if (!card.url) return; setSelectedResultUrl(card.url); + if (isSceneResultCard) { + setSceneResultToolbarTarget({ + url: card.url, + index: card.index, + }); + } }} onDoubleClick={() => { if (!card.url) return; @@ -5401,6 +5498,12 @@ export function AgentGenerationWorkspaceDarkV2({ if (event.key === "Enter" || event.key === " ") { event.preventDefault(); setSelectedResultUrl(card.url); + if (isSceneResultCard) { + setSceneResultToolbarTarget({ + url: card.url, + index: card.index, + }); + } } }} className={cn( @@ -5420,6 +5523,11 @@ export function AgentGenerationWorkspaceDarkV2({ className="h-full w-full object-contain p-2" loading="lazy" /> + {isSceneResultCard ? ( +
+ 单击打开工具栏,双击查看大图 +
+ ) : null}
+ + {sceneResultToolbarTarget && activeResultModeType === "scene_generation" ? ( + setSceneResultToolbarTarget(null)} + > + event.stopPropagation()} + > +
+
+
+ 场景图工具栏 +
+
+ 场景结果图 {sceneResultToolbarTarget.index + 1} +
+
+ 这里可以快速预览、下载,或直接进入尺寸调整弹窗。 +
+
+ +
+ +
+ {`场景结果图 + handlePreviewImageLoadError(event, sceneResultToolbarTarget.url) + } + /> +
+ +
+ + + +
+
+
+ ) : null} +
+ + {sceneResizeEditorTargetUrl ? ( + setSceneResizeEditorTargetUrl(null)} + open={Boolean(sceneResizeEditorTargetUrl)} + onOpenChange={(open) => { + if (!open) { + setSceneResizeEditorTargetUrl(null); + } + }} + /> + ) : null} + {/* Fullscreen Preview */} {fullscreenPreview && ( diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index cb6663a30..289c4ab0a 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -3401,7 +3401,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { key={`resize-${node.id}-${data.src}`} imageUrl={data.src} initialResizeMode - confirmButtonText="发送到画布" + confirmButtonText="保存处理结果" onConfirm={handleResizeEditorComplete} onCancel={() => setResizeAnnotatorOpen(false)} open={resizeAnnotatorOpen} diff --git a/components/image-annotator.tsx b/components/image-annotator.tsx index 64da95cf6..b59559fcd 100644 --- a/components/image-annotator.tsx +++ b/components/image-annotator.tsx @@ -55,6 +55,7 @@ interface ImageAnnotatorProps { initialResizeMode?: boolean; // 是否直接进入尺寸调整模式 confirmButtonText?: string; submitMode?: "confirm" | "publish-new-canvas"; + appearance?: "theme" | "dark"; open?: boolean; // 兼容 Dialog 模式 onOpenChange?: (open: boolean) => void; } @@ -67,6 +68,7 @@ export function ImageAnnotator({ initialResizeMode = false, confirmButtonText, submitMode = "confirm", + appearance = "theme", open = true, onOpenChange, }: ImageAnnotatorProps) { @@ -103,6 +105,7 @@ export function ImageAnnotator({ const resolvedConfirmButtonText = confirmButtonText || (submitMode === "publish-new-canvas" ? "发布到新的画布" : "发送到新对话"); + const forceDarkAppearance = appearance === "dark"; const toCanvasLoadUrl = useCallback((url: string) => { return buildImageProxyUrl(url); }, []); @@ -1513,11 +1516,17 @@ export function ImageAnnotator({ return ( - + 图片编辑器 编辑、标注或处理图片。 +
{/* Top Header - Minimal */} {!showSceneFusion && ( @@ -2006,6 +2015,7 @@ export function ImageAnnotator({ />
)} +
);