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>
139 lines
6.7 KiB
JavaScript
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 });
|
|
}
|
|
}
|