Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_node_parameters.js
T
DemonGatanjieuandClaude Opus 5.5 1d02d5b74d Remove the Material Library page; Prompts opens Prompt Studio
The page held four unrelated things, so each now lives where it is used:
the rail's Prompts entry (and the Ctrl+Shift+L shortcut) opens Prompt
Studio beside the canvas, where saved prompts are managed; the browser
comes back when the studio closes. Saved node values stay in Current
node, which can now delete them; moved whole workflows are no longer
offered there twice. Recent results, combos and whole workflows already
have their own homes.

Deleted ui_materials.js, ui_material_cards.js, ui_material_detail.js,
ui_material_application.js (its receipt is ui_apply_receipt.js) and
material_feedback.js, the recipe page's copy-to-library buttons and the
save_parameter_material route, with about 2,000 lines of CSS and 200 locale
keys. Words that said "material library" now say where things go.
Material files are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:59:35 +08:00

147 lines
6.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); saved values (not a recipe's) can be deleted. "Save these values" keeps the
* node's current values for the next node of its kind.
*/
import { app } from "../../../scripts/app.js";
import { translate as t } from './locales.js';
import { anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
import { showApplyReceipt } from './ui_apply_receipt.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
import { applyParameterChanges, deleteSavedValues, 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, remove) {
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);
showApplyReceipt(receiptHost, { undo() { result.undo(); redraw(); } }, node);
redraw();
} catch (error) {
showWorkbenchToast(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message));
}
});
apply.disabled = !made.length;
const actions = el('div', 'anomalous-node-param-actions');
// Values saved here or kept from images can be deleted; a recipe's sets belong to the recipe.
const saved = [...new Set(entry.sources.filter(source => source.kind === 'material' && source.filename).map(source => source.filename))];
if (saved.length) actions.append(button('anomalous-node-param-delete', t('currentNodeDelete'), () => remove(entry, saved)));
actions.append(apply);
card.append(actions);
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, remove))
: [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 remove = async (entry, filenames) => {
if (!await anomalousConfirm(t('currentNodeDeleteConfirm', { name: entry.name }), t('currentNodeDelete'), { okLabel: t('currentNodeDelete') })) return;
try {
for (const filename of filenames) await deleteSavedValues(filename);
} catch (error) {
showWorkbenchToast(t('currentNodeDeleteFailed'));
}
if (section.isConnected) load();
};
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();
}