Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_node_parameters.js
T
DemonGatanjieuandClaude Opus 5.5 6afbbf9059 List combos in Current node and put the rail's tools by use
Current node now reads what combos hold for the node's type: every box of
a node structure, by name, and a plain combo's prompt on a prompt box. They
get a chip of their own, a press goes to the combo, and the values go in
only on the user's press, as with the other sets (AD-021).

The rail's tools go by how often they are used: Current node, Model Check,
then the scan, and the tour follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:28:37 +08:00

502 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The current-node panel's parameters: every saved set of values for the node's type
* (node_parameter_sets.js: materials and recipes, merged), as cards or side by side. A card
* shows the picture made with its values (or the one they were kept from) and says where they
* came from, a chip per channel: a workflow's parameter set, a combo, a kept picture, or saved here. A
* press goes to the source; a chip of several lists them first. Then the day. Then each value it would change as a picture (numbers as two bars,
* choices as old → new) and what stays and why (seeds, model files, choices this computer does
* not have); "Show all" lists the equal values too. ⇄ opens the comparison window on this node
* alone, its values beside the set's; one press puts the changes in (the receipt undoes it); saved values (not a
* recipe's) can be deleted. Side by side: the current values and every set in columns, the
* ones a set would change marked. "Save these values" keeps the node's values for the next
* node of its kind. On top, one line on where the node's values came from: the set last put
* in here (marked on its card too) and what changed since, with a way to put them back; or,
* without that, the set the values match or come closest to.
*/
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 { openParamCompare } from './ui_param_compare.js';
import { openPopupMenu } from './ui_popup_menu.js';
import {
appliedState, applyParameterChanges, closestSet, deleteSavedValues, isAppliedEntry, loadParameterSets, loadSetPictures,
parameterRows, rememberApplied, restoreApplied, saveNodeParameters,
} from './node_parameter_sets.js';
const SEARCH_FROM = 6; // entries before a search box is worth showing
const VIEW_KEY = 'anomalous_node_params_view'; // 'cards' | 'table', this browser's choice
const ROLE_KEYS = { positive: 'recipePromptRolePositive', negative: 'recipePromptRoleNegative', both: 'recipePromptRoleBoth' };
const KEEP_KEYS = { seed: 'currentNodeKeepSeed', model: 'currentNodeKeepModel', missing: 'currentNodeKeepMissing' };
const SHOWN_SINCE = 3; // changes named in the line on top; the rest are counted
const CHANNELS = ['workflow', 'combo', 'picture', 'saved'];
const ICONS = {
workflow: '<rect x="3" y="4" width="7" height="6" rx="1.5"/><rect x="14" y="14" width="7" height="6" rx="1.5"/><path d="M6.5 10v4a3 3 0 0 0 3 3H14"/>',
combo: '<path d="m12 3 9 5-9 5-9-5 9-5z"/><path d="m3 13 9 5 9-5"/>',
picture: '<rect x="3" y="4" width="18" height="16" rx="2.5"/><circle cx="9" cy="10" r="1.8"/><path d="M21 16l-5-5-9 9"/>',
saved: '<path d="M6.5 3.5h11v17l-5.5-4-5.5 4z"/>',
lock: '<rect x="5" y="11" width="14" height="9.5" rx="2"/><path d="M8.5 11V7.5a3.5 3.5 0 0 1 7 0V11"/>',
history: '<path d="M3.5 12a8.5 8.5 0 1 0 2.6-6.1"/><path d="M3.5 4.5v4h4"/><path d="M12 8v4.5l3 2"/>',
};
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;
}
function icon(name) {
const span = el('span', 'anomalous-node-param-icon');
span.setAttribute('aria-hidden', 'true');
span.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${ICONS[name]}</svg>`; // static markup only
return span;
}
const brief = (value) => {
const text = typeof value === 'string' ? value.replace(/\s+/g, ' ') : JSON.stringify(value) ?? '';
return text.length > 48 ? `${text.slice(0, 47)}…` : text;
};
/** "10月3日" (with the year when it is not this one); '' without a time. */
function dayOf(stamp) {
if (!stamp) return '';
const date = new Date(stamp < 1e12 ? stamp * 1000 : stamp);
if (Number.isNaN(date.getTime())) return '';
const sameYear = date.getFullYear() === new Date().getFullYear();
return date.toLocaleDateString(undefined, sameYear ? { month: 'short', day: 'numeric' } : { year: 'numeric', month: 'short', day: 'numeric' });
}
/** "今天 10:32", or the day and the time. */
function whenOf(stamp) {
const date = new Date(stamp);
if (Number.isNaN(date.getTime())) return '';
const time = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
return date.toDateString() === new Date().toDateString() ? t('currentNodeToday', { time }) : `${dayOf(stamp)} ${time}`;
}
/** "cfg 5 → 6、步数 15 → 20 等 2 项". */
function changesText(changes, from, to) {
const named = changes.slice(0, SHOWN_SINCE).map(change => `${change.label} ${brief(change[from])} → ${brief(change[to])}`);
const text = named.join(t('modelSourceListSep'));
return changes.length > SHOWN_SINCE ? `${text} ${t('currentNodeAppliedMore', { count: changes.length - SHOWN_SINCE })}` : text;
}
const outputQuery = image => new URLSearchParams({ filename: image.filename, subfolder: image.subfolder || '' });
/** The entry's picture: the newest made with its values, else the one it was kept from; null for none. */
function pictureOf(entry, pictures) {
for (const source of entry.sources) {
const made = source.signature && pictures[source.signature]?.newest;
if (made) return { thumb: `/anomalous/output_thumbnail?${outputQuery(made)}`, full: `/view?${outputQuery(made)}&type=output`, title: made.filename };
}
for (const source of entry.sources) {
if (source.preview) return { thumb: source.preview, full: source.image ? `/view?${outputQuery(source.image)}&type=output` : source.preview, title: source.image?.filename || source.label };
if (source.image) return { thumb: `/anomalous/output_thumbnail?${outputQuery(source.image)}`, full: `/view?${outputQuery(source.image)}&type=output`, title: source.image.filename };
}
return null;
}
/** Shows a recipe's details on one of its parameter sets. */
function openSet(owner, source) {
// Opening goes to the Workflows page at once (which forgets any state to return to), then reads the recipe.
const opening = owner.openRecipeByFilename(source.recipeFile);
owner.recipeReturnState = { activeTab: 'set', filename: source.setFile };
return opening;
}
/**
* Compares `node` with the entry's values, that node alone: a copy of it holding those values
* and nothing wired, under the node's id so the two pair.
*/
function compareWithCanvas(entry, node) {
const copy = node.serialize();
copy.widgets_values = entry.values.map((value, index) => value ?? copy.widgets_values?.[index]); // a combo may not hold every box
copy.inputs = (copy.inputs || []).map(input => ({ ...input, link: null }));
copy.outputs = (copy.outputs || []).map(output => ({ ...output, links: [] }));
openParamCompare({ workflow: { nodes: [copy], links: [], groups: [] }, label: entry.name }, { canvas: true }, { nodeId: node.id });
}
/** The channel a source came by. */
const channelOf = source => (source.kind === 'recipe' ? 'workflow' : source.kind === 'combo' ? 'combo' : source.image ? 'picture' : 'saved');
/** A source for its chip's list ({label, title, run}: the name alone, the channel being known, in full, and where a press goes; no run for values saved here). */
function sourceAction(source, pictures, owner) {
const day = dayOf(source.timestamp);
const count = pictures[source.signature]?.count;
const said = text => [text, count ? t('currentNodeSetPictures', { count }) : '', day].filter(Boolean).join(' · ');
if (source.kind === 'recipe') {
const name = source.setName && source.setName !== source.label ? `${source.label} / ${source.setName}` : source.label;
return { label: said(name), title: said(t('currentNodeFromSet', { recipe: name })), run: source.setFile && source.recipeFile ? () => openSet(owner, source) : null };
}
if (source.kind === 'combo') {
return { label: said(source.label), title: said(t('currentNodeFromCombo', { name: source.label })), run: () => owner.openComboByFilename(source.filename) };
}
if (source.image) {
const name = source.image.filename.replace(/\.[^.]+$/, '');
return { label: said(name), title: said(t('currentNodeFromPicture', { name })), run: () => owner.showGalleryViewer?.(`/view?${outputQuery(source.image)}&type=output`) };
}
const name = said(source.origin === 'canvas_node' ? t('currentNodeFromHere') : source.label);
return { label: name, title: name, run: null };
}
/** The sources of a channel as one chip: a press goes to the one there is, or lists them. */
function sourceChip(channel, actions) {
const several = actions.length > 1;
const goes = actions.some(action => action.run);
const chip = el(goes ? 'button' : 'span', `anomalous-node-param-source is-${channel}`);
chip.append(icon(channel), el('span', '', t(`currentNodeChannel_${channel}`)));
if (several) chip.append(el('span', 'anomalous-node-param-source-count', String(actions.length)));
chip.title = actions.map(action => action.title).join('\n');
if (!goes) return chip;
chip.type = 'button';
chip.append(el('span', 'anomalous-node-param-source-go', several ? '▾' : '›'));
chip.onclick = () => (several ? openPopupMenu(chip, actions) : actions[0].run());
return chip;
}
/** Where the entry's values came from: a chip per channel, then the day. */
function sourceChips(entry, ctx) {
const row = el('div', 'anomalous-node-param-sources');
const seen = new Set();
const sources = entry.sources.filter((source) => {
const id = `${source.kind}|${source.setFile || source.filename || source.label}`;
return !seen.has(id) && seen.add(id);
});
for (const channel of CHANNELS) {
const actions = sources.filter(source => channelOf(source) === channel).map(source => sourceAction(source, ctx.pictures, ctx.owner));
if (actions.length) row.append(sourceChip(channel, actions));
}
const day = dayOf(entry.timestamp);
if (day) row.append(el('span', 'anomalous-node-param-day', day));
return row;
}
/** A value the entry would change: numbers as two bars (now, then), other values as old → new. */
function changeRow(row) {
const line = el('div', 'anomalous-node-param-row is-change');
line.title = `${row.label}: ${typeof row.to === 'string' ? row.to : JSON.stringify(row.to)}`;
line.append(el('span', 'anomalous-node-param-label', row.label));
const numbers = typeof row.from === 'number' && typeof row.to === 'number' && Number.isFinite(row.from) && Number.isFinite(row.to);
if (numbers) {
const largest = Math.max(Math.abs(row.from), Math.abs(row.to)) || 1;
const bars = el('span', 'anomalous-node-param-bars');
for (const [value, kind] of [[row.from, 'is-now'], [row.to, 'is-then']]) {
const bar = el('span', `anomalous-node-param-bar ${kind}`);
bar.style.width = `${Math.max(3, (Math.abs(value) / largest) * 100)}%`; // the value's share of the larger one
bars.append(bar);
}
const values = el('span', 'anomalous-node-param-nums');
values.append(el('span', 'is-now', brief(row.from)), el('span', '', ' → '), el('strong', '', brief(row.to)),
el('span', 'anomalous-node-param-trend', row.to > row.from ? '↑' : '↓'));
line.append(bars, values);
} else {
const values = el('span', 'anomalous-node-param-swap');
values.append(el('span', 'anomalous-node-param-pill is-now', brief(row.from)), el('span', '', '→'), el('span', 'anomalous-node-param-pill is-then', brief(row.to)));
line.append(values);
}
return line;
}
/** A value that stays (with why) or is the same already: dim, on one line. */
function quietRow(row) {
const line = el('div', `anomalous-node-param-row is-${row.status === 'same' ? 'same' : 'kept'}`);
line.append(el('span', 'anomalous-node-param-label', row.label));
const value = el('span', 'anomalous-node-param-quiet');
if (row.status !== 'same') value.append(icon('lock'));
value.append(document.createTextNode(row.status === 'same' ? brief(row.to) : `${brief(row.to)} · ${t(KEEP_KEYS[row.status])}`));
line.append(value);
line.title = `${row.label}: ${typeof row.to === 'string' ? row.to : JSON.stringify(row.to)}`;
return line;
}
/** Puts `made` into the node; with `entry` (and its `rows`), remembers it as the set last put in, until undone. */
function applyChanges(node, made, receiptHost, redraw, entry = null, rows = []) {
try {
const result = applyParameterChanges(app, node, made);
const previous = entry ? rememberApplied(app, node, entry, rows) : null;
showApplyReceipt(receiptHost, {
undo() {
result.undo();
if (entry) restoreApplied(app, node, previous);
redraw();
},
}, node);
redraw();
} catch (error) {
showWorkbenchToast(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message));
}
}
function entryCard(node, entry, ctx) {
const rows = parameterRows(node, entry.values);
const made = rows.filter(row => row.status === 'change');
const kept = rows.filter(row => row.status !== 'change' && row.status !== 'same');
const same = rows.filter(row => row.status === 'same');
const key = JSON.stringify(entry.values);
const card = el('div', 'anomalous-node-param');
const top = el('div', 'anomalous-node-param-top');
const picture = pictureOf(entry, ctx.pictures);
if (picture) {
const thumb = button('anomalous-node-param-thumb', undefined, () => ctx.owner.showGalleryViewer?.(picture.full));
thumb.title = picture.title;
const img = el('img');
img.alt = '';
img.loading = 'lazy';
img.src = picture.thumb;
img.onerror = () => thumb.replaceChildren(icon('picture'));
thumb.append(img);
top.append(thumb);
} else {
top.append(el('span', 'anomalous-node-param-thumb is-empty'));
top.lastChild.append(icon(CHANNELS.find(channel => entry.sources.some(source => channelOf(source) === channel))));
}
const main = el('div', 'anomalous-node-param-main');
const head = el('div', 'anomalous-node-param-head');
const name = el('strong', 'anomalous-node-param-name', entry.name.endsWith(` · ${node.type}`) ? entry.name.slice(0, -node.type.length - 3) : entry.name);
name.title = entry.name;
head.append(name);
if (ROLE_KEYS[entry.role]) head.append(el('span', `anomalous-node-param-role is-${entry.role}`, t(ROLE_KEYS[entry.role])));
if (isAppliedEntry(entry, ctx.applied?.record)) head.append(el('span', 'anomalous-node-param-last', t('currentNodeLastBadge')));
main.append(head, sourceChips(entry, ctx));
top.append(main);
card.append(top);
const list = el('div', 'anomalous-node-param-rows');
list.append(...made.map(changeRow));
const open = ctx.expanded.has(key);
if (open) list.append(...kept.map(quietRow), ...same.map(quietRow));
else if (kept.length) {
const line = el('div', 'anomalous-node-param-row is-kept');
const text = el('span', 'anomalous-node-param-quiet');
const parts = [...new Set(kept.map(row => row.status))].map(status => t('currentNodeKeptPart', {
names: kept.filter(row => row.status === status).map(row => row.label).join(t('modelSourceListSep')), why: t(KEEP_KEYS[status]),
}));
text.append(icon('lock'), document.createTextNode(t('currentNodeKeptLine', { names: parts.join(t('currentNodeKeptSep')) })));
line.append(text);
list.append(line);
}
if (!made.length && !open) list.append(el('p', 'anomalous-node-muted', t('currentNodeSame')));
card.append(list);
const actions = el('div', 'anomalous-node-param-actions');
if (same.length || kept.length) {
actions.append(button('anomalous-node-param-more', open ? t('currentNodeShowLess') : t('currentNodeShowAll', { count: rows.length }), () => {
if (open) ctx.expanded.delete(key);
else ctx.expanded.add(key);
ctx.redraw();
}));
}
actions.append(el('span', 'anomalous-node-param-gap'));
const compare = button('anomalous-node-param-secondary', t('currentNodeCompare'), () => compareWithCanvas(entry, node));
compare.title = t('currentNodeCompareHint');
actions.append(compare);
// 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'), () => ctx.remove(entry, saved)));
const apply = button('anomalous-node-param-apply', made.length ? t('currentNodeApply', { count: made.length }) : t('currentNodeSame'),
() => applyChanges(node, made, ctx.section, ctx.redraw, entry, rows));
apply.disabled = !made.length;
actions.append(apply);
card.append(actions);
return card;
}
/**
* Side by side: a column for now and for each set, a row per value any set would change (first)
* or keeps as it is (seeds and the like, after).
*/
function entryTable(node, entries, ctx) {
const sets = entries.map(entry => ({ entry, rows: new Map(parameterRows(node, entry.values).map(row => [row.index, row])) }));
const changed = index => sets.some(set => set.rows.get(index)?.status === 'change');
const indexes = [...new Set(sets.flatMap(set => [...set.rows.values()].filter(row => row.status !== 'same').map(row => row.index)))]
.sort((a, b) => (changed(b) - changed(a)) || a - b);
const wrap = el('div', 'anomalous-node-param-table-wrap');
const table = el('table', 'anomalous-node-param-table');
const head = el('tr');
head.append(el('th', ''), el('th', 'is-now', t('currentNodeNow')));
for (const { entry } of sets) {
const cell = el('th');
const picture = pictureOf(entry, ctx.pictures);
const thumb = picture ? button('anomalous-node-param-table-thumb', undefined, () => ctx.owner.showGalleryViewer?.(picture.full))
: el('span', 'anomalous-node-param-table-thumb is-empty');
if (picture) {
const img = el('img');
img.alt = '';
img.loading = 'lazy';
img.src = picture.thumb;
thumb.append(img);
}
cell.append(thumb, el('span', 'anomalous-node-param-table-name', entry.name));
if (isAppliedEntry(entry, ctx.applied?.record)) cell.append(el('span', 'anomalous-node-param-last', t('currentNodeLastBadge')));
cell.title = entry.name;
head.append(cell);
}
table.append(head);
for (const index of indexes) {
const widget = node.widgets?.[index];
const tr = el('tr');
tr.append(el('td', 'anomalous-node-param-label', widget?.label || widget?.name || `#${index}`), el('td', 'is-now', brief(widget?.value)));
for (const { rows } of sets) {
const row = rows.get(index);
const cell = el('td', row ? `is-${row.status}` : 'is-none', row ? brief(row.to) : '—');
if (row && row.status !== 'change' && row.status !== 'same') cell.title = t(KEEP_KEYS[row.status]);
tr.append(cell);
}
table.append(tr);
}
const foot = el('tr', 'anomalous-node-param-table-foot');
foot.append(el('td'), el('td'));
for (const { entry, rows } of sets) {
const made = [...rows.values()].filter(row => row.status === 'change');
const cell = el('td');
const apply = button('anomalous-node-param-apply', made.length ? t('currentNodeApplyShort', { count: made.length }) : t('currentNodeSame'),
() => applyChanges(node, made, ctx.section, ctx.redraw, entry, [...rows.values()]));
apply.disabled = !made.length;
apply.title = entry.name;
cell.append(apply);
foot.append(cell);
}
table.append(foot);
wrap.append(table);
if (!indexes.length) wrap.append(el('p', 'anomalous-node-muted', t('currentNodeAllSame')));
return wrap;
}
/**
* The line on top: the set last put into this node and what changed since (Put back puts
* those back); without one, the set the values match or come closest to. Null for neither.
*/
function historyLine(node, entries, ctx) {
const line = el('div', 'anomalous-node-param-history');
line.append(icon('history'));
const state = ctx.applied;
if (state) {
const { record, changed } = state;
line.append(el('span', 'anomalous-node-param-history-main', t('currentNodeAppliedLast', { name: record.name, when: whenOf(record.at) })));
line.append(el('span', `anomalous-node-param-history-note${changed.length ? '' : ' is-same'}`,
changed.length ? t('currentNodeAppliedChanged', { changes: changesText(changed, 'from', 'to') }) : t('currentNodeAppliedSame')));
if (changed.length) {
const back = changed.map(change => ({ index: change.index, label: change.label, from: change.to, to: change.from, keep: '' }));
line.append(button('anomalous-node-param-link', t('currentNodeAppliedRestore'), () => applyChanges(node, back, ctx.section, ctx.redraw)));
}
return line;
}
const near = closestSet(node, entries);
if (!near) return null;
const text = near.changes.length
? t('currentNodeClosest', { name: near.entry.name, changes: changesText(near.changes, 'from', 'to') })
: t('currentNodeMatches', { name: near.entry.name }) + (near.others ? t('currentNodeMatchesMore', { count: near.others }) : '');
line.append(el('span', 'anomalous-node-param-history-main', text));
return line;
}
function readView() {
try { return localStorage.getItem(VIEW_KEY) === 'table' ? 'table' : 'cards'; } catch { return 'cards'; }
}
function keepView(view) {
try { localStorage.setItem(VIEW_KEY, view); } catch { /* the choice is only a convenience */ }
}
/** Appends the parameters section for `node` to `container` and loads its entries. */
export function renderNodeParameters(node, container, owner) {
const section = el('section', 'anomalous-node-section anomalous-node-params');
const head = el('div', 'anomalous-node-section-head');
const views = el('div', 'anomalous-node-param-views');
views.setAttribute('role', 'radiogroup');
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 = [];
let view = readView();
const ctx = { owner, section, pictures: {}, expanded: new Set(), applied: null, redraw: () => draw(), remove: (entry, files) => remove(entry, files) };
const paintViews = () => {
views.hidden = entries.length < 2;
views.replaceChildren(...[['cards', 'currentNodeViewCards'], ['table', 'currentNodeViewTable']].map(([name, label]) => {
const choice = button(`anomalous-node-param-view${view === name ? ' is-on' : ''}`, t(label), () => {
view = name;
keepView(name);
draw();
});
choice.setAttribute('role', 'radio');
choice.setAttribute('aria-checked', String(view === name));
return choice;
}));
};
const draw = () => {
ctx.applied = appliedState(app, node);
const query = search.value.trim().toLowerCase();
const shown = entries.filter(entry => !query
|| `${entry.name} ${entry.sources.map(source => `${source.label} ${source.image?.filename || ''}`).join(' ')}`.toLowerCase().includes(query));
search.hidden = entries.length < SEARCH_FROM;
paintViews();
const history = historyLine(node, entries, ctx);
const top = history ? [history] : [];
if (!shown.length) list.replaceChildren(...top, el('p', 'anomalous-node-muted', t(entries.length ? 'currentNodeNoMatch' : 'currentNodeNoSets')));
else if (view === 'table' && entries.length >= 2) list.replaceChildren(...top, entryTable(node, shown, ctx));
else list.replaceChildren(...top, ...shown.map(entry => entryCard(node, entry, ctx)));
};
search.oninput = draw;
const load = async () => {
const run = (section._load = (section._load || 0) + 1);
try {
const loaded = await loadParameterSets(node.type, { boxes: (node.widgets || []).map(widget => widget.name) });
if (run !== section._load || !section.isConnected) return;
entries = loaded;
draw();
// The pictures made with the sets' values come after: one pass over the recent outputs.
const pictures = await loadSetPictures(loaded);
if (run !== section._load || !section.isConnected || !Object.keys(pictures).length) return;
ctx.pictures = pictures;
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'));
}
};
const tools = el('div', 'anomalous-node-param-tools');
tools.append(views, button('anomalous-node-section-btn', t('currentNodeSave'), save));
head.append(el('h3', '', t('currentNodeParams')), tools);
views.hidden = true;
load();
}