From 39793e890dee266c1ee2d5c924340ff6895dc8bb Mon Sep 17 00:00:00 2001 From: HouYunFei <1844025705@qq.com> Date: Tue, 2 Jun 2026 17:07:31 +0800 Subject: [PATCH] feat(canvas): add canvas default image count configuration and update model picker capabilities --- docs/content/docs/progress/pending-test.mdx | 1 + .../(user)/canvas/[id]/canvas-client-page.tsx | 14 +++---- .../components/canvas-assistant-panel.tsx | 11 +++--- .../components/canvas-config-node-panel.tsx | 6 +-- .../components/canvas-node-prompt-panel.tsx | 8 ++-- web/src/app/(user)/image/page.tsx | 2 +- web/src/app/(user)/video/page.tsx | 2 +- .../components/layout/app-config-modal.tsx | 33 ++++++++++++---- web/src/components/model-picker.tsx | 15 +++++-- web/src/stores/use-config-store.ts | 39 +++++++++++++------ 10 files changed, 88 insertions(+), 43 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index ac212c3..2a56905 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,7 @@ description: 当前版本已实现但仍需人工验证的变更项 # 待测试 +- 配置弹窗新增“画布默认生图张数”,新建画布生图和配置节点会读取该值;模型下拉会按生图、视频、文本能力过滤可选模型,需要验证远程渠道和本地直连模型列表都能正确显示。 - 画布左上角菜单和右上角状态栏新增“文档”入口,会使用 `NEXT_PUBLIC_DOC_URL` 配置的地址并在新标签打开文档站;需要验证登录和未登录状态下顶部入口都可见。 - 文档站搜索改为中英文混合 tokenizer,中文正文、标题和短语会按中文词、单字、二元和三元片段建立索引;需要验证 `/api/search` 和搜索弹窗能命中文档中的中文关键词。 - 文档站改为 Next.js standalone server 输出,新增 `docs/Dockerfile`、`docs/docker-compose.yml` 和 `docs/docker-compose.local.yml` 独立运行入口;需要验证文档页、搜索接口和 LLM 文本接口在文档站容器中可访问。 diff --git a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx index 776de60..733970d 100644 --- a/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx +++ b/web/src/app/(user)/canvas/[id]/canvas-client-page.tsx @@ -494,7 +494,7 @@ function InfiniteCanvasPage() { const createConnectedNode = useCallback( (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio, pending: PendingConnectionCreate) => { - const metadata = type === CanvasNodeType.Config ? { model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, count: 3 } : undefined; + const metadata = type === CanvasNodeType.Config ? { model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count) } : undefined; const newNode = createCanvasNode(type, pending.position, metadata); const connection = normalizeConnection(pending.connection.nodeId, newNode.id, [...nodesRef.current, newNode], pending.connection.handleType); if (!connection) { @@ -509,7 +509,7 @@ function InfiniteCanvasPage() { setPendingConnectionCreate(null); setConnecting(null); }, - [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, setConnecting], + [effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, setConnecting], ); const cancelPendingConnectionCreate = useCallback(() => { @@ -625,7 +625,7 @@ function InfiniteCanvasPage() { ? { model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, - count: 3, + count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count), } : undefined; const newNode = createCanvasNode(type, targetPosition, configMetadata); @@ -635,7 +635,7 @@ function InfiniteCanvasPage() { setSelectedConnectionId(null); if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id); }, - [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, getCanvasCenter], + [effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, getCanvasCenter], ); const deleteNodes = useCallback( @@ -1990,7 +1990,7 @@ function InfiniteCanvasPage() { prompt: "", model: effectiveConfig.imageModel || effectiveConfig.model, size: effectiveConfig.size, - count: 3, + count: getGenerationCount(effectiveConfig.canvasImageCount || effectiveConfig.count), }, ); const connection = { id: nanoid(), fromNodeId: sourceNode.id, toNodeId: configNode.id }; @@ -2004,7 +2004,7 @@ function InfiniteCanvasPage() { setSelectedConnectionId(null); setDialogNodeId(configNode.id); }, - [effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message], + [effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message], ); const insertAssistantImage = useCallback( @@ -2707,7 +2707,7 @@ function buildGenerationConfig(config: AiConfig, node: CanvasNodeData | undefine vquality: node?.metadata?.vquality || config.vquality || defaultConfig.vquality, videoGenerateAudio: node?.metadata?.generateAudio || config.videoGenerateAudio || defaultConfig.videoGenerateAudio, videoWatermark: node?.metadata?.watermark || config.videoWatermark || defaultConfig.videoWatermark, - count: String(node?.metadata?.count || (mode === "image" ? 3 : config.count) || defaultConfig.count), + count: String(node?.metadata?.count || (mode === "image" ? config.canvasImageCount || config.count : config.count) || defaultConfig.count), }; } diff --git a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx index 0b10338..edaffb8 100644 --- a/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx +++ b/web/src/app/(user)/canvas/components/canvas-assistant-panel.tsx @@ -80,6 +80,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeS const selectedNodeKey = useMemo(() => Array.from(selectedNodeIds).sort().join(","), [selectedNodeIds]); const allSelectedReferences = useMemo(() => buildAssistantReferences(nodes, selectedNodeIds), [nodes, selectedNodeIds]); const selectedReferences = useMemo(() => allSelectedReferences.filter((item) => !removedReferenceIds.has(item.id)), [allSelectedReferences, removedReferenceIds]); + const assistantConfig = useMemo(() => ({ ...effectiveConfig, count: effectiveConfig.canvasImageCount || effectiveConfig.count }), [effectiveConfig]); const iconButtonStyle = { color: theme.node.muted }; useEffect(() => { @@ -140,7 +141,7 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeS }; const sendMessage = async (text: string, nextMode: AssistantMode, history: CanvasAssistantMessage[], savedReferences?: CanvasAssistantReference[]) => { - const requestConfig = { ...effectiveConfig, model: nextMode === "image" ? effectiveConfig.imageModel || effectiveConfig.model : effectiveConfig.textModel || effectiveConfig.model }; + const requestConfig = { ...effectiveConfig, count: nextMode === "image" ? effectiveConfig.canvasImageCount || effectiveConfig.count : effectiveConfig.count, model: nextMode === "image" ? effectiveConfig.imageModel || effectiveConfig.model : effectiveConfig.textModel || effectiveConfig.model }; if (!isAiConfigReady(requestConfig, requestConfig.model)) { openConfigDialog(true); return; @@ -319,11 +320,11 @@ export function CanvasAssistantPanel({ nodes, selectedNodeIds, sessions, activeS prompt={prompt} isRunning={isRunning} references={selectedReferences} - config={effectiveConfig} + config={assistantConfig} onModeChange={setMode} onPromptChange={setPrompt} onSubmit={submit} - onConfigChange={updateConfig} + onConfigChange={(key, value) => updateConfig(key === "count" ? "canvasImageCount" : key, value)} onMissingConfig={() => openConfigDialog(true)} onRemoveReference={(id) => { setRemovedReferenceIds((prev) => new Set(prev).add(id)); @@ -429,11 +430,11 @@ function AssistantComposer({ {mode === "image" ? ( <> - onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} /> + onConfigChange("imageModel", model)} capability="image" onMissingConfig={onMissingConfig} /> document.body} buttonClassName="canvas-composer-settings canvas-composer-icon !h-8 !min-w-8 !rounded-full !px-2" onConfigChange={onConfigChange} onMissingConfig={onMissingConfig} /> ) : ( - onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} /> + onConfigChange("textModel", model)} capability="text" onMissingConfig={onMissingConfig} /> )}