Files
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

139 lines
6.7 KiB
JavaScript

/**
* What dropping a dragged prompt on the canvas would do (prompt_card_drag.js drags Prompt
* Studio's cards). While it is dragged, every prompt box on the canvas is outlined in its
* role's colour; the box under the pointer is the target (or a node's only box), the hint
* says what releasing writes where, including the opposite-role box on the same sampler, and
* those boxes show the new text over their own until the pointer leaves. prompt_boxes.js
* decides boxes and roles.
*/
import { app } from "../../../scripts/app.js";
import { translate as t } from './locales.js';
import { getCanvasPosition } from './material_drag.js';
import { extractMaterialPromptEnvelope } from './node_material_actions.js';
import { partnerBox, planPromptFill, promptBoxes } from './prompt_boxes.js';
const ROLE_CLASSES = { positive: 'is-positive', negative: 'is-negative', both: 'is-both', '': 'is-unknown' };
const BOX_LABELS = { positive: 'promptBoxPositive', negative: 'promptBoxNegative', both: 'promptBoxBoth', '': 'promptBoxPlain' };
const OPPOSITE = { positive: 'negative', negative: 'positive' };
// The dragged prompt's text, known when its drag starts.
let dragged = { filename: '', envelope: null };
let previews = []; // { overlay, element } over the boxes a release would write
let previewKey = '';
/** A drag whose text is known already (a Prompt Studio card, keyed `key`). */
export function prepareTextDrag(key, envelope) {
dragged = { filename: key, envelope };
}
const known = material => (dragged.filename === material.filename ? dragged : null);
/** Outlines every prompt box of the open graph by role; returns what removes the outlines. */
export function outlinePromptBoxes(graph = app.graph) {
const marked = [];
for (const node of graph?._nodes || []) {
for (const box of promptBoxes(node)) {
const element = box.widget.element;
if (!element) continue;
element.classList.add('anomalous-prompt-box', ROLE_CLASSES[box.role]);
marked.push(element);
}
}
return () => {
for (const element of marked) element.classList.remove('anomalous-prompt-box', 'is-hover', ...Object.values(ROLE_CLASSES));
};
}
/** Marks the box a drag points at (null: none). */
export function markTargetBox(box) {
for (const element of document.querySelectorAll('.anomalous-prompt-box.is-hover')) {
if (element !== box?.widget.element) element.classList.remove('is-hover');
}
box?.widget.element?.classList.add('is-hover');
}
/** The prompt box a drag event points at on `node`: the box under the pointer, or the node's only box. */
export function promptBoxAt(node, event) {
const boxes = promptBoxes(node);
const hit = boxes.find(box => box.widget.element?.contains(event.target));
if (hit) return hit;
const position = getCanvasPosition(event, app.canvas);
const widget = position && node.getWidgetOnPos?.(position[0], position[1], true);
return boxes.find(box => box.widget === widget) || (boxes.length === 1 ? boxes[0] : null);
}
/** Why a node refuses a prompt drop at this point ('' when it takes it). */
export function promptRefusal(node, event) {
const boxes = promptBoxes(node);
if (!boxes.length) return t('promptDropNoBox');
return promptBoxAt(node, event) ? '' : t('promptDropPickBox');
}
/** What releasing a prompt material on `box` of `node` will do, in one line. */
export function promptDropHint(material, node, box) {
const envelope = known(material)?.envelope || extractMaterialPromptEnvelope(material);
const label = role => t(BOX_LABELS[role]);
let hint = t('promptDropInto', { node: `#${node.id}`, box: label(box.role) });
const other = OPPOSITE[box.role];
if (!envelope.hasPrompt) {
const partner = other && partnerBox(node, box.role);
return partner ? hint + t('promptDropPartnerMaybe', { node: `#${partner.node.id}`, box: label(other), role: t(other === 'negative' ? 'recipePromptRoleNegative' : 'recipePromptRolePositive') }) : hint;
}
if (other && !envelope[box.role]) {
hint += t('promptDropOnlyOther', { role: t(other === 'negative' ? 'recipePromptRoleNegative' : 'recipePromptRolePositive') });
}
const partner = other && envelope[box.role] && envelope[other] ? partnerBox(node, box.role) : null;
if (partner) hint += t('promptDropPartner', { node: `#${partner.node.id}`, box: label(other) });
return hint;
}
/** Removes the previews (the drag ended, or the pointer left the box). */
export function clearDropPreview() {
for (const { overlay, element } of previews) {
overlay.remove();
element.classList.remove('is-previewing');
}
previews = [];
previewKey = '';
}
/** Shows, over `box` of `node` and its partner box, the text a release would write; no box clears. */
export function previewPromptDrop(material, node, box) {
const envelope = known(material)?.envelope;
const targets = [];
if (node && box && envelope?.hasPrompt) {
targets.push([node, box]);
const other = OPPOSITE[box.role];
const partner = other && envelope[box.role] && envelope[other] ? partnerBox(node, box.role) : null;
if (partner) targets.push([partner.node, partner.box]);
}
const key = targets.map(([target, item]) => `${target.id}:${item.index}`).join('|');
if (key === previewKey) return;
clearDropPreview();
previewKey = key;
for (const [target, item] of targets) {
const [entry] = planPromptFill(target, envelope, item);
const element = item.widget.element;
const rect = element?.getBoundingClientRect();
if (!entry || !rect?.width || !rect.height) continue; // a box not laid out shows nothing
const overlay = document.createElement('div');
overlay.className = `anomalous-prompt-preview ${ROLE_CLASSES[item.role]}`;
// The canvas zoom scales the box; the text follows it, down to a readable size.
const scale = element.offsetWidth ? rect.width / element.offsetWidth : 1;
Object.assign(overlay.style, {
left: `${rect.left}px`, top: `${rect.top}px`, width: `${rect.width}px`, height: `${rect.height}px`,
fontSize: `${Math.max(11, parseFloat(getComputedStyle(element).fontSize) * scale)}px`, // readable when zoomed out
});
const tag = document.createElement('span');
tag.className = 'anomalous-prompt-preview-tag';
tag.textContent = t('promptPreviewTag');
const body = document.createElement('div');
body.className = 'anomalous-prompt-preview-text';
body.textContent = entry.value;
overlay.append(tag, body);
document.body.appendChild(overlay);
element.classList.add('is-previewing');
previews.push({ overlay, element });
}
}