The Node Assistant becomes "Current node" (当前节点): one page instead of the "actions" / "parameter sets" tabs, model actions on top, Parameters below. - node_parameter_sets.js: the node type's saved values from the material library and from recipes' parameter sets as one list; the same values saved in both are one entry naming both sources. For the selected node, exactly what an entry would change; seeds (and their after-run control), model files and choices this computer lacks are kept. - ui_node_parameters.js: the list, "apply these N" (one undo in the receipt) and "save these values". - api/node_material.py: POST /anomalous/save_node_material keeps one node's values as a node_parameter_selection material, deduplicated by type and values; the library lists it under parameters. - Removed: ui_node_presets.js with its hover popovers, the beta notice and their styles and strings. The panel and help texts use the new name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.9 KiB
JavaScript
131 lines
5.9 KiB
JavaScript
/**
|
|
* The current-node panel's parameters: every saved set of values for the node's type
|
|
* (node_parameter_sets.js: materials and recipes, merged), each with exactly what it would
|
|
* change on this node; one press puts those in (seeds and model files stay, the receipt
|
|
* undoes it). "Save these values" keeps the node's current values as a material.
|
|
*/
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { translate as t } from './locales.js';
|
|
import { anomalousPrompt } from './ui_dialog.js';
|
|
import { showMaterialApplication } from './ui_material_application.js';
|
|
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
|
import { applyParameterChanges, loadParameterSets, parameterChanges, saveNodeParameters } from './node_parameter_sets.js';
|
|
|
|
const SHOWN_CHANGES = 4;
|
|
const SEARCH_FROM = 6; // entries before a search box is worth showing
|
|
const ROLE_KEYS = { positive: 'recipePromptRolePositive', negative: 'recipePromptRoleNegative', both: 'recipePromptRoleBoth' };
|
|
|
|
function el(tag, className, text) {
|
|
const node = document.createElement(tag);
|
|
if (className) node.className = className;
|
|
if (text !== undefined) node.textContent = text;
|
|
return node;
|
|
}
|
|
|
|
function button(className, label, onClick) {
|
|
const node = el('button', className, label);
|
|
node.type = 'button';
|
|
node.onclick = onClick;
|
|
return node;
|
|
}
|
|
|
|
const brief = (value) => {
|
|
const text = typeof value === 'string' ? value.replace(/\s+/g, ' ') : JSON.stringify(value);
|
|
return text.length > 48 ? `${text.slice(0, 47)}…` : text;
|
|
};
|
|
|
|
const sourceLabel = source => (source.kind === 'recipe'
|
|
? t('currentNodeFromRecipe', { name: source.label })
|
|
: t('currentNodeFromMaterial'));
|
|
|
|
function entryCard(node, entry, receiptHost, redraw) {
|
|
const changes = parameterChanges(node, entry.values);
|
|
const made = changes.filter(change => !change.keep);
|
|
const card = el('div', 'anomalous-node-param');
|
|
const head = el('div', 'anomalous-node-param-head');
|
|
head.append(el('strong', 'anomalous-node-param-name', entry.name));
|
|
if (ROLE_KEYS[entry.role]) head.append(el('span', `anomalous-node-param-role is-${entry.role}`, t(ROLE_KEYS[entry.role])));
|
|
const sources = el('div', 'anomalous-node-param-sources');
|
|
for (const label of new Set(entry.sources.map(sourceLabel))) sources.append(el('span', '', label));
|
|
card.append(head, sources);
|
|
|
|
if (made.length) {
|
|
const list = el('ul', 'anomalous-node-param-changes');
|
|
for (const change of made.slice(0, SHOWN_CHANGES)) {
|
|
const item = el('li', '', `${change.label}: ${brief(change.from)} → ${brief(change.to)}`);
|
|
item.title = `${change.label}: ${change.to}`;
|
|
list.append(item);
|
|
}
|
|
if (made.length > SHOWN_CHANGES) list.append(el('li', 'is-more', t('currentNodeMoreChanges', { count: made.length - SHOWN_CHANGES })));
|
|
card.append(list);
|
|
}
|
|
const kept = changes.filter(change => change.keep);
|
|
if (kept.length) card.append(el('p', 'anomalous-node-param-kept', t('currentNodeKept', { names: kept.map(change => change.label).join(', ') })));
|
|
|
|
const apply = button('anomalous-node-param-apply', made.length ? t('currentNodeApply', { count: made.length }) : t('currentNodeSame'), () => {
|
|
try {
|
|
const result = applyParameterChanges(app, node, made);
|
|
showMaterialApplication(receiptHost, { undo() { result.undo(); redraw(); } }, node);
|
|
redraw();
|
|
} catch (error) {
|
|
showWorkbenchToast(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message));
|
|
}
|
|
});
|
|
apply.disabled = !made.length;
|
|
card.append(apply);
|
|
return card;
|
|
}
|
|
|
|
/** Appends the parameters section for `node` to `container` and loads its entries. */
|
|
export function renderNodeParameters(node, container) {
|
|
const section = el('section', 'anomalous-node-section');
|
|
const head = el('div', 'anomalous-node-section-head');
|
|
const search = el('input', 'anomalous-node-param-search');
|
|
search.type = 'search';
|
|
search.placeholder = t('currentNodeSearch');
|
|
search.hidden = true;
|
|
const list = el('div', 'anomalous-node-param-list');
|
|
list.append(el('p', 'anomalous-node-muted', t('loading')));
|
|
section.append(head, search, list);
|
|
container.append(section);
|
|
|
|
let entries = [];
|
|
const draw = () => {
|
|
const query = search.value.trim().toLowerCase();
|
|
const shown = entries.filter(entry => !query
|
|
|| `${entry.name} ${entry.sources.map(source => source.label).join(' ')}`.toLowerCase().includes(query));
|
|
search.hidden = entries.length < SEARCH_FROM;
|
|
list.replaceChildren(...(shown.length
|
|
? shown.map(entry => entryCard(node, entry, section, draw))
|
|
: [el('p', 'anomalous-node-muted', t(entries.length ? 'currentNodeNoMatch' : 'currentNodeNoSets'))]));
|
|
};
|
|
search.oninput = draw;
|
|
|
|
const load = async () => {
|
|
const run = (section._load = (section._load || 0) + 1);
|
|
try {
|
|
const loaded = await loadParameterSets(node.type);
|
|
if (run !== section._load || !section.isConnected) return;
|
|
entries = loaded;
|
|
draw();
|
|
} catch (error) {
|
|
if (run === section._load) list.replaceChildren(el('p', 'anomalous-node-muted is-error', t('currentNodeLoadFailed')));
|
|
}
|
|
};
|
|
|
|
const save = async () => {
|
|
const name = await anomalousPrompt(t('currentNodeSaveName'), node.title || node.type, t('currentNodeSave'));
|
|
if (name === null) return;
|
|
try {
|
|
const result = await saveNodeParameters(node, name || node.title || node.type);
|
|
showWorkbenchToast(t(result.status === 'duplicate' ? 'currentNodeSavedBefore' : 'currentNodeSaved', { name: result.name }));
|
|
if (section.isConnected) load();
|
|
} catch (error) {
|
|
showWorkbenchToast(t('currentNodeSaveFailed'));
|
|
}
|
|
};
|
|
head.append(el('h3', '', t('currentNodeParams')), button('anomalous-node-section-btn', t('currentNodeSave'), save));
|
|
load();
|
|
}
|