- Every node value under the details' "every node's values" has a star; marked values show at
the top of the workflow's own values and of each set as tiles (node, value, a note, and on a
set the default when it differs). Set rows list them first; the comparison window leads its
key values with them and their note.
- Kept in the recipe's params as focusParams [{node, type, widget, note}]; the server refuses
any other shape (at most 12, short strings, notes up to 120 characters). Kept while the node
of that id and type is there, also when the workflow is updated from the canvas.
- compareParams' isKey now also receives the node ids, so a mark holds for one node only.
- Tests: tests/recipe_focus.mjs, focus values in test_recipe_roundtrip.py.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
133 lines
6.3 KiB
JavaScript
133 lines
6.3 KiB
JavaScript
/**
|
|
* Parameter sets (参数方案) at a glance. A recipe card says how many sets it has, and its
|
|
* chip opens the recipe on its first set. A set's row in the details shows its key parameters'
|
|
* values (the starred ones, key_params.js, and the workflow's focus values, recipe_focus.js;
|
|
* marked when they differ from the recipe's own), how many values differ from the recipe, and
|
|
* ⇄ to compare the two (ui_param_compare.js). Each workflow is read once and kept while its
|
|
* data lives.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { detachedGraph } from './detached_graph.js';
|
|
import { keyParamTest, onKeyParamsChange } from './key_params.js';
|
|
import { focusTest } from './recipe_focus.js';
|
|
import { compareParams, readParams, shownValue } from './param_compare.js';
|
|
import { openParamCompare } from './ui_param_compare.js';
|
|
|
|
const CHIP_CHARS = 24;
|
|
const read = new WeakMap(); // workflow object → readParams
|
|
const decorated = new WeakMap(); // a set's card → [notebook, recipe], to draw it again after a star changes
|
|
let following = false;
|
|
|
|
/** A star or a focus value changed: the set rows on screen show the new key values. */
|
|
export function redrawParameterSets() {
|
|
for (const row of document.querySelectorAll('.anomalous-wfd-set')) {
|
|
const args = decorated.get(row);
|
|
if (!args) continue;
|
|
row.querySelector('.anomalous-preset-item-keys')?.remove();
|
|
row.querySelector('.anomalous-preset-set-compare')?.remove();
|
|
decorateParameterSet(row, ...args);
|
|
}
|
|
}
|
|
|
|
/** A workflow's values as readParams reads them, read once per workflow object. */
|
|
export function workflowParams(workflow) {
|
|
if (!read.has(workflow)) read.set(workflow, readParams(detachedGraph(workflow)));
|
|
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;
|
|
}
|
|
|
|
const short = value => {
|
|
const text = shownValue(value);
|
|
return text.length > CHIP_CHARS ? `${text.slice(0, CHIP_CHARS - 1)}…` : text;
|
|
};
|
|
|
|
/** { 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}`;
|
|
}
|
|
|
|
/** Adds the key values, the count of differences and ⇄ to a set's row in the details (`row`, with its main and actions). */
|
|
export function decorateParameterSet(row, notebook, recipe) {
|
|
const setWorkflow = notebook?.data?.workflow;
|
|
if (!setWorkflow || !recipe?.workflow) return;
|
|
decorated.set(row, [notebook, recipe]);
|
|
if (!following) {
|
|
following = true;
|
|
onKeyParamsChange(redrawParameterSets);
|
|
}
|
|
const starred = keyParamTest();
|
|
const focus = focusTest(recipe);
|
|
let result;
|
|
try {
|
|
result = compareParams(workflowParams(recipe.workflow), workflowParams(setWorkflow), {
|
|
isKey: (type, widget, ...ids) => starred(type, widget) || Boolean(focus(type, widget, ...ids)),
|
|
});
|
|
} catch (error) {
|
|
console.warn('[AMB] Parameter set not compared with its recipe.', error);
|
|
return;
|
|
}
|
|
const counted = row => !row.volatile && !row.same;
|
|
const differing = result.nodes.reduce((sum, node) => sum + node.rows.filter(counted).length, 0)
|
|
+ result.keys.filter(({ row }) => counted(row)).length;
|
|
const line = el('div', 'anomalous-preset-item-keys');
|
|
for (const { node, row } of result.keys) {
|
|
const marked = focus(node.type, row.name, node.bId, node.aId);
|
|
const chip = el('span', `anomalous-preset-key-chip${row.same ? '' : ' is-differs'}${marked ? ' is-focus' : ''}`, `${row.name} ${short(row.b)}`);
|
|
chip.title = `${node.title} · ${row.name}: ${shownValue(row.a)} → ${shownValue(row.b)}${marked?.note ? `\n${marked.note}` : ''}`;
|
|
if (marked) line.prepend(chip);
|
|
else line.append(chip);
|
|
}
|
|
line.append(el('span', 'anomalous-preset-key-count', differing ? t('paramSetDiffers', { count: differing }) : t('paramSetSame')));
|
|
row.querySelector('.anomalous-wfd-set-main')?.append(line);
|
|
|
|
const compare = el('button', 'anomalous-preset-item-btn anomalous-preset-set-compare', '⇄');
|
|
compare.type = 'button';
|
|
compare.title = t('paramSetCompare');
|
|
compare.onclick = (event) => {
|
|
event.stopPropagation();
|
|
openParamCompare({ workflow: recipe.workflow, label: t('workflowOwnValues') }, { workflow: setWorkflow, label: notebook.name || t('recipeParameterUntitled') }, { focus });
|
|
};
|
|
row.querySelector('.anomalous-wfd-set-actions')?.prepend(compare);
|
|
} |