feat(canvas): add canvas default image count configuration and update model picker capabilities

This commit is contained in:
HouYunFei
2026-06-02 17:07:31 +08:00
parent 7bfa5a2bd9
commit 39793e890d
10 changed files with 88 additions and 43 deletions
@@ -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),
};
}
@@ -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({
<AssistantModeSwitch mode={mode} theme={theme} onChange={onModeChange} />
{mode === "image" ? (
<>
<ModelPicker className="h-8 shrink-0" config={config} value={config.imageModel || config.model} onChange={(model) => onConfigChange("imageModel", model)} onMissingConfig={onMissingConfig} />
<ModelPicker className="h-8 shrink-0" config={config} value={config.imageModel || config.model} onChange={(model) => onConfigChange("imageModel", model)} capability="image" onMissingConfig={onMissingConfig} />
<CanvasImageSettingsPopover config={config} placement="topRight" getPopupContainer={() => document.body} buttonClassName="canvas-composer-settings canvas-composer-icon !h-8 !min-w-8 !rounded-full !px-2" onConfigChange={onConfigChange} onMissingConfig={onMissingConfig} />
</>
) : (
<ModelPicker className="h-8 shrink-0" config={config} value={config.textModel || config.model} onChange={(model) => onConfigChange("textModel", model)} onMissingConfig={onMissingConfig} />
<ModelPicker className="h-8 shrink-0" config={config} value={config.textModel || config.model} onChange={(model) => onConfigChange("textModel", model)} capability="text" onMissingConfig={onMissingConfig} />
)}
</div>
<Button
@@ -38,7 +38,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, inputs, o
const theme = canvasThemes[useThemeStore((state) => state.theme)];
const mode = node.metadata?.generationMode || "image";
const config = buildNodeConfig(globalConfig, node, mode);
const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(node.metadata?.count || 3)) || 1)));
const count = Math.max(1, Math.min(15, Math.floor(Math.abs(Number(config.count)) || 1)));
const credits = requestCreditCost({ channelMode: config.channelMode, modelCosts, model: config.model, count: mode === "image" ? count : 1 });
const chipStyle = { background: theme.node.fill, borderColor: theme.node.stroke, color: theme.node.text };
const textInputs = inputs.filter((input) => input.type === "text");
@@ -125,7 +125,7 @@ export function CanvasConfigNodePanel({ node, isRunning, inputSummary, inputs, o
</div>
<div className={`mb-2 grid min-w-0 cursor-default items-center gap-2 ${mode === "text" ? "grid-cols-1" : "grid-cols-[minmax(0,1fr)_148px]"}`} onMouseDown={(event) => event.stopPropagation()}>
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} onMissingConfig={() => openConfigDialog(true)} fullWidth />
<ModelPicker className="canvas-compact-control h-10" config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability={mode} onMissingConfig={() => openConfigDialog(true)} fullWidth />
{mode === "video" ? (
<CanvasVideoSettingsPopover config={config} placement="topRight" buttonClassName="canvas-compact-control !h-10 !w-full !justify-start !rounded-lg !px-2" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
) : mode === "image" ? (
@@ -405,7 +405,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark || defaultConfig.videoWatermark,
count: String(node.metadata?.count || (mode === "image" ? 3 : globalConfig.count) || defaultConfig.count),
count: String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
};
}
@@ -80,7 +80,7 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
<CanvasPromptLibrary onSelect={updatePrompt} />
{mode === "image" ? (
<>
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} onMissingConfig={() => openConfigDialog(true)} />
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability="image" onMissingConfig={() => openConfigDialog(true)} />
<CanvasImageSettingsPopover
config={config}
placement="topLeft"
@@ -92,11 +92,11 @@ export function CanvasNodePromptPanel({ node, isRunning, onPromptChange, onConfi
</>
) : mode === "video" ? (
<>
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} onMissingConfig={() => openConfigDialog(true)} />
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability="video" onMissingConfig={() => openConfigDialog(true)} />
<CanvasVideoSettingsPopover config={config} buttonClassName="!h-10 !max-w-[170px] !justify-start !rounded-full !px-3" onConfigChange={(key, value) => onConfigChange(node.id, videoConfigPatch(key, value))} />
</>
) : (
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} onMissingConfig={() => openConfigDialog(true)} />
<ModelPicker config={config} value={config.model} onChange={(model) => onConfigChange(node.id, { model })} capability="text" onMissingConfig={() => openConfigDialog(true)} />
)}
</div>
<Button
@@ -134,7 +134,7 @@ function buildNodeConfig(globalConfig: AiConfig, node: CanvasNodeData, mode: Can
vquality: node.metadata?.vquality || globalConfig.vquality || defaultConfig.vquality,
videoGenerateAudio: node.metadata?.generateAudio || globalConfig.videoGenerateAudio || defaultConfig.videoGenerateAudio,
videoWatermark: node.metadata?.watermark || globalConfig.videoWatermark || defaultConfig.videoWatermark,
count: String(node.metadata?.count || (mode === "image" ? 3 : globalConfig.count) || defaultConfig.count),
count: String(node.metadata?.count || (mode === "image" ? globalConfig.canvasImageCount || globalConfig.count : globalConfig.count) || defaultConfig.count),
};
}