diff --git a/components/agents/AgentGenerationWorkspaceDarkV2.tsx b/components/agents/AgentGenerationWorkspaceDarkV2.tsx index b09742e5f..903611ee5 100644 --- a/components/agents/AgentGenerationWorkspaceDarkV2.tsx +++ b/components/agents/AgentGenerationWorkspaceDarkV2.tsx @@ -67,6 +67,7 @@ import { AgentPreviewImage } from "@/components/agents/AgentPreviewImage"; import { useAgentStylePerformanceMonitor } from "@/components/agents/use-agent-style-performance-monitor"; import { ImageAnnotator } from "@/components/image-annotator"; import { useCanvasStore } from "@/store/canvas"; +import { imageGenerationService } from "@/services/canvas"; import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry"; import { requestLongTaskResult } from "@/lib/client/long-task"; import { buildImageProxyUrl } from "@/lib/image-delivery"; @@ -218,6 +219,19 @@ type BatchDownloadDialogState = { expiresAt: number; }; +type DetailResultLocalEditTarget = { + cardId: string; + imageUrl: string; + title: string; +}; + +type DetailResultLocalEditRect = { + x: number; + y: number; + width: number; + height: number; +}; + const MAX_CHAT_TEXT_PART_LENGTH = 2000; const MAX_CHAT_HIDDEN_PROMPT_LENGTH = 4000; const MAX_CHAT_ATTACHMENT_NAME_LENGTH = 100; @@ -700,6 +714,177 @@ function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } +function DetailResultLocalEditDialog({ + submitting, + target, + onClose, + onSubmit, +}: { + submitting: boolean; + target: DetailResultLocalEditTarget | null; + onClose: () => void; + onSubmit: (params: { + imageUrl: string; + prompt: string; + rect: DetailResultLocalEditRect; + title: string; + }) => void; +}) { + const [prompt, setPrompt] = useState(""); + const [rect, setRect] = useState(null); + const dragStartRef = useRef<{ x: number; y: number } | null>(null); + + useEffect(() => { + if (!target) { + setPrompt(""); + setRect(null); + dragStartRef.current = null; + } + }, [target]); + + const getPoint = useCallback((event: React.PointerEvent) => { + const bounds = event.currentTarget.getBoundingClientRect(); + const x = bounds.width > 0 ? (event.clientX - bounds.left) / bounds.width : 0; + const y = bounds.height > 0 ? (event.clientY - bounds.top) / bounds.height : 0; + return { + x: Math.min(1, Math.max(0, x)), + y: Math.min(1, Math.max(0, y)), + }; + }, []); + + const updateRect = useCallback( + (event: React.PointerEvent) => { + const start = dragStartRef.current; + if (!start) return; + const point = getPoint(event); + const left = Math.min(start.x, point.x); + const top = Math.min(start.y, point.y); + setRect({ + x: left, + y: top, + width: Math.max(0.01, Math.abs(point.x - start.x)), + height: Math.max(0.01, Math.abs(point.y - start.y)), + }); + }, + [getPoint] + ); + + const handleSubmit = useCallback(() => { + if (!target || !rect) return; + const normalizedPrompt = prompt.trim(); + if (!normalizedPrompt) { + toast.error("请填写局部编辑提示词"); + return; + } + if (rect.width < 0.025 || rect.height < 0.025) { + toast.error("请框选更明确的编辑区域"); + return; + } + onSubmit({ + imageUrl: target.imageUrl, + prompt: normalizedPrompt, + rect, + title: target.title, + }); + }, [onSubmit, prompt, rect, target]); + + return ( + (!open ? onClose() : undefined)}> + + + 局部编辑详情图 + + 在图片上拖拽框选需要编辑的位置,并填写局部修改要求。 + + +
+
+ {target?.imageUrl ? ( +
{ + updateRect(event); + dragStartRef.current = null; + }} + onPointerDown={(event) => { + event.preventDefault(); + event.currentTarget.setPointerCapture(event.pointerId); + const point = getPoint(event); + dragStartRef.current = point; + setRect({ x: point.x, y: point.y, width: 0.01, height: 0.01 }); + }} + onPointerMove={(event) => { + if (!dragStartRef.current) return; + event.preventDefault(); + updateRect(event); + }} + onPointerUp={(event) => { + updateRect(event); + dragStartRef.current = null; + if (event.currentTarget.hasPointerCapture(event.pointerId)) { + event.currentTarget.releasePointerCapture(event.pointerId); + } + }} + > + {target.title} + {rect ? ( +
+ ) : null} +
+ ) : null} +
+
+
+ 框选区域后,输入你想改的内容。处理完成的新图会自动加入 AI详情结果区。 +
+