fix(nodes): linked text review round for #343

- A legacy frontend Primitive linked to an editor's text or the Pair's
  negative now runs with its value: the graphToPrompt wrapper and the queue
  hook fill it in (through virtual reroutes), in new and pre-#343 workflows.
- Moving a linked text and releasing it on the editor keeps it on `text`:
  a link being moved counts as free, as the frontend counts it.
- A link dropped on a prompt box (or its link note) fills that box's input,
  so the Pair's Negative prompt box takes `negative_text` (Classic through
  getInputOnPos, Nodes 2.0 through the box's slot key).
- Only exact STRING links are steered; wildcards keep the frontend choice.
- A linked Classic text box gives its row back and gets it back on
  disconnect; the PromptManagerText note uses the theme colour.
- The metadata parsers map positional widget values through the widget
  table when an editor's widget-bound inputs do not line up with it.
- Real-host cases for the legacy Primitive (new and saved workflows, a real
  Run), the moved link, the Negative box, paste/clone/undo and Split.
This commit is contained in:
Vito Sansevero
2026-10-25 15:08:28 -07:00
parent d5d7f5e33d
commit 2fadd765f6
15 changed files with 924 additions and 36 deletions
+29 -2
View File
@@ -1,7 +1,7 @@
import {createScope, createButton, createField, mountTheme} from './shared/toolkit/core.js';
import {createPromptLibrary} from './prompt-library.js';
import {attachMetadataAutocomplete} from './metadata-autocomplete.js';
import {exposeTextSockets, linkNoteText, preferTextInputs} from './text-links.js';
import {exposeTextSockets, linkNoteText, preferTextInputs, textInputAtPoint} from './text-links.js';
const names = ['text', 'category', 'tags', 'search_text', 'prepend_text', 'append_text'];
let nextEditorId = 0;
@@ -32,6 +32,8 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
const configure = prototype.configure;
const serialize = prototype.serialize;
const connections = prototype.onConnectionsChange;
const inputOnPos = prototype.getInputOnPos;
const added = prototype.onAdded;
prototype.onNodeCreated = function (...args) {
created?.apply(this, args);
const scope = createScope(); this._focusedScope = scope;
@@ -67,6 +69,7 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
const mode = data?.settings?.webui_display_mode;
if (!scope.signal.aborted && data?.success && ['popup','newtab'].includes(mode)) webDisplayMode = mode;
}).catch(() => {});
const dropTargets = [];
function textControl(name, label) {
const widget = widgets.get(name);
const nativeInput = widget.inputEl;
@@ -83,6 +86,13 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
field.classList.add('pm-editor-main');
const note = document.createElement('p'); note.className = 'pm-editor-status';
note.textContent = linkNoteText(label);
// The prompt box and its link note are drop targets for this input (#343): Nodes 2.0 resolves a drop
// through the nearest `.lg-node-widget` and its `[data-slot-key]`; Classic asks getInputOnPos.
for (const target of [field,note]) {
const key = document.createElement('span'); key.hidden = true; key.dataset.slotKey = '';
target.classList.add('lg-node-widget'); target.append(key);
dropTargets.push({name,element:target,key});
}
return {input,field,note};
}
const positive = textControl('text',pair ? 'Positive prompt' : 'Main prompt');
@@ -147,7 +157,12 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
widget.callback?.(text, app.canvas, thisNode);
thisNode.setDirtyCanvas(true,true); app.graph?.setDirtyCanvas(true,true);
}
// The node id is assigned when the node joins a graph (and changes on paste), so keys follow it.
function syncSlotKeys() {
for (const target of dropTargets) target.key.dataset.slotKey = `${thisNode.id}-in-${thisNode.inputs?.findIndex(input => input.name === target.name)}`;
}
function render() {
syncSlotKeys();
mainField.hidden = Boolean(isConnected('text')); connected.hidden = !isConnected('text');
if (negative) { negative.field.hidden = Boolean(isConnected('negative_text')); negative.note.hidden = !isConnected('negative_text'); }
if (save) { save.disabled = saving || hasConnectedPairValues(); saveConnectionNotice.hidden = !hasConnectedPairValues(); }
@@ -218,7 +233,14 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
footer.setAttribute('role',opened ? 'status' : 'alert');
footer.textContent = opened ? 'Web UI opened.' : 'Allow popups to open Web UI, then try again.';
});
this._focusedView = {element,render,restore,snapshot};
function inputAt(position) {
const canvas = app.canvas;
if (canvas?.linkConnector?.state?.connectingTo !== 'input' || !canvas.ds || !canvas.canvas) return undefined;
const [x,y] = canvas.ds.convertOffsetToCanvas(position);
const box = canvas.canvas.getBoundingClientRect();
return textInputAtPoint(thisNode, [box.left + x, box.top + y], dropTargets);
}
this._focusedView = {element,render,restore,snapshot,inputAt,syncSlotKeys};
this.addDOMWidget('prompt_manager_focused_ui','div',element,{serialize:false, hideOnZoom:false, getMinHeight:() => 360});
this.size = [pair ? 1100 : split ? 900 : 550,pair ? 700 : 650]; this.setSize(this.size); restore();
};
@@ -235,4 +257,9 @@ export function installFocusedNode(nodeType, app, api, parseQuery, {layout = 'fo
};
prototype.onConnectionsChange = function (...args) { const result = connections?.apply(this,args); this._focusedView?.render(); return result; };
prototype.onRemoved = function (...args) { this._focusedScope?.dispose(); return removed?.apply(this,args); };
prototype.onAdded = function (...args) { const result = added?.apply(this,args); this._focusedView?.syncSlotKeys(); return result; };
// Classic: a link dropped on a prompt box (or its link note) fills that box's input.
prototype.getInputOnPos = function (...args) {
return inputOnPos?.apply(this,args) ?? this._focusedView?.inputAt(args[0]);
};
}