Files
JasonHoku-ComfyUI-Ultimate-…/resources/logic_utils.js
T
Jason HokuandClaude Opus 4.6 1fc660ad78 fix: Revise & Generate sends complete config with model, seed, prompts
The GENERATE NEW button was only sending sampler/scheduler/steps/cfg/
denoise/lora — missing model, seed, prompts, and all advanced settings.
The generated image would use whatever the sampler node had as defaults
instead of the actual parameters from the source image.

Fixed triggerGen() to build a complete config entry:
- Model and seed from the (now editable) modal fields
- Positive and negative prompts from the modal textareas
- seed_behavior set to "fixed" so the exact seed is used
- All advanced settings carried from the source item: vae, clip_type,
  model_type, text_encoders, attention_mode, clip_skip,
  model_sampling_override + shift values, advanced_sampling,
  flux_guidance, gguf_options

Also wraps output in {"configs": [...]} format (was sending bare array).

Template changes:
- Model input: removed disabled attribute + opacity
- Seed input: removed disabled attribute + opacity
Both are now editable so users can change model or seed before re-gen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:18:37 -07:00

841 lines
34 KiB
JavaScript

/**
* UTILITIES & API
* Handles server communication and generic helpers.
* FIXED: Session reload now properly re-renders grid
*/
function toggleFullscreen() {
// Send current session name so fullscreen reload uses the right session
var currentSession = document.getElementById('session-input')?.value || '';
window.parent.postMessage({ type: 'toggle_fullscreen', node_id: TARGET_NODE_ID, session_name: currentSession }, '*');
}
function setupKeyReloadFullscreen() {
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
switch (e.key) {
case 'f':
toggleFullscreen()
break;
case 'r':
loadSession()
}
})
}
// Persist session to disk
async function saveState() {
try {
await fetch('/config_tester/save_manifest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_name: document.getElementById('session-input')?.value || "default",
manifest: fullManifest
})
});
} catch (e) { console.error("Save failed", e); }
}
// Load session from disk
async function loadSession() {
const sessInput = document.getElementById('session-input');
if (!sessInput) return;
const sess = sessInput.value;
console.log(`[Load] 🔄 Loading session: ${sess}`);
// UI Feedback
const grid = document.getElementById('grid');
if (grid) grid.style.opacity = '0.5';
try {
const r = await fetch(`/view?filename=manifest.json&type=output&subfolder=benchmarks/${sess}&t=${Date.now()}`);
if (!r.ok) throw new Error("Session not found");
const data = await r.json();
console.log(`[Load] 📊 Loaded ${data.items?.length || 0} items`);
// 1. CRITICAL FIX: Reset viewport position
const viewport = document.getElementById('viewport');
const canvas = document.getElementById('canvas');
// Reset pan/zoom to defaults
if (typeof resetZoom === 'function') {
// This resets currentScale, panOffsetX, panOffsetY
// currentScale = 1;
// panOffsetX = 0;
// panOffsetY = 0;
// if (canvas) {
// canvas.style.transform = 'translate(0px, 0px) scale(1)';
// }
}
// 2. Clear grid completely
if (grid) {
grid.innerHTML = '';
grid.style.paddingTop = '0px';
grid.style.height = '0px';
}
// 3. Clear all caches
nodeMap.clear();
// Clear filter sets
['sampler', 'scheduler', 'lora', 'denoise', 'model', 'positive', 'negative', 'size', 'seed'].forEach(k => {
if (filters[k]) filters[k].clear();
});
// 4. Swap data (CRITICAL: Do this before pipeline)
fullManifest = data;
activeData = fullManifest.items || [];
meta = fullManifest.meta || {};
console.log(`[Load] 📦 activeData now has ${activeData.length} items`);
// 4b. Recompute label global values with new data so unique-only works on first load
if (typeof computeLabelGlobalValues === 'function' && labelMode && labelMode.enabled) {
computeLabelGlobalValues();
}
// 5. Reset indices
refreshIndices();
// 7. Re-initialize filters (don't skip this!)
if (typeof initFilters === 'function') {
initFilters();
}
// Initialize search filter UI
if (typeof renderSearchFilters === 'function') {
renderSearchFilters();
}
// 8. CRITICAL FIX: Force pipeline and render
// Wait a tick for the DOM to update, then process
await new Promise(resolve => setTimeout(resolve, 10));
console.log(`[Load] 🔧 Running pipeline...`);
updateDataPipeline();
// 9. Wait for pipeline to complete, then render
await new Promise(resolve => setTimeout(resolve, 100));
console.log(`[Load] 🎨 Rendering ${processedData?.length || 0} processed items...`);
// Force a full render
if (typeof renderDOM === 'function') {
renderDOM();
}
// 11. Restore UI
if (grid) grid.style.opacity = '1';
// Close Settings Panel
const cogPanel = document.getElementById('cog-menu-dropdown');
const cogOverlay = document.getElementById('cog-menu-overlay');
if (cogPanel) cogPanel.style.display = 'none';
if (cogOverlay) cogOverlay.style.display = 'none';
document.body.style.overflow = '';
// Hide landing page, show viewport
const landing = document.getElementById('session-landing');
if (landing) landing.style.display = 'none';
if (viewport) viewport.style.display = 'block';
console.log('[Load] ✅ Session loaded successfully');
} catch (e) {
console.error('[Load] ❌ Load failed:', e);
if (grid) grid.style.opacity = '1';
alert("Load Error: " + e.message);
}
}
/**
* Merge another session's images into the current view.
* Appends items from the target session, tagging each with a _session field.
* Does not save — the merge is view-only until user explicitly saves.
*/
async function mergeSession(sessionName) {
if (!sessionName) return;
console.log(`[Merge] 🔄 Merging session: ${sessionName}`);
try {
const r = await fetch(`/view?filename=manifest.json&type=output&subfolder=benchmarks/${sessionName}&t=${Date.now()}`);
if (!r.ok) throw new Error("Session not found: " + sessionName);
const data = await r.json();
const newItems = data.items || [];
if (newItems.length === 0) {
alert("No items in session: " + sessionName);
return;
}
// Tag items with source session and deduplicate by id
const existingIds = new Set(activeData.map(function(d) { return d.id; }));
var added = 0;
for (var i = 0; i < newItems.length; i++) {
newItems[i]._session = sessionName;
if (!existingIds.has(newItems[i].id)) {
fullManifest.items.push(newItems[i]);
added++;
}
}
// Update references
activeData = fullManifest.items;
console.log(`[Merge] 📊 Added ${added} items from "${sessionName}" (${newItems.length - added} duplicates skipped)`);
// Rebuild everything
refreshIndices();
if (typeof computeLabelGlobalValues === 'function' && labelMode && labelMode.enabled) {
computeLabelGlobalValues();
}
if (typeof initFilters === 'function') initFilters();
if (typeof renderSearchFilters === 'function') renderSearchFilters();
updateDataPipeline();
// Show merge notification
var notice = document.createElement('div');
notice.style.cssText = 'position: fixed; top: 60px; left: 50%; transform: translateX(-50%); background: #00aa44; color: #fff; padding: 8px 20px; border-radius: 6px; font-size: 13px; z-index: 9999; pointer-events: none;';
notice.textContent = 'Merged ' + added + ' images from "' + sessionName + '"';
document.body.appendChild(notice);
setTimeout(function() { notice.style.opacity = '0'; notice.style.transition = 'opacity 0.5s'; }, 2000);
setTimeout(function() { if (notice.parentNode) notice.parentNode.removeChild(notice); }, 2500);
} catch (e) {
console.error('[Merge] ❌ Merge failed:', e);
alert("Merge Error: " + e.message);
}
}
// Load session from picker dropdown or session card
function filterSessionPicker(query) {
const rows = document.querySelectorAll('.session-picker-row');
const q = query.toLowerCase().trim();
rows.forEach(row => {
const name = (row.dataset.name || '').toLowerCase();
row.style.display = (!q || name.includes(q)) ? '' : 'none';
});
}
function loadSessionFromPicker(sessionName) {
if (!sessionName) return;
const sessInput = document.getElementById('session-input');
if (sessInput) sessInput.value = sessionName;
loadSession();
}
// Fetch available sessions and populate picker dropdown + landing page
async function fetchAndShowSessions() {
try {
const resp = await fetch('/config_tester/list_sessions');
if (!resp.ok) return;
const sessions = await resp.json();
if (!sessions || sessions.length === 0) return;
// Populate the session picker list in cog menu (already sorted by most recent from backend)
const pickerList = document.getElementById('session-picker-list');
if (pickerList) {
pickerList.textContent = ''; // Clear existing entries
window._sessionPickerData = sessions; // Store for search filtering
sessions.forEach(s => {
const row = document.createElement('div');
row.className = 'session-picker-row';
row.dataset.name = s.name;
row.style.cssText = 'padding: 5px 8px; cursor: pointer; font-size: 11px; color: #ccc; border-bottom: 1px solid #222;';
row.onmouseenter = () => { row.style.background = '#333'; };
row.onmouseleave = () => { row.style.background = 'none'; };
row.onclick = () => loadSessionFromPicker(s.name);
const date = new Date(s.mtime * 1000);
const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
const nameSpan = document.createElement('span');
nameSpan.style.color = '#fff';
nameSpan.textContent = s.name;
const metaSpan = document.createElement('span');
metaSpan.style.cssText = 'color:#666; font-size:10px;';
metaSpan.textContent = ` (${s.item_count} imgs, ${dateStr})`;
// Merge button — adds this session's images to current view
const mergeBtn = document.createElement('button');
mergeBtn.textContent = '+Merge';
mergeBtn.title = 'Merge this session into current view';
mergeBtn.style.cssText = 'float: right; background: #335; color: #88f; border: 1px solid #446; border-radius: 3px; padding: 0 4px; font-size: 9px; cursor: pointer; margin-left: 4px;';
mergeBtn.onclick = function(e) { e.stopPropagation(); mergeSession(s.name); };
row.appendChild(mergeBtn);
row.appendChild(nameSpan);
row.appendChild(metaSpan);
pickerList.appendChild(row);
});
}
// Render session cards on the landing page
const cardsContainer = document.getElementById('session-cards');
const landingEl = document.getElementById('session-landing');
if (cardsContainer && landingEl) {
cardsContainer.innerHTML = '';
sessions.forEach(s => {
const card = document.createElement('div');
card.style.cssText = 'width: 180px; background: #2a2a2a; border: 1px solid #444; border-radius: 8px; overflow: hidden; cursor: pointer; transition: border-color 0.2s, transform 0.15s;';
card.onmouseenter = () => { card.style.borderColor = '#0af'; card.style.transform = 'translateY(-2px)'; };
card.onmouseleave = () => { card.style.borderColor = '#444'; card.style.transform = 'none'; };
card.onclick = () => loadSessionFromPicker(s.name);
// Thumbnail
const thumbDiv = document.createElement('div');
thumbDiv.style.cssText = 'width: 100%; height: 120px; background: #1a1a1a; display: flex; align-items: center; justify-content: center; overflow: hidden;';
if (s.first_image) {
const img = document.createElement('img');
img.src = `/view?filename=${encodeURIComponent(s.first_image.split('/').pop())}&type=output&subfolder=${encodeURIComponent(s.first_image.substring(0, s.first_image.lastIndexOf('/')))}&t=${Date.now()}`;
img.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';
img.onerror = () => { img.style.display = 'none'; thumbDiv.textContent = '📷'; thumbDiv.style.fontSize = '24px'; thumbDiv.style.color = '#555'; };
thumbDiv.appendChild(img);
} else {
thumbDiv.textContent = '📷';
thumbDiv.style.fontSize = '24px';
thumbDiv.style.color = '#555';
}
card.appendChild(thumbDiv);
// Info
const info = document.createElement('div');
info.style.cssText = 'padding: 8px; font-size: 11px;';
const nameEl = document.createElement('div');
nameEl.style.cssText = 'font-weight: bold; color: #ddd; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;';
nameEl.textContent = s.name;
nameEl.title = s.name;
info.appendChild(nameEl);
const metaEl = document.createElement('div');
metaEl.style.cssText = 'color: #888; font-size: 10px;';
const date = new Date(s.mtime * 1000);
metaEl.textContent = `${s.item_count} images · ${date.toLocaleDateString()}`;
info.appendChild(metaEl);
card.appendChild(info);
cardsContainer.appendChild(card);
});
}
} catch (e) {
console.error('[Sessions] Failed to fetch sessions:', e);
}
}
// Show session landing page if no data is loaded
function showSessionLandingIfEmpty() {
const hasData = fullManifest && fullManifest.items && fullManifest.items.length > 0;
const landing = document.getElementById('session-landing');
const viewport = document.getElementById('viewport');
// Always populate session picker dropdown and landing cards
fetchAndShowSessions();
if (!hasData && landing && viewport) {
landing.style.display = 'block';
viewport.style.display = 'none';
} else if (landing && viewport) {
landing.style.display = 'none';
viewport.style.display = 'block';
}
}
/**
* Export favorited items as a Config Builder JSON — ready to paste into
* the Config Builder's lora_config widget to regenerate the same images
* with different settings (size, steps, etc.).
*
* Groups favorites by unique (model, lora, prompt) combos into config
* arrays. Reads ALL fields from the manifest items — nothing is defaulted
* unless the item genuinely doesn't have that field.
*/
function exportFavoritesAsConfigJSON() {
if (!activeData || activeData.length === 0) {
alert('No data loaded');
return;
}
var favorites = activeData.filter(function(d) { return d.favorited; });
if (favorites.length === 0) {
alert('No favorited items to export');
return;
}
// Build flat config entries — one per favorite item.
// Each config is a direct match for what the sampler node's expand_configs() expects:
// single values for sampler, scheduler, model, etc. (NOT arrays).
var flatConfigs = [];
var seen = {};
for (var fi = 0; fi < favorites.length; fi++) {
var d = favorites[fi];
// Build the flat config from the manifest item's actual fields
var flatConfig = {
sampler: d.sampler || 'euler',
scheduler: d.scheduler || 'normal',
steps: d.steps || 20,
cfg: d.cfg || 7,
denoise: d.denoise || 1,
seed: d.seed || 0,
seed_behavior: 'fixed',
model: d.model || meta.model || 'None',
lora: d.lora || 'None',
vae: d.vae || (d.model_type && d.model_type !== 'checkpoint' ? 'None' : 'Default'),
clip_type: d.clip_type || 'stable_diffusion',
positive: d.config_positive || d.positive || meta.positive || '',
negative: d.config_negative || d.negative || meta.negative || '',
attention_mode: d.attention_mode || 'default'
};
// Copy model_type directly from manifest item
if (d.model_type) flatConfig.model_type = d.model_type;
// Optional fields — only include if present on the item
if (d.text_encoders && d.text_encoders.length > 0) flatConfig.text_encoders = d.text_encoders;
if (d.gguf_options) flatConfig.gguf_options = d.gguf_options;
if (d.model_prompt_prefix) flatConfig.model_prompt_prefix = d.model_prompt_prefix;
if (d.model_prompt_suffix) flatConfig.model_prompt_suffix = d.model_prompt_suffix;
if (d.model_sampling_override && d.model_sampling_override !== 'none') {
flatConfig.model_sampling_override = d.model_sampling_override;
if (d.model_sampling_shift) flatConfig.model_sampling_shift = d.model_sampling_shift;
if (d.model_sampling_flux_max_shift) flatConfig.model_sampling_flux_max_shift = d.model_sampling_flux_max_shift;
if (d.model_sampling_flux_base_shift) flatConfig.model_sampling_flux_base_shift = d.model_sampling_flux_base_shift;
}
if (d.use_advanced_sampling) {
flatConfig.use_advanced_sampling = true;
flatConfig.advanced_guider = d.advanced_guider || 'cfg_guider';
flatConfig.advanced_scheduler = d.advanced_scheduler || 'basic';
}
if (d.use_flux_guidance) {
flatConfig.use_flux_guidance = true;
flatConfig.flux_guidance_value = d.flux_guidance_value || 3.5;
}
if (d.clip_skip && d.clip_skip !== -1) flatConfig.clip_skip = d.clip_skip;
// Deduplicate: skip if we already have an identical config
// (same model + lora + sampler + scheduler + steps + cfg + seed + prompt)
var dedupKey = [flatConfig.model, flatConfig.lora, flatConfig.sampler,
flatConfig.scheduler, flatConfig.steps, flatConfig.cfg,
flatConfig.seed, flatConfig.positive].join('|');
if (seen[dedupKey]) continue;
seen[dedupKey] = true;
flatConfigs.push(flatConfig);
}
// Wrap in the format the sampler node expects: {"configs": [...]}
var configOutput = {
configs: flatConfigs
};
// Check for GGUF/diffusion models that need a VAE connected
var needsVaeWarning = flatConfigs.some(function(c) {
return c.model_type && c.model_type !== 'checkpoint' && (!c.vae || c.vae === 'Default' || c.vae === 'None');
});
var vaeNote = needsVaeWarning
? '\n\n\u26a0\ufe0f Some configs use GGUF/diffusion models \u2014 make sure to connect a VAE to the sampler node\'s optional_vae input.'
: '';
// Copy to clipboard
var jsonStr = JSON.stringify(configOutput, null, 2);
navigator.clipboard.writeText(jsonStr).then(function() {
alert('Config JSON copied to clipboard! (' + favorites.length + ' favorites \u2192 ' + flatConfigs.length + ' config' + (flatConfigs.length !== 1 ? 's' : '') + ')\n\nPaste into the Sampler Grid node\'s configs_json input.' + vaeNote);
}).catch(function() {
// Fallback: show in a prompt dialog
prompt('Copy this Config JSON:', jsonStr);
});
console.log('[Export] Exported ' + favorites.length + ' favorites as ' + flatConfigs.length + ' configs');
}
// Export favorited images to benchmark_favorites folder
async function exportFavorites() {
const statusEl = document.getElementById('export-status');
const btn = event.target;
// Get session name
const sessInput = document.getElementById('session-input');
if (!sessInput) {
if (statusEl) statusEl.innerText = '❌ Error: Session input not found';
return;
}
const sessionName = sessInput.value;
// Get pack metadata checkbox state
const packCheckbox = document.getElementById('pack-metadata-checkbox');
const packMetadata = packCheckbox ? packCheckbox.checked : false;
// Get organize by prompt checkbox state
const organizeCheckbox = document.getElementById('organize-by-prompt-checkbox');
const organizeByPrompt = organizeCheckbox ? organizeCheckbox.checked : false;
// Get organize by lora checkbox state
const organizeLoraCheckbox = document.getElementById('organize-by-lora-checkbox');
const organizeByLora = organizeLoraCheckbox ? organizeLoraCheckbox.checked : false;
// Get export prompt txt checkbox state
const promptTxtCheckbox = document.getElementById('export-prompt-txt-checkbox');
const exportPromptTxt = promptTxtCheckbox ? promptTxtCheckbox.checked : false;
// Get copy manifest checkbox state
const copyManifestCheckbox = document.getElementById('copy-manifest-checkbox');
const copyManifest = copyManifestCheckbox ? copyManifestCheckbox.checked : true;
// Get pack workflow checkbox state
const packWorkflowCheckbox = document.getElementById('pack-workflow-checkbox');
const packWorkflow = packWorkflowCheckbox ? packWorkflowCheckbox.checked : false;
// Get pack nodes workflow checkbox state
const packNodesWorkflowCheckbox = document.getElementById('pack-nodes-workflow-checkbox');
const packNodesWorkflow = packNodesWorkflowCheckbox ? packNodesWorkflowCheckbox.checked : false;
// Capture current ComfyUI workflow if pack_workflow is checked
let workflowData = null;
if (packWorkflow) {
try {
const app = window.app || window.parent?.app;
if (app && app.graph) {
workflowData = app.graph.serialize();
console.log('[Export] Captured current ComfyUI workflow');
} else {
console.warn('[Export] Could not access ComfyUI app.graph — workflow will not be packed');
}
} catch (e) {
console.warn('[Export] Error capturing workflow:', e);
}
}
// Generate per-image nodes workflows if pack_nodes_workflow is checked
let nodesWorkflows = null;
if (packNodesWorkflow && typeof buildComfyNodesWorkflow === 'function') {
try {
nodesWorkflows = {};
// activeData is the global array of all manifest items in the dashboard
const favoritedItems = activeData.filter(item => item.favorited);
for (const item of favoritedItems) {
const wf = buildComfyNodesWorkflow(item);
// Key by the item's file path so backend can match
nodesWorkflows[item.file || item.id] = wf;
}
console.log(`[Export] Generated ${Object.keys(nodesWorkflows).length} per-image node workflows`);
} catch (e) {
console.warn('[Export] Error generating node workflows:', e);
}
}
// Show loading state
if (statusEl) {
statusEl.innerText = '⏳ Exporting favorites...';
statusEl.style.color = '#ffaa00';
}
if (btn) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.style.cursor = 'wait';
}
try {
const response = await fetch('/config_tester/export_favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_name: sessionName,
pack_metadata: packMetadata,
organize_by_prompt: organizeByPrompt,
organize_by_lora: organizeByLora,
export_prompt_txt: exportPromptTxt,
copy_manifest: copyManifest,
pack_workflow: packWorkflow,
pack_nodes_workflow: packNodesWorkflow,
workflow_data: workflowData,
nodes_workflows: nodesWorkflows
})
});
const resultText = await response.text();
if (response.ok) {
if (statusEl) {
statusEl.innerText = '✅ ' + resultText;
statusEl.style.color = '#4caf50';
}
// Reset status after 5 seconds
setTimeout(() => {
if (statusEl) {
statusEl.innerText = '';
}
}, 5000);
} else {
if (statusEl) {
statusEl.innerText = '❌ Error: ' + resultText;
statusEl.style.color = '#ff3860';
}
}
} catch (error) {
console.error('[Export] Error:', error);
if (statusEl) {
statusEl.innerText = '❌ Network error: ' + error.message;
statusEl.style.color = '#ff3860';
}
} finally {
// Re-enable button
if (btn) {
btn.disabled = false;
btn.style.opacity = '1';
btn.style.cursor = 'pointer';
}
}
}
// Delete all non-favorited items from the session
async function deleteNonFavorites() {
if (!window.confirm("Are you sure you want to delete all non-favorited items? This cannot be undone.")) {
return;
}
const statusEl = document.getElementById('delete-status');
const sessInput = document.getElementById('session-input');
if (!sessInput) {
if (statusEl) statusEl.innerText = '❌ Error: Session input not found';
return;
}
const sessionName = sessInput.value;
if (statusEl) {
statusEl.innerText = '⏳ Deleting non-favorited items...';
statusEl.style.color = '#ffaa00';
}
try {
const response = await fetch('/config_tester/delete_non_favorites', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_name: sessionName })
});
const resultText = await response.text();
if (response.ok) {
if (statusEl) {
statusEl.innerText = '✅ ' + resultText;
statusEl.style.color = '#4caf50';
}
// Reload the page to refresh the dashboard with updated manifest
setTimeout(() => { location.reload(); }, 2000);
} else {
if (statusEl) {
statusEl.innerText = '❌ Error: ' + resultText;
statusEl.style.color = '#ff3860';
}
}
} catch (error) {
console.error('[Delete] Error:', error);
if (statusEl) {
statusEl.innerText = '❌ Network error: ' + error.message;
statusEl.style.color = '#ff3860';
}
}
}
// Reject a specific image (X button)
function rejectItem(element) {
const card = element.closest('.card');
if (!card || !card._dataItem) return;
const item = card._dataItem;
item.rejected = true;
if (typeof markItemChanged === "function") {
markItemChanged(item);
} else {
pendingSaveItems.set(item.id, item);
scheduleBatchedSave();
}
card.style.opacity = '0';
card.style.transform = 'scale(0.9)';
card.style.pointerEvents = 'none';
setTimeout(() => {
updateDataPipeline();
scheduleJSONUpdate();
}, 100);
}
// Helper to select text in JSON bars
function selectJSON(id) {
const r = document.createRange();
r.selectNode(document.getElementById(id));
window.getSelection().removeAllRanges();
window.getSelection().addRange(r);
}
// Trigger generation from Modal
async function triggerGen(btn) {
// Build a complete config from the modal fields + the original item's metadata.
// This produces an exact config_json entry that the sampler can run directly.
const d = activeData ? activeData.find(x => x.id === window.currentModalId) : null;
const config = {
sampler: document.getElementById('f-smp').value,
scheduler: document.getElementById('f-sch').value,
steps: parseInt(document.getElementById('f-stp').value) || 20,
cfg: parseFloat(document.getElementById('f-cfg').value) || 7,
denoise: parseFloat(document.getElementById('f-den').value) || 1,
lora: document.getElementById('f-lor').value || 'None',
model: document.getElementById('f-model').value || 'None',
seed: parseInt(document.getElementById('f-seed').value) || 0,
seed_behavior: 'fixed',
positive: document.getElementById('f-pos').value || '',
negative: document.getElementById('f-neg').value || ''
};
// Carry over additional fields from the original item if available
if (d) {
if (d.vae && d.vae !== 'Default') config.vae = d.vae;
if (d.clip_type) config.clip_type = d.clip_type;
if (d.model_type && d.model_type !== 'checkpoint') config.model_type = d.model_type;
if (d.text_encoders && d.text_encoders.length > 0) config.text_encoders = d.text_encoders;
if (d.attention_mode && d.attention_mode !== 'default') config.attention_mode = d.attention_mode;
if (d.clip_skip && d.clip_skip !== -1) config.clip_skip = d.clip_skip;
if (d.model_sampling_override && d.model_sampling_override !== 'none') {
config.model_sampling_override = d.model_sampling_override;
if (d.model_sampling_shift) config.model_sampling_shift = d.model_sampling_shift;
if (d.model_sampling_flux_max_shift) config.model_sampling_flux_max_shift = d.model_sampling_flux_max_shift;
if (d.model_sampling_flux_base_shift) config.model_sampling_flux_base_shift = d.model_sampling_flux_base_shift;
}
if (d.use_advanced_sampling) {
config.use_advanced_sampling = true;
config.advanced_guider = d.advanced_guider || 'cfg_guider';
config.advanced_scheduler = d.advanced_scheduler || 'basic';
}
if (d.use_flux_guidance) {
config.use_flux_guidance = true;
config.flux_guidance_value = d.flux_guidance_value || 3.5;
}
if (d.gguf_options) config.gguf_options = d.gguf_options;
}
// Wrap in the format the sampler expects
const configOutput = { configs: [config] };
const jsonStr = JSON.stringify(configOutput, null, 2);
try {
// Communicate with ComfyUI Graph
const graph = window.parent.app.graph;
const node = graph._nodes.find(n => n.type === "UltimateSamplerGrid");
if (node) {
const widget = node.widgets.find(w => w.name === "configs_json");
if (widget) {
widget.value = jsonStr;
window.parent.app.queuePrompt(0);
const b = btn; b.innerText = "QUEUED!";
setTimeout(() => { closeM(); b.innerText = "GENERATE NEW"; }, 1000);
}
}
} catch (e) { alert("Error: " + e); }
}
// Scan an external directory and load it as a session
async function scanDirectory() {
const pathInput = document.getElementById('scan-directory-input');
const statusEl = document.getElementById('scan-status');
const btn = document.getElementById('scan-directory-btn');
if (!pathInput || !pathInput.value.trim()) {
if (statusEl) {
statusEl.innerText = 'Please enter a directory path';
statusEl.style.color = '#ff3860';
}
return;
}
const directoryPath = pathInput.value.trim();
// Use session input value if set, otherwise auto-generate from dir name
const sessInput = document.getElementById('session-input');
let sessionName = '';
// Auto-generate from directory name
const dirParts = directoryPath.replace(/\\/g, '/').split('/').filter(Boolean);
sessionName = 'scan-' + (dirParts[dirParts.length - 1] || 'unnamed').replace(/[^\w-]/g, '');
// UI feedback - scanning
if (statusEl) {
statusEl.innerText = 'Scanning directory...';
statusEl.style.color = '#ffaa00';
}
if (btn) {
btn.disabled = true;
btn.style.opacity = '0.6';
btn.textContent = 'SCANNING...';
}
try {
const response = await fetch('/config_tester/scan_directory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
directory_path: directoryPath,
session_name: sessionName
})
});
const result = await response.json();
if (response.ok) {
const fromManifest = result.from_manifest || 0;
let metaInfo;
if (fromManifest > 0) {
metaInfo = `${fromManifest} from manifest`;
if (result.with_metadata > fromManifest) {
metaInfo += `, ${result.with_metadata - fromManifest} with embedded metadata`;
}
} else {
metaInfo = result.with_metadata > 0
? `${result.with_metadata} with metadata`
: 'no metadata found';
}
if (statusEl) {
statusEl.innerText = `Found ${result.item_count} images (${metaInfo}). Loading...`;
statusEl.style.color = '#4caf50';
}
// Update session input with the scan session name
if (sessInput) sessInput.value = result.session_name;
// Load the session into the dashboard using existing infrastructure
await loadSession();
if (statusEl) {
statusEl.innerText = `Loaded ${result.item_count} images (${metaInfo})`;
statusEl.style.color = '#4caf50';
}
} else {
if (statusEl) {
statusEl.innerText = 'Error: ' + (result.error || response.statusText);
statusEl.style.color = '#ff3860';
}
}
} catch (error) {
console.error('[Scan] Error:', error);
if (statusEl) {
statusEl.innerText = 'Network error: ' + error.message;
statusEl.style.color = '#ff3860';
}
} finally {
if (btn) {
btn.disabled = false;
btn.style.opacity = '1';
btn.textContent = 'SCAN';
}
}
}
setupKeyReloadFullscreen()