201 lines
8.2 KiB
JavaScript
201 lines
8.2 KiB
JavaScript
// prompt_machine_frontend.js
|
|
import { app } from "../../../scripts/app.js";
|
|
|
|
const PROMPT_MACHINE_TOOLTIPS = {
|
|
subject: "Core hero or subject and physical details (age, hair, eyes, outfit, accessories).\nExample: 'A young female astronomer with messy raven hair and grey eyes, wearing a fitted leather jacket.'",
|
|
pose_and_expression: "Body posture, facial expression, action, and primary gestures.\nExample: 'Leaning over an ancient star map with a subtle smile, holding a brass compass in her right hand.'",
|
|
secondary_subject: "Supporting characters, companion animals, or secondary focal objects.\nExample: 'A sleek white robotic cat sitting on the desk beside her, watching the holograms.'",
|
|
background_and_environment: "Immediate physical surroundings, wider environment, and background setting.\nExample: 'Inside a high-tech glass dome observatory with scattered parchment maps, overlooking a star-filled galaxy.'",
|
|
lighting: "Lighting sources, direction, intensity, reflections, shadows, and mood.\nExample: 'Soft cyan holographic glow illuminating her face, warm golden sunlight streaming through the side window.'",
|
|
style: "Visual medium, photographic style, aesthetic textures, and dominant color palette.\nExample: 'Cinematic 35mm film photography, fine grain, shallow depth of field. Deep navy blues and warm amber tones.'",
|
|
creative_hook: "One surreal, impossible, or striking visual concept.\nExample: 'Constellations floating like tiny glowing fireflies in the air inside the room.'",
|
|
technical: "Camera equipment, lens specs, shot type, and framing (Optional).\nExample: 'Shot on 85mm f/1.4 lens, eye-level medium close-up, sharp focus on subject, soft background bokeh.'",
|
|
json_output: "Output structured JSON mapping instead of a formatted text paragraph."
|
|
};
|
|
|
|
function setupPromptMachine(node) {
|
|
if (!node.widgets) return;
|
|
node.widgets.forEach(w => {
|
|
if (PROMPT_MACHINE_TOOLTIPS[w.name]) {
|
|
w.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
|
|
if (w.options) {
|
|
w.options.tooltip = PROMPT_MACHINE_TOOLTIPS[w.name];
|
|
}
|
|
const updateInputEl = () => {
|
|
if (w.inputEl) {
|
|
w.inputEl.title = PROMPT_MACHINE_TOOLTIPS[w.name];
|
|
const firstLine = PROMPT_MACHINE_TOOLTIPS[w.name].split('\n')[0];
|
|
if (!w.inputEl.placeholder) {
|
|
w.inputEl.placeholder = firstLine;
|
|
}
|
|
w.inputEl.readOnly = false;
|
|
w.inputEl.disabled = false;
|
|
}
|
|
};
|
|
updateInputEl();
|
|
// Retry if inputEl creates asynchronously
|
|
if (!w.inputEl) {
|
|
setTimeout(updateInputEl, 100);
|
|
setTimeout(updateInputEl, 500);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupPromptStyleMachine(node) {
|
|
if (node._sata_style_initialized) return;
|
|
|
|
const csvWidget = node.widgets?.find(w => w.name === "csv_file");
|
|
let nameWidget = node.widgets?.find(w => w.name === "name");
|
|
|
|
if (!csvWidget || !nameWidget) {
|
|
return;
|
|
}
|
|
|
|
node._sata_style_initialized = true;
|
|
const initialSavedName = nameWidget.value || "None";
|
|
|
|
// Convert widget to combo dropdown gracefully if it's not already
|
|
if (nameWidget.type !== "combo") {
|
|
const index = node.widgets.indexOf(nameWidget);
|
|
if (index !== -1) {
|
|
node.widgets.splice(index, 1);
|
|
nameWidget = node.addWidget("combo", "name", initialSavedName, () => { }, {
|
|
values: [initialSavedName !== "None" ? initialSavedName : "None"]
|
|
});
|
|
const newIdx = node.widgets.indexOf(nameWidget);
|
|
if (newIdx !== -1 && newIdx !== index) {
|
|
node.widgets.splice(newIdx, 1);
|
|
node.widgets.splice(index, 0, nameWidget);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!nameWidget.options) {
|
|
nameWidget.options = { values: [initialSavedName] };
|
|
}
|
|
|
|
let namesConfig = [];
|
|
|
|
// Fetch names from backend for a given CSV
|
|
async function loadNames(csvValue, preferredName) {
|
|
if (!csvValue || csvValue === "None") return;
|
|
try {
|
|
const resp = await fetch(`/sata/prompt_machine/names?csv=${encodeURIComponent(csvValue)}`);
|
|
const data = await resp.json();
|
|
namesConfig = (data.names && data.names.length > 0) ? data.names : ["None"];
|
|
|
|
refreshNames(preferredName);
|
|
} catch (err) {
|
|
console.error("[PromptMachineFrontend] Failed to load names:", err);
|
|
}
|
|
}
|
|
|
|
// Refresh dropdown with available names while preserving target selection
|
|
function refreshNames(preferredName) {
|
|
const available = namesConfig.length > 0 ? namesConfig : ["None"];
|
|
nameWidget.options.values = available;
|
|
|
|
const targetVal = preferredName !== undefined ? preferredName : nameWidget.value;
|
|
if (available.includes(targetVal)) {
|
|
nameWidget.value = targetVal;
|
|
} else {
|
|
nameWidget.value = available[0] || "None";
|
|
}
|
|
|
|
// Force update previews for current name
|
|
updatePreviews(nameWidget.value);
|
|
|
|
if (app.graph) {
|
|
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
|
|
if (app.graph.change) app.graph.change();
|
|
}
|
|
}
|
|
|
|
// Hook CSV change (user triggered)
|
|
const oldCsvCallback = csvWidget.callback;
|
|
csvWidget.callback = function (value) {
|
|
if (oldCsvCallback) oldCsvCallback.apply(this, arguments);
|
|
loadNames(value); // refresh without preferred name (defaults to first available)
|
|
};
|
|
|
|
// --- Preview Logic ---
|
|
let previewEl = node.widgets?.find(w => w.name === "preview_text")?.element;
|
|
if (!previewEl) {
|
|
previewEl = document.createElement("div");
|
|
Object.assign(previewEl.style, {
|
|
marginTop: "10px",
|
|
padding: "8px",
|
|
background: "rgba(0,0,0,0.4)",
|
|
borderRadius: "4px",
|
|
fontSize: "14px",
|
|
lineHeight: "1.5",
|
|
color: "#ddd",
|
|
wordBreak: "break-word",
|
|
maxHeight: "400px",
|
|
overflowY: "auto"
|
|
});
|
|
|
|
node.addDOMWidget("preview_text", "html", previewEl, {
|
|
getValue: () => previewEl.innerHTML,
|
|
setValue: (v) => { previewEl.innerHTML = v; },
|
|
serialize: false
|
|
});
|
|
}
|
|
|
|
async function updatePreviews(name) {
|
|
const csv = csvWidget.value;
|
|
if (!csv || !name || name === "None") {
|
|
previewEl.innerHTML = "<i>No preview available</i>";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const resp = await fetch(`/sata/prompt_machine/get?csv=${encodeURIComponent(csv)}&name=${encodeURIComponent(name)}`);
|
|
const data = await resp.json();
|
|
let html = "";
|
|
if (data.positive) html += `${data.positive}`;
|
|
|
|
previewEl.innerHTML = html || "<i>No preview available</i>";
|
|
|
|
if (node.computeSize && node.setSize) {
|
|
node.setSize(node.computeSize());
|
|
}
|
|
|
|
if (app.graph) {
|
|
if (app.graph.setDirtyCanvas) app.graph.setDirtyCanvas(true, true);
|
|
if (app.graph.change) app.graph.change();
|
|
}
|
|
} catch (err) {
|
|
console.error("[PromptMachineFrontend] Failed to load preview:", err);
|
|
}
|
|
}
|
|
|
|
// Hook Name change
|
|
const oldNameCallback = nameWidget.callback;
|
|
nameWidget.callback = function (value) {
|
|
if (oldNameCallback) oldNameCallback.apply(this, arguments);
|
|
updatePreviews(value);
|
|
};
|
|
|
|
// Initial sync preserving saved value
|
|
loadNames(csvWidget.value, initialSavedName);
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "SATA_UtilityNode.PromptMachineFrontend",
|
|
async nodeCreated(node) {
|
|
if (node.comfyClass === "Prompt_Machine") {
|
|
setupPromptMachine(node);
|
|
} else if (node.comfyClass === "Prompt_Style_Machine") {
|
|
setupPromptStyleMachine(node);
|
|
}
|
|
},
|
|
async loadedGraphNode(node) {
|
|
if (node.comfyClass === "Prompt_Machine") {
|
|
setupPromptMachine(node);
|
|
} else if (node.comfyClass === "Prompt_Style_Machine") {
|
|
setupPromptStyleMachine(node);
|
|
}
|
|
}
|
|
}); |