The left column is gone. The details are one column: the flow, a row of tabs under it (the workflow's own values, each preset with how many values it changes, + to save the canvas or copy the one shown, all pictures and versions at the end; it stays in view while the panel scrolls) and the tab chosen in a panel joined to it. The flow is small and shows only the structure: the stages in the order they run, then the finishing nodes and the saves; above them one lane each for the model files, the LoRAs and the control models, each a bar over the stages it feeds (recipe_flow.flowLanes, tested). Steps, splits and strengths moved into the boxes' tooltips; the legend and the hint line are gone. What the preset shown changes from the workflow's own is orange, and its head says how many values that is. A box's values open under the flow only once one is picked, with x to close. The set rows' key-value chips went with the column; the tabs count the differences with ui_param_set_keys.setDifferences. Remaining "方案" labels in the preset menus now say 预设. Checked on the isolated test ComfyUI with the two-pass Anima card: each preset marks the right boxes, the inspector, the + menu and its editor, all pictures, versions, sticky tabs, and the narrow (docked) layout. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
3.5 KiB
JavaScript
79 lines
3.5 KiB
JavaScript
/**
|
|
* Presets (预设) at a glance. A recipe card says how many presets it has, and its chip opens the
|
|
* recipe on its first one. The details' preset tabs and a preset's head say how many values it
|
|
* sets unlike the recipe's own (setDifferences). Each workflow is read once and kept while its
|
|
* data lives.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { readDetached } from './detached_graph.js';
|
|
import { compareParams, readParams } from './param_compare.js';
|
|
|
|
const read = new WeakMap(); // workflow object → readParams
|
|
|
|
/** A workflow's values as readParams reads them, read once per workflow object. */
|
|
export function workflowParams(workflow) {
|
|
if (!read.has(workflow)) read.set(workflow, readDetached(workflow, readParams));
|
|
return read.get(workflow);
|
|
}
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
/** { recipe filename → its sets' value signatures }, read once per list drawn; {} when it cannot be read. */
|
|
export function fetchParameterSetSignatures() {
|
|
return fetch('/anomalous/parameters/signatures', { cache: 'no-store' })
|
|
.then(response => (response.ok ? response.json() : {}))
|
|
.then(data => (data && typeof data.sets === 'object' && data.sets) || {})
|
|
.catch(() => ({}));
|
|
}
|
|
|
|
/**
|
|
* The recipe card's 参数方案 N chip, once the signatures are here: N counts the sets whose values
|
|
* differ from the recipe's (saving a recipe keeps a set of its own values too). It opens the
|
|
* recipe on its first set.
|
|
*/
|
|
export function showParameterSetCount(owner, card, recipe, signatures) {
|
|
signatures.then((map) => {
|
|
const own = recipe?.data?.parameter_signature?.value;
|
|
const sets = Array.isArray(map[recipe?.filename]) ? map[recipe.filename] : [];
|
|
const count = sets.filter(signature => !own || signature !== own).length;
|
|
const line = card.querySelector('.anomalous-wf-extras');
|
|
if (!count || !line) return;
|
|
const chip = el('button', 'anomalous-recipe-set-chip', `▤ ${count}`);
|
|
chip.type = 'button';
|
|
chip.title = `${t('paramSetCount', { count })} · ${t('paramSetCountHint')}`;
|
|
chip.setAttribute('aria-label', chip.title);
|
|
chip.onclick = (event) => {
|
|
event.stopPropagation();
|
|
owner.recipeReturnState = { activeTab: 'set', firstSet: true };
|
|
card.click();
|
|
};
|
|
line.append(chip);
|
|
});
|
|
}
|
|
|
|
/** A new set's name: the recipe's name and the first number from 2 no set of it has yet (`Name 2`). */
|
|
export function nextSetName(recipe, notebooks) {
|
|
const base = String(recipe?.name || t('recipeUntitled')).trim();
|
|
const taken = new Set((notebooks || []).map(notebook => String(notebook?.name || '').trim()));
|
|
for (let n = 2; ; n += 1) if (!taken.has(`${base} ${n}`)) return `${base} ${n}`;
|
|
}
|
|
|
|
/** How many values preset `notebook` sets unlike its workflow `recipe` (a seed's after-run choice aside); null when they cannot be compared. */
|
|
export function setDifferences(notebook, recipe) {
|
|
const setWorkflow = notebook?.data?.workflow;
|
|
if (!setWorkflow || !recipe?.workflow) return null;
|
|
try {
|
|
const result = compareParams(workflowParams(recipe.workflow), workflowParams(setWorkflow));
|
|
return result.nodes.reduce((sum, node) => sum + node.rows.filter(row => !row.volatile && !row.same).length, 0);
|
|
} catch (error) {
|
|
console.warn('[AMB] Parameter set not compared with its recipe.', error);
|
|
return null;
|
|
}
|
|
}
|