Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_node_parameters.js
T
DemonGatanjieuandClaude Opus 5.5 a173e4e561 feat(node): Current node panel with one list of saved parameters
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>
2026-10-02 06:09:49 +08:00

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();
}