diff --git a/.codex-tmp/interactive-dialog-generator/交互式对话框生成器.html b/.codex-tmp/interactive-dialog-generator/交互式对话框生成器.html new file mode 100644 index 0000000..c7f485d --- /dev/null +++ b/.codex-tmp/interactive-dialog-generator/交互式对话框生成器.html @@ -0,0 +1,408 @@ + + + + + + 交互式对话框生成器 + + + + + + + +
+

+ 交互式对话框生成器 +

+ +
+ +
+ +
+

+ 上传背景图片 +

+
+ +

点击或拖拽图片到此处

+

支持 JPG、PNG、WEBP 格式

+ +
+
+ + +
+

+ 点击添加对话框 +

+

每点一次即在预览区新增一个对话框

+
+
+ 白色圆角对话框 +
+
+ 蓝色气泡对话框 +
+
+ 黄色提示对话框 +
+
+ 灰色简约对话框 +
+
+
+ +
+ +
+
+ + +
+

+ 预览区域 +

+
+
+
+
+ +

上传图片后开始编辑

+
+
+

+ 提示:对话框可拖动定位,输入文字后点确认即可渲染,双击已确认的框可重新编辑 +

+
+
+
+ + + + diff --git a/src/features/agent/AgentPage.tsx b/src/features/agent/AgentPage.tsx index a96d470..fff8b76 100644 --- a/src/features/agent/AgentPage.tsx +++ b/src/features/agent/AgentPage.tsx @@ -13,7 +13,7 @@ import { SendOutlined, ThunderboltOutlined, } from "@ant-design/icons"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import WorkspacePageShell from "../../components/WorkspacePageShell"; import type { WebGenerationPreviewTask } from "../../types"; @@ -72,6 +72,24 @@ const agentModes = [ }, ]; +const agentModelOptions = [ + { id: "gemini-3.1-pro", label: "Gemini 3.1 Pro" }, + { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro" }, + { id: "gpt-4o", label: "GPT-4o" }, +]; + +const thinkingSpeedOptions = [ + { id: "fast", label: "快速" }, + { id: "balanced", label: "均衡" }, + { id: "precise", label: "精细" }, +]; + +const thinkingDepthOptions = [ + { id: "concise", label: "简洁" }, + { id: "standard", label: "标准" }, + { id: "deep", label: "深度" }, +]; + const quickStarts = ["「新品发布」全链路运营", "「销售日报」自动分析", "「竞品监控」每周报告"]; function getTaskSourceLabel(task: WebGenerationPreviewTask): string | null { @@ -93,6 +111,21 @@ function AgentPage({ const [prompt, setPrompt] = useState("让 Omni Agent 帮我规划「新品发布会全流程」"); const [isRunning, setIsRunning] = useState(false); const [notice, setNotice] = useState("选择一个 Agent 模式,输入目标后即可开始。"); + const [agentModel, setAgentModel] = useState(agentModelOptions[0].id); + const [thinkingSpeed, setThinkingSpeed] = useState(thinkingSpeedOptions[1].id); + const [thinkingDepth, setThinkingDepth] = useState(thinkingDepthOptions[1].id); + const [activeDropdown, setActiveDropdown] = useState(null); + const controlsRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (controlsRef.current && !controlsRef.current.contains(event.target as Node)) { + setActiveDropdown(null); + } + }; + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, []); const selectedMode = agentModes.find((item) => item.id === activeMode) ?? agentModes[0]; const recentTasks = tasks.slice(0, 3); @@ -203,15 +236,85 @@ function AgentPage({ />
-
+
- +
+ + {activeDropdown === "model" && ( +
+ {agentModelOptions.map((m) => ( + + ))} +
+ )} +
+
+ + {activeDropdown === "speed" && ( +
+ {thinkingSpeedOptions.map((s) => ( + + ))} +
+ )} +
+
+ + {activeDropdown === "depth" && ( +
+ {thinkingDepthOptions.map((d) => ( + + ))} +
+ )} +
diff --git a/src/features/canvas/CanvasPage.tsx b/src/features/canvas/CanvasPage.tsx index 5f405ff..0806c67 100644 --- a/src/features/canvas/CanvasPage.tsx +++ b/src/features/canvas/CanvasPage.tsx @@ -389,6 +389,8 @@ function CanvasPage({ const canvasRef = useRef(null); const videoGenerationInFlightRef = useRef(new Set()); const canvasReferenceUploadPromisesRef = useRef(new Map>()); + const canvasDragCounterRef = useRef(0); + const [isCanvasDragging, setIsCanvasDragging] = useState(false); const suppressNextPaneClickRef = useRef(false); const canvasAutoSaveTimerRef = useRef(null); const canvasAutoSaveIdleHandleRef = useRef(null); @@ -1278,7 +1280,7 @@ function CanvasPage({ model: defaultVideoModel, aspectRatio: "16:9", resolution: getDefaultVideoQuality(defaultVideoModel), - duration: "4", + duration: "5", videoMode: "text2video", sourceTextNodeId: source.id, position: { @@ -1302,7 +1304,7 @@ function CanvasPage({ model: defaultVideoModel, aspectRatio: "16:9", resolution: getDefaultVideoQuality(defaultVideoModel), - duration: "4", + duration: "5", videoMode: "text2video", sourceTextNodeId: "", position, @@ -1358,7 +1360,7 @@ function CanvasPage({ imageUrl = "", fileName = "本地图片", position = { x: 0, y: 0 }, - options?: { title?: string; sourceImageNodeId?: string } + options?: { title?: string; sourceImageNodeId?: string; sourceTextNodeId?: string } ) => { const nodeNumber = imageNodeIdRef.current; imageNodeIdRef.current += 1; @@ -1372,6 +1374,7 @@ function CanvasPage({ imageSize: getDefaultImageQuality(fallbackVisibleImageModel), fileName, sourceImageNodeId: options?.sourceImageNodeId, + sourceTextNodeId: options?.sourceTextNodeId, position, size: createCanvasNodeSize("image"), }; @@ -1977,6 +1980,120 @@ function CanvasPage({ setNodeMenu(null); }; + // ── Canvas drag-and-drop file upload ────────────────────────────── + const handleCanvasDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + canvasDragCounterRef.current += 1; + if (canvasDragCounterRef.current === 1) { + setIsCanvasDragging(true); + } + }, []); + + const handleCanvasDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + canvasDragCounterRef.current -= 1; + if (canvasDragCounterRef.current <= 0) { + canvasDragCounterRef.current = 0; + setIsCanvasDragging(false); + } + }, []); + + const handleCanvasDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }, []); + + const handleCanvasDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + canvasDragCounterRef.current = 0; + setIsCanvasDragging(false); + const files = Array.from(e.dataTransfer.files).filter( + (f) => f.type.startsWith("image/") + ); + if (files.length === 0) return; + + const rect = canvasRef.current?.getBoundingClientRect(); + if (!rect) return; + const dropPosition = { + x: (e.clientX - rect.left - canvasViewport.x) / canvasViewport.zoom, + y: (e.clientY - rect.top - canvasViewport.y) / canvasViewport.zoom, + }; + + let offsetX = 0; + let offsetY = 0; + for (const file of files) { + const imageUrl = URL.createObjectURL(file); + addImageNode(imageUrl, file.name, { + x: dropPosition.x + offsetX, + y: dropPosition.y + offsetY, + }); + offsetX += 60; + offsetY += 60; + } + setContextMenu(null); + setNodeMenu(null); + }, + [canvasViewport.x, canvasViewport.y, canvasViewport.zoom, addImageNode], + ); + + // ── Text composer drag-and-drop ────────────────────────────────── + const [textComposerDragNodeId, setTextComposerDragNodeId] = useState(null); + const textComposerDragCounterRef = useRef(0); + + const handleTextComposerDragEnter = useCallback((_e: React.DragEvent, nodeId: string) => { + _e.preventDefault(); + _e.stopPropagation(); + textComposerDragCounterRef.current += 1; + if (textComposerDragCounterRef.current === 1) { + setTextComposerDragNodeId(nodeId); + } + }, []); + + const handleTextComposerDragLeave = useCallback((_e: React.DragEvent) => { + _e.preventDefault(); + _e.stopPropagation(); + textComposerDragCounterRef.current -= 1; + if (textComposerDragCounterRef.current <= 0) { + textComposerDragCounterRef.current = 0; + setTextComposerDragNodeId(null); + } + }, []); + + const handleTextComposerDragOver = useCallback((_e: React.DragEvent) => { + _e.preventDefault(); + _e.stopPropagation(); + }, []); + + const handleTextComposerDrop = useCallback( + (e: React.DragEvent, sourceNode: CanvasTextNode) => { + e.preventDefault(); + e.stopPropagation(); + textComposerDragCounterRef.current = 0; + setTextComposerDragNodeId(null); + const files = Array.from(e.dataTransfer.files).filter( + (f) => f.type.startsWith("image/") + ); + if (files.length === 0) return; + + let offsetX = 0; + let offsetY = 0; + for (const file of files) { + const imageUrl = URL.createObjectURL(file); + addImageNode(imageUrl, file.name, { + x: sourceNode.position.x + sourceNode.size.width + 40 + offsetX, + y: sourceNode.position.y + offsetY, + }, { sourceTextNodeId: sourceNode.id }); + offsetX += 60; + offsetY += 60; + } + }, + [addImageNode], + ); + const activeTextNode = textNodeMenu ? textNodes.find((node) => node.id === textNodeMenu.nodeId) ?? null : null; @@ -3550,7 +3667,7 @@ function CanvasPage({
event.preventDefault() : handleCanvasContextMenu} @@ -3558,6 +3675,10 @@ function CanvasPage({ onDoubleClick={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDoubleClick} onMouseMove={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasMouseMove} onWheel={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasWheel} + onDragEnter={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragEnter} + onDragOver={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragOver} + onDragLeave={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDragLeave} + onDrop={(shouldShowEmptyProjectState || isWaitingForProjects) ? undefined : handleCanvasDrop} style={{ "--canvas-bg-size": `${34 * canvasViewport.zoom}px`, "--canvas-bg-dot": `${1.35 * canvasViewport.zoom}px`, @@ -4137,7 +4258,13 @@ function CanvasPage({ }; return ( -
+
handleTextComposerDragEnter(e, textNode.id)} + onDragOver={handleTextComposerDragOver} + onDragLeave={handleTextComposerDragLeave} + onDrop={(e) => handleTextComposerDrop(e, textNode)} + >