// 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 = "No preview available"; 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 || "No preview available"; 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); } } });