Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_keep_menu.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

136 lines
5.6 KiB
JavaScript

/**
* The keep menu of an output image (the star on gallery cards, Keep in the image
* workbench): its whole workflow (Workflows page), its combo (Combos page) or its prompts
* (saved prompts, in Prompt Studio), each row saying where it goes; one kept already shows ✓ and opens it.
* Keeping runs recipe_save.js / image_keep.js and says what happened in a toast with Open.
*/
import { translate as t } from './locales.js';
import { anomalousAlert } from './ui_dialog.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
import { keepImageAsRecipe } from './recipe_save.js';
import { keepImageAsCombo, keepImagePrompts, keptKey, loadKeptImages } from './image_keep.js';
const NO_WORKFLOW = 'Image has no reusable UI workflow'; // the inspect route's message
const KINDS = Object.freeze([
{ kind: 'recipe', icon: '🪡', keep: keepImageAsRecipe, open: (owner, item) => owner.openRecipeByFilename(item.filename) },
{ kind: 'combo', icon: '🧩', keep: keepImageAsCombo, open: (owner, item) => owner.openComboByFilename(item.filename) },
{ kind: 'prompt', icon: '✍️', keep: keepImagePrompts, open: owner => owner.openPromptStudio() },
]);
let closeOpenMenu = null;
function el(tag, className, text) {
const node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
/** Under the anchor, or above it when there is no room below; inside the window. */
function place(menu, anchor) {
const box = anchor.getBoundingClientRect();
const { width, height } = menu.getBoundingClientRect();
const left = Math.max(8, Math.min(box.left, window.innerWidth - width - 8));
const below = box.bottom + 6;
menu.style.left = `${left}px`;
menu.style.top = `${below + height > window.innerHeight - 8 ? Math.max(8, box.top - height - 6) : below}px`;
}
function keptMessage(spec, result) {
if (result.existed) return t('keepExisted', { name: result.name });
const message = t(`keptAs_${spec.kind}`, { name: result.name });
return result.missing?.length ? `${message} ${t('keepComboMissing', { count: result.missing.length })}` : message;
}
async function keep(owner, spec, sourceImage, kept, { name, onKept, beforeOpen }) {
try {
const result = await spec.keep(sourceImage, { name });
if (result.empty) {
await anomalousAlert(t('keepNoPrompt'));
return;
}
kept[spec.kind] = { filename: result.filename, name: result.name };
onKept?.(kept);
showWorkbenchToast(keptMessage(spec, result), {
label: t('keepOpen'),
run: () => {
beforeOpen?.();
spec.open(owner, kept[spec.kind]);
},
});
} catch (error) {
console.error(`[AMB] Could not keep the image as ${spec.kind}:`, error);
await anomalousAlert(t(error?.message === NO_WORKFLOW ? 'keepNoWorkflow' : 'keepFailed'));
}
}
/** Closes the menu that is open, if any. */
export function closeKeepMenu() {
closeOpenMenu?.();
}
/**
* Opens the menu at `anchor` for `sourceImage`. `name`: what to call what is kept (else the
* image's suggested name); `onKept(kept)` gets the image's { recipe, combo, prompt } after a
* keep; `beforeOpen()` runs before something kept is opened (the workbench closes itself).
*/
export async function openKeepMenu(owner, anchor, sourceImage, options = {}) {
closeKeepMenu();
const menu = el('div', 'anomalous-keep-menu');
menu.setAttribute('role', 'menu');
menu.append(el('p', 'anomalous-keep-menu-title', t('keepMenuTitle')), el('p', 'anomalous-keep-menu-wait', t('loading')));
document.body.append(menu);
place(menu, anchor);
const onPointer = (event) => {
if (!menu.contains(event.target) && !anchor.contains(event.target)) close();
};
const onKey = (event) => {
if (event.key !== 'Escape') return;
event.stopPropagation();
close();
};
const close = () => {
menu.remove();
window.removeEventListener('pointerdown', onPointer, true);
window.removeEventListener('keydown', onKey, true);
if (closeOpenMenu === close) closeOpenMenu = null;
};
window.addEventListener('pointerdown', onPointer, true);
window.addEventListener('keydown', onKey, true);
closeOpenMenu = close;
let kept = {};
try {
kept = (await loadKeptImages()).get(keptKey(sourceImage)) || {};
} catch (error) {
console.warn('[AMB] Could not read what this image was kept as:', error);
}
if (closeOpenMenu !== close) return;
kept = { recipe: kept.recipe || null, combo: kept.combo || null, prompt: kept.prompt || null };
menu.querySelector('.anomalous-keep-menu-wait')?.remove();
for (const spec of KINDS) {
const item = kept[spec.kind];
const row = el('button', `anomalous-keep-row${item ? ' is-kept' : ''}`);
row.type = 'button';
row.setAttribute('role', 'menuitem');
const copy = el('span', 'anomalous-keep-row-copy');
copy.append(el('strong', '', t(`keepAs_${spec.kind}`)),
el('span', '', item ? t('keepKeptAs', { name: item.name }) : t(`keepAs_${spec.kind}Hint`)));
row.append(el('span', 'anomalous-keep-row-icon', spec.icon), copy, el('span', 'anomalous-keep-row-mark', item ? '✓' : ''));
row.onclick = () => {
close();
if (!item) {
void keep(owner, spec, sourceImage, kept, options);
return;
}
options.beforeOpen?.();
spec.open(owner, item);
};
menu.append(row);
}
place(menu, anchor);
}