From e44c0c4b3ee9b0682547334a3722763445b2c789 Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 18 Aug 2026 14:16:28 +0800 Subject: [PATCH] feat(canvas): enhance multi-image generation and user control over expanded states --- CHANGELOG.md | 1 + docs/content/docs/progress/pending-test.mdx | 4 ++-- web/src/pages/canvas/project.tsx | 19 +++++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 313f155..e071c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased + [新增] 提示词来源新增 Freestylefly GPT Image 2 内置来源。 ++ [优化] 画布多图展开状态改为由用户主动控制,操作画布其他区域时不再自动收起。 ## v0.15.1 - 2026-08-07 diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 027efc4..5994754 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -12,10 +12,10 @@ The current release needs manual verification in these areas: - Global Dropdown, Menu, Select, Cascader, and TreeSelect popup backgrounds, hover states, and selected states in both light and dark themes. - Canvas node resize stability, prompt scrolling, generated-prompt restoration, edit-prompt persistence after closing an existing image or text node panel, expanded prompt editing with live synchronization and `@` references, image editing, drag-and-drop references, and generation configuration. - Canvas side panel: clicking an element row should smoothly center and select its node without zooming above 100%; image elements with content should still open the large preview without triggering canvas focus. -- Multi-image canvas generation: starting N images should immediately create one image node with N expandable slots and one incoming connection; each empty slot should independently show generating or failure while requests are running, every slot with image content should always display that image, and the first successful image should appear as the primary image immediately. The collapsed state should use a compact stack with at most three closely spaced backplates and a clearly legible count control; clicking the control should hide the floating toolbar and expand the slots upward and to the right. Every completed secondary image should keep a clearly legible primary-image action visible in both themes, and clicking the empty canvas should collapse the group. +- Multi-image canvas generation: starting N images should immediately create one image node with N expandable slots and one incoming connection; each empty slot should independently show generating or failure while requests are running, every slot with image content should always display that image, and the first successful image should appear as the primary image immediately. The collapsed state should use a compact stack with at most three closely spaced backplates and a clearly legible count control; clicking the control should hide the floating toolbar and expand the slots upward and to the right. Every completed secondary image should keep a clearly legible primary-image action visible in both themes. Expanded groups should remain expanded when clicking elsewhere, dragging, or resizing, multiple groups may remain expanded together, and only an explicit click or double-click should collapse them. - Multi-image primary selection: choosing another primary image should preserve the node center and current maximum edge while adapting to the new image ratio; free-resize nodes should keep their exact dimensions, and the viewport zoom indicator must not change. - Failed multi-image slots: every failed slot, including the current primary slot, should expose retry and delete actions. Retry should update only that slot without replacing an existing successful primary image; delete should remove only that failed slot and update the displayed count. -- Multi-image resize: starting a resize while child images are expanded should collapse the group once, then resize the root node normally without triggering nested React updates or an application error. +- Multi-image resize: resizing the root node while child images are expanded should keep the group expanded and resize normally without triggering nested React updates or an application error. - Multi-image alternatives: expanded images should use up to four columns so larger batches stay compact; every completed image, including the primary image, should provide an independent download action, while alternatives should also keep clearly legible create-copy and set-primary actions in both themes. Create-copy should add an independent image node to the right of the group, select it, and leave the original image group unchanged. - Canvas Agent startup, MCP status, model and permission controls, approvals, version reporting, diagnostics, streaming responses, history consistency, multi-tab isolation, and local Skill management. - Agent header tabs in English and Chinese at different panel widths; labels should collapse to icons and counts before either edge is clipped. diff --git a/web/src/pages/canvas/project.tsx b/web/src/pages/canvas/project.tsx index ef64651..bdb0942 100644 --- a/web/src/pages/canvas/project.tsx +++ b/web/src/pages/canvas/project.tsx @@ -235,7 +235,7 @@ function InfiniteCanvasPage() { const [titleEditing, setTitleEditing] = useState(false); const [titleDraft, setTitleDraft] = useState(""); const [historyState, setHistoryState] = useState({ canUndo: false, canRedo: false }); - const [expandedImageNodeId, setExpandedImageNodeId] = useState(null); + const [expandedImageNodeIds, setExpandedImageNodeIds] = useState>(new Set()); const [isNodeDragging, setIsNodeDragging] = useState(false); const [isNodeResizing, setIsNodeResizing] = useState(false); const [dropTargetGroupId, setDropTargetGroupId] = useState(null); @@ -719,6 +719,7 @@ function InfiniteCanvasPage() { setAngleNodeId((current) => (current && allIds.has(current) ? null : current)); setPreviewNodeId((current) => (current && allIds.has(current) ? null : current)); setRunningNodeId((current) => (current && allIds.has(current) ? null : current)); + setExpandedImageNodeIds((current) => new Set([...current].filter((nodeId) => !allIds.has(nodeId)))); setContextMenu((current) => (current?.type === "node" && allIds.has(current.nodeId) ? null : current)); cleanupCanvasFiles({ projectId, nodes: nodesRef.current.filter((node) => !allIds.has(node.id)), chatSessions }); }, @@ -733,7 +734,6 @@ function InfiniteCanvasPage() { const deselectCanvas = useCallback(() => { cancelPendingConnectionCreate(); - setExpandedImageNodeId(null); setSelectedNodeIds(new Set()); setSelectedConnectionId(null); setContextMenu(null); @@ -974,7 +974,6 @@ function InfiniteCanvasPage() { (event: ReactPointerEvent) => { setContextMenu(null); setNodeCreatePosition(null); - setExpandedImageNodeId(null); setHoveredNodeId(null); setToolbarNodeId(null); setDialogNodeId(null); @@ -1427,7 +1426,6 @@ function InfiniteCanvasPage() { const handleNodeResizeStart = useCallback(() => { setIsNodeResizing(true); - setExpandedImageNodeId(null); }, []); const handleNodeResizeEnd = useCallback(() => setIsNodeResizing(false), []); @@ -1453,7 +1451,12 @@ function InfiniteCanvasPage() { }, []); const toggleBatchExpanded = useCallback((nodeId: string) => { - setExpandedImageNodeId((current) => (current === nodeId ? null : nodeId)); + setExpandedImageNodeIds((current) => { + const next = new Set(current); + if (next.has(nodeId)) next.delete(nodeId); + else next.add(nodeId); + return next; + }); }, []); const setBatchPrimary = useCallback((nodeId: string, imageId: string) => { @@ -2560,7 +2563,7 @@ function InfiniteCanvasPage() { const deleteBatchImage = useCallback((nodeId: string, imageId: string) => { const node = nodesRef.current.find((item) => item.id === nodeId); - if ((node?.metadata?.images?.length || 0) <= 2) setExpandedImageNodeId(null); + if ((node?.metadata?.images?.length || 0) <= 2) setExpandedImageNodeIds((current) => new Set([...current].filter((id) => id !== nodeId))); setNodes((prev) => prev.map((item) => { if (item.id !== nodeId) return item; @@ -2838,7 +2841,7 @@ function InfiniteCanvasPage() { showPanel={dialogNodeId === node.id && !selectionBox && !getNodeDefinition(node.type)?.hidePanel} groupChildCount={groupChildCountById.get(node.id) || 0} isGroupDropTarget={dropTargetGroupId === node.id} - batchExpanded={expandedImageNodeId === node.id} + batchExpanded={expandedImageNodeIds.has(node.id)} showImageInfo={showImageInfo} mentionReferences={mentionReferencesByNodeId.get(node.id) || EMPTY_REFERENCES} pluginHost={pluginHost} @@ -2895,7 +2898,7 @@ function InfiniteCanvasPage() {