Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_prompt_target.js
T
DemonGatanjieuandClaude Opus 5.5 41c62d8667 Prompt Studio: a ✕ beside the node's name switches to the draft
"Use draft" (改草稿) read as "edit the draft". The node's name now has a ✕
that stops editing it, and the draft is labelled as not in any node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 11:35:01 +08:00

178 lines
7.3 KiB
JavaScript

/**
* The prompt side of Prompt Studio: the prompt boxes of the prompt node last selected on the
* canvas, edited in place (each change is a node write and a Ctrl+Z step; Undo here takes the
* studio's own changes back one by one), or a positive and a negative draft when there is no
* such node, kept while ComfyUI stays open. The node stays the target when the selection moves
* to empty canvas or another kind of node; selecting another prompt node switches to it, and
* the ✕ beside the node's name switches to the draft. The boxes follow edits made on the canvas.
*/
import { app } from '../../../scripts/app.js';
import { translate as t } from './locales.js';
import { materialNodeHeading } from './material_inspector.js';
import { applyNodeMaterialValues, selectedMaterialNode } from './node_material_actions.js';
import { promptBoxes } from './prompt_boxes.js';
import { createPromptBoxEditor } from './ui_prompt_box_editor.js';
import { showWorkbenchToast } from './ui_prompt_toast.js';
const POLL_MS = 400;
const MAX_UNDO = 50;
const ROLE_NAMES = { positive: 'promptStudioPositive', negative: 'promptStudioNegative' };
function el(tag, className, content) {
const node = document.createElement(tag);
if (className) node.className = className;
if (content !== undefined) node.textContent = content;
return node;
}
function button(className, label, title, onClick) {
const node = el('button', className, label);
node.type = 'button';
if (title) node.title = title;
node.onclick = onClick;
return node;
}
const inOpenGraph = node => Boolean(node?.graph && node.graph.getNodeById(node.id) === node
&& (node.graph === app.graph || node.graph === app.canvas?.graph));
/** Why a card of its role may not go into a box of `boxRole` ('' when it may). */
function refusal(card, boxRole) {
const cardRole = card.role === 'negative' ? 'negative' : 'positive';
if ((boxRole === 'positive' || boxRole === 'negative') && boxRole !== cardRole) {
return t('promptStudioWrongBox', { card: t(ROLE_NAMES[cardRole]), box: t(ROLE_NAMES[boxRole]) });
}
return '';
}
/**
* Fills `parent`. `owner` keeps the draft; `prefs` is the studio's { view, glossLang };
* `onSave(text, role)` keeps a box's text as a card. Returns { addCard(card), render() }.
*/
export function createPromptTarget(parent, { owner, drawer, scope, prefs, onSave }) {
const draft = owner.promptStudioDraft ||= { positive: '', negative: '' };
const panel = el('section', 'anomalous-ps-target');
const head = el('header', 'anomalous-ps-target-head');
const label = el('div', 'anomalous-ps-target-label');
const labelText = el('span', 'anomalous-ps-target-name');
const toDraft = button('anomalous-ps-target-x', '✕', t('promptStudioUseDraftHint'), () => show(null));
toDraft.setAttribute('aria-label', t('promptStudioUseDraftHint'));
label.append(labelText, toDraft);
const undoButton = button('anomalous-ps-mini', t('promptStudioUndo'), t('promptStudioUndoHint'), undo);
head.append(label, undoButton);
const note = el('p', 'anomalous-ps-target-note', t('promptStudioDraftHint'));
const boxesEl = el('div', 'anomalous-ps-boxes');
panel.append(head, note, boxesEl);
parent.append(panel);
let targetNode = null;
let seen = null; // the selection last looked at
let editors = [];
let history = [];
function setHistory(next) {
history = next;
undoButton.disabled = !history.length;
}
function undo() {
const entry = history[history.length - 1];
if (!entry) return;
setHistory(history.slice(0, -1));
try {
entry();
} catch {
showWorkbenchToast(t('materialUndoChanged'));
setHistory([]);
}
editors.forEach(editor => editor.render());
}
function nodeEditor(node, box, many) {
const role = box.role === 'negative' ? 'negative' : 'positive';
return createPromptBoxEditor({
role: box.role,
name: many || box.name !== 'text' ? box.name : '',
getValue: () => String(box.widget.value ?? ''),
write(value) {
try {
const applied = applyNodeMaterialValues(app, node, [{ index: box.index, value }]);
setHistory([...history.slice(1 - MAX_UNDO), () => applied.undo()]);
return true;
} catch (error) {
showWorkbenchToast(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message));
return false;
}
},
refuse: card => refusal(card, box.role),
onSave: value => onSave(value, role),
prefs, drawer, scope,
});
}
function draftEditor(role) {
return createPromptBoxEditor({
role,
name: '',
getValue: () => draft[role],
write(value) {
const before = draft[role];
draft[role] = value;
setHistory([...history.slice(1 - MAX_UNDO), () => {
if (draft[role] !== value) throw new Error('materialUndoChanged');
draft[role] = before;
}]);
return true;
},
refuse: card => refusal(card, role),
onSave: value => onSave(value, role),
prefs, drawer, scope,
});
}
/** Edits `node`'s prompt boxes, or the draft (null). */
function show(node) {
targetNode = node;
setHistory([]);
const boxes = node ? promptBoxes(node) : [];
panel.classList.toggle('is-draft', !node);
toDraft.hidden = !node;
note.hidden = Boolean(node);
labelText.textContent = node
? t('promptStudioEditing', { node: `${materialNodeHeading(node) || node.type} #${node.id}` })
: t('promptStudioDraft');
editors = node ? boxes.map(box => nodeEditor(node, box, boxes.length > 1)) : ['positive', 'negative'].map(draftEditor);
boxesEl.replaceChildren(...editors.map(editor => editor.element));
editors.forEach(editor => editor.render());
}
function tick() {
const selected = selectedMaterialNode(app);
if (selected !== seen) {
seen = selected;
if (selected && selected !== targetNode && promptBoxes(selected).length) { show(selected); return; }
}
if (targetNode && !inOpenGraph(targetNode)) { show(null); return; }
editors.forEach(editor => editor.refresh());
}
seen = selectedMaterialNode(app);
show(seen && promptBoxes(seen).length ? seen : null);
const timer = setInterval(tick, POLL_MS);
scope.onDispose(() => clearInterval(timer));
return {
/** A card clicked: into the box of its role (or one that takes either), at the end. */
addCard(card) {
const role = card.role === 'negative' ? 'negative' : 'positive';
const editor = editors.find(item => item.role === role) || editors.find(item => !refusal(card, item.role));
if (!editor) {
showWorkbenchToast(t('promptStudioNoBoxFor', { card: t(ROLE_NAMES[role]) }));
return;
}
void editor.addText(card.content);
},
render: () => editors.forEach(editor => editor.render()),
};
}