Files
JasonHoku-ComfyUI-Ultimate-…/resources/template.html
T
Jason HokuandClaude Fable 5.1 a1f3d3f646 feat(dashboard): Stats panel per setting (Roadmap 26 M1) + Sobel edge metric (Roadmap 25)
- resources/logic_analytics.js: computeAnalytics(items) in ONE pass over the items: per-axis rows
  (model, lora, sampler, scheduler, steps, cfg, denoise, resolution, attention, upscale, pass,
  prompt, and every wf_params key) with count, favorites %, rejects %, mean duration, mean of
  every builtin metric and the pairwise rating; sortAnalyticsRows, analyticsToMarkdown
- resources/logic_analytics_panel.js: docked non-modal panel (header button + cog entry), scope
  "current filter (favs/rejects counted)" or "whole session", sortable columns, dim rows under
  n=3, Copy Markdown, re-renders (throttled) on every data change, Escape closes; builds DOM
  with textContent only
- logic_pipeline.js: pipelineFilterCtx/pipelineMatches extracted (behaviour identical)
- sobel: mean Sobel gradient magnitude of luma (kernels /4; a full step edge = 1), numpy and
  torch parity; Dashboard sort/filter/labels, Builder metric list; backfill gains fill_missing
  (adds keys missing from older items, never changes stored values); the distributed metrics
  pass fills metrics a worker on an older USCG omitted
- tests: 15 python, 12 node, 5 Playwright (incl. an XSS-shaped manifest value rendered as text)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 18:49:25 -08:00

1039 lines
67 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>USCG - __TITLE__</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<style>
/* INJECTED CSS */
__CSS_CONTENT__
</style>
</head>
<body>
<!-- COMPACT SINGLE-LINE HEADER -->
<div id="header">
<button class="filter-popup-btn" onclick="toggleFiltersPopup()" title="Open Filters & Info">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon>
</svg>
</button>
<div style="flex-grow: 1;"></div>
<div class="meta-box" style="overflow: visible;"> <span class="meta-label">Sort</span>
<div class="sort-container" id="sort-container">
<button class="sort-btn" id="sort-main-btn" onclick="toggleSortPopup()">
<span id="sort-primary-label">Oldest</span>
<span style="opacity:0.5; font-size:8px;">▼</span>
</button>
<div id="sort-popup" class="sort-popup-panel" style="display: none;">
<div class="sort-col">
<div class="sort-col-header" style="color:var(--accent);">PRIMARY</div>
<div id="sort-list-primary">
</div>
</div>
<div class="sort-col">
<div class="sort-col-header" style="color:var(--accent-sch);">SECONDARY</div>
<div id="sort-list-secondary">
</div>
</div>
</div>
</div>
</div>
<button class="fs-btn" id="compare-open-btn" onclick="openCompareMode()" title="Compare (C)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="4" width="8" height="16" rx="1.5"></rect>
<rect x="14" y="4" width="8" height="16" rx="1.5"></rect>
</svg>
</button>
<button class="fs-btn" id="analytics-open-btn" onclick="toggleAnalyticsPanel()" title="Stats per setting (favorites rate, time, metrics)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="20" x2="6" y2="12"></line><line x1="12" y1="20" x2="12" y2="6"></line><line x1="18" y1="20" x2="18" y2="9"></line></svg>
</button>
<button class="fs-btn" onclick="loadSession()" title="Reload Session (R)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button class="fs-btn" onclick="resetZoom();" title="Center Grid (0)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="22" y1="12" x2="18" y2="12"></line>
<line x1="6" y1="12" x2="2" y2="12"></line>
<line x1="12" y1="6" x2="12" y2="2"></line>
<line x1="12" y1="22" x2="12" y2="18"></line>
</svg>
</button>
<!-- COG WHEEL MENU -->
<div class="cog-menu-container">
<button class="action-btn" onclick="toggleCogMenu()" title="Settings">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
</div>
<button class="fs-btn" id="open-standalone-btn" onclick="openStandalone()" title="Open standalone dashboard (new tab, phone-friendly URL)">↗</button>
<button class="fs-btn" id="session-switch-btn" onclick="uscgOpenSessionPicker()" title="Switch session" style="display:none;">&#128194;</button>
<button class="fs-btn" id="share-link-btn" onclick="uscgShareLink(this)" title="Share / copy a link to this session" style="display:none;">&#128279;</button>
<button class="fs-btn" onclick="toggleFullscreen()" title="Toggle Fullscreen">⛶</button>
</div>
<!-- SETTINGS PANEL (Unified Cog + Session) -->
<div id="cog-menu-dropdown" class="cog-menu-panel" style="display: none;">
<div class="cog-panel-header">
<span style="font-size: 13px; font-weight: 700; color: #fff;">Settings</span>
<button class="close-popup-btn" onclick="toggleCogMenu()">&#10005;</button>
</div>
<div class="cog-panel-content">
<!-- Section 1: Session Management -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">SESSION</div>
<input id="session-input" class="session-name-input" type="text" value="__TITLE__"
placeholder="Session name...">
<div class="session-actions" style="margin-top: 6px;">
<button class="session-action-btn save-btn" onclick="saveState()">SAVE</button>
</div>
<div style="margin-top: 8px;">
<span class="meta-label" style="font-size: 10px;">Load Session: <span style="color:#666;">(sorted by most recent)</span></span>
<input type="text" id="session-search" placeholder="Search sessions..."
style="width: 100%; margin-top: 4px; background: #1a1a1a; color: #ccc; border: 1px solid #444; border-radius: 4px; padding: 4px 6px; font-size: 11px; box-sizing: border-box;"
oninput="filterSessionPicker(this.value)">
<div id="session-picker-list" style="max-height: 180px; overflow-y: auto; margin-top: 4px; border: 1px solid #333; border-radius: 4px; background: #1a1a1a;"></div>
</div>
</div>
<!-- Section 2: Grid Display -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">GRID DISPLAY</div>
<div class="cog-menu-item">
<span class="meta-label">Go To Card #: </span>
<input type="text" inputmode="numeric" id="goto-input" min="1" placeholder="#"
title="Jump to image number (press Enter)" style="width: 45px;">
</div>
<div class="cog-menu-item">
<span class="meta-label">Number Of Columns In Grid: </span>
<input type="text" inputmode="numeric" id="col-count" min="0" placeholder="Auto"
title="Set grid columns (0 = Auto)">
</div>
</div>
<!-- Section 3: Label Mode -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">LABEL MODE</div>
<div style="font-size: 9px; color: #666; margin-bottom: 8px;">
Show metadata labels as overlays on card images. Unique-only filters out values shared by every card.
</div>
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 8px;">
<input type="checkbox" id="label-mode-toggle"
style="width: 14px; height: 14px; cursor: pointer;"
onchange="toggleLabelMode(this.checked)">
<label for="label-mode-toggle"
style="font-size: 11px; color: #ccc; cursor: pointer; user-select: none; font-weight: bold;">
Enable Label Overlays
</label>
</div>
<div id="label-fields-container" style="display: none;">
<div style="display: flex; align-items: center; gap: 6px; margin-bottom: 8px;">
<label for="label-size-slider" style="font-size: 10px; color: #999; white-space: nowrap;">Size:</label>
<input type="range" id="label-size-slider" min="6" max="16" value="9" step="1"
style="flex: 1; height: 14px; cursor: pointer;"
oninput="updateLabelSize(this.value)">
<span id="label-size-value" style="font-size: 10px; color: #999; min-width: 20px;">9px</span>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-model" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-model" style="font-size: 10px; color: #b43cc8; cursor: pointer; user-select: none;">Model</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-lora" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-lora" style="font-size: 10px; color: #c88c28; cursor: pointer; user-select: none;">LoRA</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-prompt" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-prompt" style="font-size: 10px; color: #28a078; cursor: pointer; user-select: none;">Prompt</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-sampler" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-sampler" style="font-size: 10px; color: #3c82c8; cursor: pointer; user-select: none;">Sampler</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-scheduler" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-scheduler" style="font-size: 10px; color: #3c82c8; cursor: pointer; user-select: none;">Scheduler</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-cfg" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-cfg" style="font-size: 10px; color: #c85050; cursor: pointer; user-select: none;">CFG</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-steps" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-steps" style="font-size: 10px; color: #c85050; cursor: pointer; user-select: none;">Steps</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-seed" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-seed" style="font-size: 10px; color: #888; cursor: pointer; user-select: none;">Seed</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-denoise" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-denoise" style="font-size: 10px; color: #00b4a0; cursor: pointer; user-select: none;">Denoise</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-metrics" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-metrics" style="font-size: 10px; color: #7ab0e0; cursor: pointer; user-select: none;">Metrics</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-wfParams" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-wfParams" style="font-size: 10px; color: #b48ae0; cursor: pointer; user-select: none;">Workflow params</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscale" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscale" style="font-size: 10px; color: #e06090; cursor: pointer; user-select: none;">Upscale (all)</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleMode" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleMode" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Up Mode</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleModel" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleModel" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Up Model</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleRatio" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleRatio" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Up Ratio</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleDenoise" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleDenoise" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Up Denoise</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleResizeMethod" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleResizeMethod" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Resize Method</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleHiresSteps" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleHiresSteps" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">HiRes Steps</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-upscaleTiling" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-upscaleTiling" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">Tiling Info</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-hiresPromptBehavior" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-hiresPromptBehavior" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">HiRes Prompt</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-hiresPromptText" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-hiresPromptText" style="font-size: 10px; color: #d06080; cursor: pointer; user-select: none;">HiRes Prompt Text</label>
</div>
<div style="display: flex; align-items: center; gap: 4px;">
<input type="checkbox" id="label-field-rating" onchange="updateLabelFields()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-field-rating" style="font-size: 10px; color: #e0c040; cursor: pointer; user-select: none;" title="Pairwise rating and vote count (voted cards only)">Rating</label>
</div>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="label-unique-only" checked
onchange="updateLabelFields()"
style="width: 12px; height: 12px; cursor: pointer;">
<label for="label-unique-only"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Hide values shared by every card (unique-only)
</label>
</div>
</div>
</div>
<!-- Section 3b: Image metrics for this session (background job) -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">IMAGE METRICS</div>
<button class="session-action-btn" id="compute-metrics-btn"
style="background: #2b5f9e; width: 100%; font-size: 11px; padding: 10px;"
onclick="startSessionMetrics()" title="Measure sharpness, entropy, colorfulness, contrast, luma, saturation and Sobel edges for every image that is missing any of them (CPU only)">
COMPUTE METRICS FOR THIS SESSION
</button>
<div id="compute-metrics-progress" style="display: none; margin-top: 6px; font-size: 10px; color: #9ab;"></div>
<div style="font-size: 9px; color: #666; margin-top: 6px;">
Fills in metrics missing from older items (for example Sobel on sessions measured before it existed). Never changes values already stored. Skips videos. Do not run it while this session is still generating.
</div>
</div>
<!-- Section 4: Export Favorites -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">EXPORT FAVORITES</div>
<button class="session-action-btn"
style="background: #6366f1; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 8px;"
onclick="exportFavoritesAsConfigJSON()" title="Generate a Config Builder JSON from favorited items — paste into the Builder to re-run with different settings">
📋 EXPORT FAVORITES AS CONFIG JSON
</button>
<div style="font-size: 9px; color: #666; margin-bottom: 10px;">
Copies a Config Builder JSON to clipboard with all favorited items' settings (models, LoRAs, prompts, samplers, etc.). Paste into the Config Builder's lora_config widget to re-run with different sizes/settings.
</div>
<button class="session-action-btn"
style="background: #005e52; color: #fff; width: 100%; font-size: 11px; padding: 10px;"
onclick="exportFavorites()" title="Export favorited images to benchmark_favorites folder">
COPY FAVORITES TO BENCHMARK FAVORITES FOLDER
</button>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" checked id="pack-metadata-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="pack-metadata-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Pack metadata into copied favorites (for CivitAI)
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="organize-by-prompt-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="organize-by-prompt-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Organize into separate folders by prompt (Prompt1, Prompt2, etc.)
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="organize-by-lora-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="organize-by-lora-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Organize into separate folders by lora sets (loras1, loras2, etc.)
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="export-prompt-txt-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="export-prompt-txt-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Export positive prompt as .txt file alongside each image
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" checked id="copy-manifest-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="copy-manifest-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Copy cleaned favorites-only manifest into favorites folder
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="pack-workflow-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="pack-workflow-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Pack full ComfyUI workflow into image metadata (drag into ComfyUI to load)
</label>
</div>
<div style="margin-top: 8px; display: flex; align-items: center; gap: 6px;">
<input type="checkbox" id="pack-nodes-workflow-checkbox"
style="width: 14px; height: 14px; cursor: pointer;">
<label for="pack-nodes-workflow-checkbox"
style="font-size: 10px; color: #999; cursor: pointer; user-select: none;">
Pack config data as pure nodes workflow into image metadata
</label>
</div>
<div id="export-status" style="margin-top: 8px; font-size: 10px; color: #666; min-height: 16px;"></div>
<button class="session-action-btn"
style="background: #cc2222; width: 100%; font-size: 11px; padding: 10px; margin-top: 12px; border: 2px solid #ff4444;"
onclick="deleteNonFavorites()" title="Permanently delete all non-favorited images and update manifest">
🗑️ DELETE ALL NON FAVORITED ITEMS
</button>
<button class="session-action-btn"
style="background: #7a2200; width: 100%; font-size: 11px; padding: 10px; margin-top: 6px; border: 2px solid #cc4400;"
onclick="deleteRejected()" title="Permanently delete all rejected images and update manifest">
🗑️ DELETE ALL REJECTED ITEMS
</button>
<div id="delete-status" style="margin-top: 4px; font-size: 10px; color: #666; min-height: 16px;"></div>
</div>
<!-- Pairwise review (blind A/B compare + scoreboard) -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">PAIRWISE REVIEW</div>
<div style="font-size: 9px; color: #666; margin-bottom: 8px;">
Blind two-up compare. Votes are saved in the session manifest and rolled up per setting.
</div>
<button class="session-action-btn"
style="background: #e0a020; color: #000; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="openCompareMode()" title="Start comparing pairs (C)">
START COMPARE (C)
</button>
<button class="session-action-btn"
style="background: #3e8ed0; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 8px;"
onclick="openScoreboard()" title="Win rates per setting from all votes">
SCOREBOARD
</button>
<div class="cmp-setting-row">
<label for="cmp-mode">Pairing</label>
<select id="cmp-mode" onchange="cmpSettingsChanged()">
<option value="matched">Matched (same prompt + seed)</option>
<option value="seed">Same settings, different seed</option>
<option value="one_axis">One axis at a time</option>
<option value="random">Random (same media type)</option>
</select>
</div>
<div class="cmp-setting-row">
<label for="cmp-axis">Axis focus</label>
<select id="cmp-axis" onchange="cmpSettingsChanged()">
<option value="">Any axis</option>
<option value="model">Model</option>
<option value="lora">LoRA</option>
<option value="sampler">Sampler</option>
<option value="scheduler">Scheduler</option>
<option value="cfg">CFG</option>
<option value="steps">Steps</option>
<option value="denoise">Denoise</option>
<option value="resolution">Resolution</option>
<option value="prompt">Prompt</option>
<option value="upscale">Upscale</option>
</select>
</div>
<div class="cmp-setting-row">
<label for="cmp-reveal">Reveal after vote</label>
<select id="cmp-reveal" onchange="cmpSettingsChanged()">
<option value="0">Off</option>
<option value="1000" selected>1 s</option>
<option value="2000">2 s</option>
</select>
</div>
<div class="cmp-setting-row">
<input type="checkbox" id="cmp-blind" checked onchange="cmpSettingsChanged()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="cmp-blind" style="cursor: pointer;">Blind (hide settings until you vote)</label>
</div>
<div class="cmp-setting-row">
<input type="checkbox" id="cmp-restrict" checked onchange="cmpSettingsChanged()" style="width: 12px; height: 12px; cursor: pointer;">
<label for="cmp-restrict" style="cursor: pointer;">Scoreboard: only votes inside the current filter</label>
</div>
<button class="session-action-btn"
style="background: #cc2222; width: 100%; font-size: 11px; padding: 8px; margin-top: 8px; border: 2px solid #ff4444;"
onclick="clearComparisons()" title="Delete every vote saved in this session">
CLEAR VOTES
</button>
<div id="cmp-status" style="margin-top: 6px; font-size: 10px; color: #666; min-height: 14px;"></div>
</div>
<!-- Section 5: Manifest Analytics -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">MANIFEST ANALYTICS</div>
<div style="font-size: 9px; color: #666; margin-bottom: 8px;">
Per-setting favorites rate, time and metrics (live), or usage counts of favorited items.
</div>
<button class="session-action-btn"
style="background: #3e8ed0; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="openAnalyticsPanel()" title="Favorites rate, rejects, time and metrics per setting value (live panel)">
SETTINGS STATS (PER AXIS)
</button>
<button class="session-action-btn"
style="background: #8b5cf6; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="runManifestAnalysis('lora')" title="Count LoRA usage across favorited items">
LORA USAGE STATS
</button>
<button class="session-action-btn"
style="background: #6366f1; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="runManifestAnalysis('model')" title="Count model/checkpoint usage across favorited items">
MODEL USAGE STATS
</button>
<button class="session-action-btn"
style="background: #005e52; color: #fff; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="runManifestAnalysis('prompt')" title="Count full prompts with favorited vs total">
FULL PROMPT STATS
</button>
<button class="session-action-btn"
style="background: #d0873e; width: 100%; font-size: 11px; padding: 10px; margin-bottom: 6px;"
onclick="runManifestAnalysis('tags')" title="Count individual prompt tags from favorited items">
PROMPT TAG STATS
</button>
<div id="analytics-status" style="margin-top: 4px; font-size: 10px; color: #666; min-height: 16px;"></div>
</div>
<!-- Section 6: Scan External Directory -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">SCAN EXTERNAL DIRECTORY</div>
<div style="font-size: 9px; color: #666; margin-bottom: 8px;">
Load any folder of images into the dashboard. Reads PNG metadata for filtering/sorting.
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<input type="text" id="scan-directory-input"
placeholder="Paste absolute directory path..."
style="flex: 1; background: #000; border: 1px solid #333; color: #fff;
padding: 8px; font-size: 11px; font-family: monospace; border-radius: 3px;"
onkeydown="if(event.key==='Enter')scanDirectory()">
<button class="session-action-btn" id="scan-directory-btn"
style="background: #6366f1; white-space: nowrap; padding: 8px 16px; font-size: 11px; font-weight: bold;"
onclick="scanDirectory()">
SCAN
</button>
</div>
<div id="scan-status" style="margin-top: 6px; font-size: 10px; color: #666; min-height: 16px;"></div>
</div>
<!-- Section 7: Keyboard Shortcuts Reference -->
<div class="cog-panel-section">
<div class="cog-panel-section-title">KEYBOARD SHORTCUTS</div>
<table style="width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 11px;">
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Space</td>
<td style="padding: 4px 8px; color: #ccc;">Scroll down one row</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Shift+Space</td>
<td style="padding: 4px 8px; color: #ccc;">Scroll up one row</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Arrow Keys</td>
<td style="padding: 4px 8px; color: #ccc;">Pan in any direction</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">+ / -</td>
<td style="padding: 4px 8px; color: #ccc;">Zoom in / out</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">0</td>
<td style="padding: 4px 8px; color: #ccc;">Auto-fit first row to viewport</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">F</td>
<td style="padding: 4px 8px; color: #ccc;">Toggle fullscreen</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">R</td>
<td style="padding: 4px 8px; color: #ccc;">Reload session</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">C</td>
<td style="padding: 4px 8px; color: #ccc;">Compare mode (blind A/B review)</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">A/D, S, W</td>
<td style="padding: 4px 8px; color: #ccc;">In compare: A wins / B wins, tie, skip (arrows work too)</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Z, I, B</td>
<td style="padding: 4px 8px; color: #ccc;">In compare: undo, unblind, scoreboard</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">1-9</td>
<td style="padding: 4px 8px; color: #ccc;">Quick favorite Nth visible card</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Shift+0-9</td>
<td style="padding: 4px 8px; color: #ccc;">Set column count (0 = auto)</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Ctrl+S</td>
<td style="padding: 4px 8px; color: #ccc;">Save (forwarded to ComfyUI)</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Escape</td>
<td style="padding: 4px 8px; color: #ccc;">Close modal / lightbox</td>
</tr>
<tr style="border-bottom: 1px solid #333;">
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Double-click</td>
<td style="padding: 4px 8px; color: #ccc;">Toggle favorite on card</td>
</tr>
<tr>
<td style="padding: 4px 8px; color: #0af; font-family: monospace;">Shift+Click</td>
<td style="padding: 4px 8px; color: #ccc;">Isolate a single filter</td>
</tr>
</table>
</div>
</div>
</div>
<!-- Overlay for settings panel -->
<div id="cog-menu-overlay" class="filters-overlay" onclick="toggleCogMenu()" style="display: none;"></div>
<!-- REAL-TIME ETA BAR -->
<div id="eta-bar" style="display: none; align-items: center; gap: 10px; padding: 4px 12px; background: #1a1a2e; border-bottom: 1px solid #333; font-size: 11px; color: #aaa; position: relative;">
<span style="color: #0af; font-weight: bold;">ETA</span>
<span id="eta-text" style="flex: 1;">--</span>
<div style="position: absolute; bottom: 0; left: 0; height: 2px; width: 100%; background: #222;">
<div id="eta-progress" style="height: 100%; width: 0%; background: linear-gradient(90deg, #0af, #0f6); transition: width 0.5s ease;"></div>
</div>
</div>
<!-- PREVIEW PASS BAR (shown only when the session holds preview images) -->
<div id="preview-bar" style="display: none; align-items: center; gap: 10px; padding: 4px 12px; background: #2a2418; border-bottom: 1px solid #443a22; font-size: 11px; color: #cdb26a;">
<span style="font-weight: bold;">PREVIEW PASS</span>
<span id="preview-bar-text" style="flex: 1;">--</span>
<button id="full-pass-btn" onclick="runFullPassForSurvivors()"
title="Render the survivors at full quality: turns Preview pass off in the Config Builder and queues the workflow (on the standalone page it queues the run through the run API)"
style="background: #3a2f12; border: 1px solid #8a7330; color: #ffd76a; padding: 3px 10px; border-radius: 4px; cursor: pointer; font-size: 11px;">&#9654; Full pass for survivors</button>
</div>
<div id="filters-popup" class="filters-popup" style="display: none;">
<div class="filters-popup-header">
<h3>Filters & Info</h3>
<button onclick="resetAllFilters()" title="Reset all filters to show everything"
style="background: #c45000; border: 1px solid #ff7733; color: #fff; font-size: 10px; font-weight: 700; padding: 4px 10px; border-radius: 4px; cursor: pointer; letter-spacing: 0.5px;">
RESET ALL
</button>
<button class="close-popup-btn" onclick="toggleFiltersPopup()">✕</button>
</div>
<div class="filters-popup-content">
<!-- TOP-LEVEL FILTERS SECTION -->
<div class="popup-section"
style="border-bottom: 2px solid #333; padding-bottom: 16px; margin-bottom: 16px;">
<div class="popup-section-title">VISIBILITY</div>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button id="top-filter-favorites" class="filter-btn top-filter-btn active"
onclick="toggleFavoritesFilter()" title="Toggle visibility of favorited images">
⭐ Favorites
</button>
<button id="top-filter-nonfavorited" class="filter-btn top-filter-btn active"
onclick="toggleNonFavoritedFilter()" title="Toggle visibility of non-favorited images">
◯ Non-Favorited
</button>
<button id="top-filter-rejected" class="filter-btn top-filter-btn" onclick="toggleRejectedFilter()"
title="Toggle visibility of rejected images">
✕ Rejected
</button>
</div>
</div>
<!-- SEARCH FILTER SECTION -->
<div class="popup-section">
<div class="popup-section-title">SEARCH FILTER</div>
<div class="search-filter-container">
<div class="search-filter-row">
<select id="search-filter-type" class="search-filter-select">
<option value="">Select filter type...</option>
<option value="model">Model</option>
<option value="positive">Positive Prompt</option>
<option value="negative">Negative Prompt</option>
<option value="sampler">Sampler</option>
<option value="scheduler">Scheduler</option>
<option value="lora">LoRA</option>
</select>
<input type="text" id="search-filter-input" class="search-filter-input"
placeholder="Enter search term..." onkeypress="if(event.key==='Enter') addSearchFilter()">
<button class="search-filter-add-btn" onclick="addSearchFilter()">ADD</button>
</div>
<div id="active-search-filters" class="active-search-filters"></div>
</div>
</div>
<!-- LOGIC FILTER SECTION -->
<div class="popup-section">
<div class="popup-section-title">LOGIC FILTER <span style="font-size: 9px; color: #555; font-weight: normal;">(numeric comparisons, AND logic)</span></div>
<div class="search-filter-container">
<div class="search-filter-row">
<select id="logic-filter-field" class="search-filter-select">
<option value="">Field...</option>
<option value="lora_strength">LoRA Strength</option>
<option value="cfg">CFG</option>
<option value="steps">Steps</option>
<option value="denoise">Denoise</option>
<option value="width">Width</option>
<option value="height">Height</option>
<option value="seed">Seed</option>
</select>
<select id="logic-filter-op" class="search-filter-select" style="min-width: 52px; flex: 0 0 52px;">
<option value=">">&gt;</option>
<option value="<">&lt;</option>
<option value=">=">&gt;=</option>
<option value="<=">&lt;=</option>
<option value="=">=</option>
<option value="!=">!=</option>
</select>
<input type="number" id="logic-filter-value" class="search-filter-input" step="0.01"
placeholder="Value..." onkeypress="if(event.key==='Enter') addLogicFilter()" style="min-width: 70px;">
<button class="search-filter-add-btn" onclick="addLogicFilter()">ADD</button>
</div>
<div class="search-filter-row" id="metric-range-row" style="display: none; margin-top: 4px;">
<span style="font-size: 10px; color: #888; white-space: nowrap;">Metric range:</span>
<select id="metric-range-field" class="search-filter-select" onchange="updateMetricRangeHints()"></select>
<input type="number" id="metric-range-min" class="search-filter-input" step="any" placeholder="min" style="min-width: 60px;" onkeypress="if(event.key==='Enter') addMetricRange()">
<input type="number" id="metric-range-max" class="search-filter-input" step="any" placeholder="max" style="min-width: 60px;" onkeypress="if(event.key==='Enter') addMetricRange()">
<button class="search-filter-add-btn" onclick="addMetricRange()">ADD</button>
</div>
<div id="active-logic-filters" class="active-search-filters"></div>
</div>
</div>
<!-- QUICK FILTER SECTION -->
<div class="popup-section" id="quick-filter-section">
<div class="popup-section-title">QUICK FILTER <span style="font-size: 9px; color: #555; font-weight: normal;">(faceted: pick type → pick value → drill down)</span></div>
<div class="search-filter-container">
<div class="search-filter-row">
<div id="quick-filter-anchor" style="position:relative; display:inline-block;">
<button class="search-filter-add-btn" id="quick-filter-add-btn"
onclick="_openQuickFilterTypeMenu(this)">
+ Add Quick Filter ▾
</button>
<div id="quick-filter-type-menu" style="display:none; position:absolute; z-index:1000; background:#1a1a1a; border:1px solid #333; border-radius:4px; min-width:150px; box-shadow:0 4px 12px rgba(0,0,0,0.5); top:100%; left:0; margin-top:2px;"></div>
<div id="quick-filter-value-menu" style="display:none; position:absolute; z-index:1001; background:#1a1a1a; border:1px solid #333; border-radius:4px; min-width:180px; max-height:220px; overflow-y:auto; box-shadow:0 4px 12px rgba(0,0,0,0.5); top:100%; left:0; margin-top:2px;"></div>
</div>
</div>
<div id="quick-filter-chips" style="margin-top:6px;"></div>
</div>
</div>
<!-- Meta Info Section -->
<!-- Filters Section -->
<div class="popup-section">
<div class="popup-section-title">FILTERS <span
style="font-size: 9px; color: #555; font-weight: normal;">(Shift+Click to isolate)</span></div>
<div style="margin: 6px 0 2px 0; padding: 4px 0;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 11px; color: #aaa;">
<input type="checkbox" id="toggle-triggers" checked
style="cursor: pointer;"
onchange="if(typeof toggleTriggerWords==='function') toggleTriggerWords(this.checked)">
Show Lora Trigger Words in Prompts
</label>
</div>
<br />
<div class="filter-row">
<span class="filter-label">Model</span>
<div id="filter-model" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Positive Prompt</span>
<div id="filter-positive" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Negative Prompt</span>
<div id="filter-negative" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Sampler</span>
<div id="filter-sampler" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Scheduler</span>
<div id="filter-scheduler" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Denoise</span>
<div id="filter-denoise" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">LoRA</span>
<div id="filter-lora" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Size</span>
<div id="filter-size" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Seed</span>
<div id="filter-seed" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Steps</span>
<div id="filter-steps" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">CFG</span>
<div id="filter-cfg" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Upscale</span>
<div id="filter-upscaleMethod" class="filter-buttons-container"></div>
</div>
<div class="filter-row">
<span class="filter-label">Type</span>
<div id="filter-mediaType" class="filter-buttons-container"></div>
</div>
<div class="filter-row" style="display: none;">
<span class="filter-label">Pass</span>
<div id="filter-passType" class="filter-buttons-container"></div>
</div>
</div>
</div>
</div>
<!-- Overlay for popup -->
<div id="filters-overlay" class="filters-overlay" onclick="toggleFiltersPopup()" style="display: none;"></div>
<!-- Session landing page (shown when no session is loaded) -->
<div id="session-landing" style="display: none; padding: 40px 20px; text-align: center; height: 100%; overflow-y: auto; box-sizing: border-box;">
<h2 style="color: #888; font-size: 18px; margin-bottom: 8px;">Ultimate Grid Dashboard</h2>
<p style="color: #555; font-size: 12px; margin-bottom: 24px;">Select a session to view</p>
<div id="session-cards" style="display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; max-width: 900px; margin: 0 auto; padding-bottom: 40px;"></div>
</div>
<div id="viewport">
<div id="canvas">
<div class="grid" id="grid"></div>
</div>
</div>
<!-- MOBILE NAVIGATION BUTTONS -->
<button id="mobile-nav-up" class="mobile-nav-btn mobile-nav-up" onclick="scrollUpOneRow()" title="Scroll Up">
▲
</button>
<button id="mobile-nav-down" class="mobile-nav-btn mobile-nav-down" onclick="scrollDownOneRow()"
title="Scroll Down">
▼
</button>
<div class="json-container">
<!-- Button row -->
<div style="display: flex; gap: 0;">
<button id="json-btn-good" class="json-view-btn" onclick="viewGoodJSON()"
title="Click to view accepted configs JSON">
View Configs (0)
</button>
<button id="json-btn-favorite" class="json-view-btn" onclick="viewFavoritedJSON()"
title="Click to view favorited configs JSON">
View Favorited (0)
</button>
<button id="json-btn-bad" class="json-view-btn" onclick="viewRejectedJSON()"
title="Click to view rejected configs JSON">
View Rejected (0)
</button>
</div>
<!-- Hidden JSON display areas (shown when button is clicked) -->
<div id="json-bar-good" class="json-bar" style="display: none;" title="ACCEPTED CONFIGS (Click to select)"
onclick="selectJSON('json-bar-good')">[]</div>
<div id="json-bar-favorite" class="json-bar" style="display: none;" title="FAVORITED CONFIGS (Click to select)"
onclick="selectJSON('json-bar-favorite')">[]</div>
<div id="json-bar-bad" class="json-bar" style="display: none;" title="REJECTED CONFIGS (Click to select)"
onclick="selectJSON('json-bar-bad')">[]</div>
</div>
<!-- Pairwise review overlay (see logic_compare.js) -->
<div id="compare-overlay" style="display: none;">
<div id="cmp-head">
<span id="cmp-mode-label"></span>
<span id="cmp-progress"></span>
<span id="cmp-repeat-badge" style="display: none;">repeating voted pairs</span>
<span class="cmp-spacer"></span>
<button class="cmp-head-btn" id="cmp-unblind-btn" onclick="cmpToggleUnblind()" title="Show what differs before you vote (I)">Unblind (I)</button>
<button class="cmp-head-btn" onclick="openScoreboard()" title="Scoreboard (B)">Scoreboard (B)</button>
<button class="cmp-head-btn" onclick="cmpUndo()" title="Undo last vote (Z)">Undo (Z)</button>
<button class="cmp-head-btn cmp-close" onclick="closeCompareMode()" title="Close (Esc)">&#10005;</button>
</div>
<div id="cmp-panes">
<div class="cmp-pane" id="cmp-pane-a" onclick="cmpPaneTap('a')">
<div class="cmp-label">A</div>
<div class="cmp-media" id="cmp-media-a"></div>
<div class="cmp-chips" id="cmp-chips-a"></div>
</div>
<div class="cmp-pane" id="cmp-pane-b" onclick="cmpPaneTap('b')">
<div class="cmp-label">B</div>
<div class="cmp-media" id="cmp-media-b"></div>
<div class="cmp-chips" id="cmp-chips-b"></div>
</div>
</div>
<div id="cmp-empty" style="display: none;"></div>
<div id="cmp-bar">
<button class="cmp-btn" onclick="cmpVoteSide('a')">&#9664; A wins</button>
<button class="cmp-btn" onclick="cmpVoteSide('tie')">Tie</button>
<button class="cmp-btn" onclick="cmpSkip()">Skip</button>
<button class="cmp-btn" onclick="cmpVoteSide('b')">B wins &#9654;</button>
<button class="cmp-btn cmp-btn-undo" onclick="cmpUndo()">&#8630; Undo</button>
</div>
</div>
<div id="modal">
<button class="modal-close-x" onclick="closeM()" title="Close (Esc)">&times;</button>
<!-- Lightbox navigation bar -->
<div style="display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: rgba(0,0,0,0.6); border-bottom: 1px solid #333; flex-shrink: 0;">
<button class="modal-nav-btn" onclick="(function(){ var idx = processedData.findIndex(function(x){return x.id===window.currentModalId;}); if(idx>0) openM(processedData[idx-1].id); })()" title="Previous image (← or Shift+Space)"
style="background: #222; border: 1px solid #444; color: #ccc; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">◀</button>
<span id="modal-nav-counter" style="font-size: 11px; color: #777; min-width: 60px; text-align: center;"></span>
<button class="modal-nav-btn" onclick="(function(){ var idx = processedData.findIndex(function(x){return x.id===window.currentModalId;}); if(idx < processedData.length-1) openM(processedData[idx+1].id); })()" title="Next image (→ or Space)"
style="background: #222; border: 1px solid #444; color: #ccc; padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 14px;">▶</button>
<span class="modal-kbd-hint" style="font-size: 9px; color: #444; margin-left: 8px;">← → Space navigate &nbsp;|&nbsp; 1-9 favorite</span>
</div>
<div class="modal-main">
<div class="preview"><img id="m-img" src="" draggable="true" title="Drag onto a Load Image node or the canvas"><div id="m-preview-note" style="display: none; font-size: 11px; color: #cdb26a; padding: 4px 8px; text-align: center;"></div></div>
<div class="controls">
<div id="m-revise-controls" style="display:flex; flex-direction:column; gap:10px; flex:1; min-height:0;">
<h2 style="margin:0 0 10px 0; color:#fff;">Revise & Generate</h2>
<button class="modal-action-btn copy-comfy-btn" onclick="copyConfigsAsComfyNodes(window.currentModalId)"
style="background: #6366f1; margin-top: 10px;">
📋 Copy Configs To Clipboard As Comfy Nodes
</button>
<button id="modal-send-loadimage-btn" class="modal-action-btn" onclick="sendModalToLoadImage()"
title="Put this image into a Load Image node (the selected one, the only one, or a new one)"
style="background: #2b6cb0; margin-top: 6px;">
🖼 Send to Load Image
</button>
<div class="field"><label>Model</label><input id="f-model"></div>
<div class="field"><label>Seed</label><input id="f-seed" type="number">
</div>
<div class="field"><label>Positive Prompt</label><textarea id="f-pos" rows="3"
style="background:#000; border:1px solid #333; color:#fff; padding:8px; font-size:12px; font-family:inherit; resize:vertical;"></textarea>
</div>
<div class="field"><label>Negative Prompt</label><textarea id="f-neg" rows="2"
style="background:#000; border:1px solid #333; color:#fff; padding:8px; font-size:12px; font-family:inherit; resize:vertical;"></textarea>
</div>
<div style="border-top:1px solid #333; margin:10px 0; padding-top:10px;">
<div style="font-size:11px; color:#666; margin-bottom:8px;">ADJUST PARAMETERS:</div>
</div>
<div class="field"><label>Sampler</label><input id="f-smp"></div>
<div class="field"><label>Scheduler</label><input id="f-sch"></div>
<div class="field"><label>Steps</label><input id="f-stp" type="number"></div>
<div class="field"><label>CFG</label><input id="f-cfg" type="number" step="0.5"></div>
<div class="field"><label>Denoise</label><input id="f-den" type="number" step="0.05"></div>
<div class="field"><label>LoRA</label><input id="f-lor"></div>
<div style="margin-top:auto; display:flex; gap:10px;">
<button class="action-btn"
style="flex:2; background:var(--accent); color:#000; border:none; height:36px; font-weight:800"
onclick="triggerGen(this)">GENERATE NEW</button>
<button class="action-btn" style="flex:1" onclick="closeM()">EXIT</button>
</div>
</div>
<div id="m-workflow-panel" style="display:none; flex-direction:column; gap:10px; flex:1; min-height:0;">
<h2 style="margin:0 0 6px 0; color:#fff;">Workflow job</h2>
<div id="m-wf-meta" style="font-size:11px; color:#888; word-break:break-all;"></div>
<table id="m-wf-params" style="width:100%; border-collapse:collapse; font-size:12px; color:#ccc;"></table>
<div style="margin-top:auto; display:flex; flex-direction:column; gap:8px;">
<button id="m-wf-download" class="modal-action-btn" style="background:#6366f1;" onclick="downloadWorkflowJobPrompt()">⬇ Download job prompt (API JSON)</button>
<button id="m-wf-copy" class="modal-action-btn" style="background:#333;" onclick="copyWorkflowJobPrompt()">📋 Copy job prompt</button>
<button class="action-btn" onclick="closeM()">EXIT</button>
</div>
</div>
</div>
</div>
<div class="reel-wrap">
<div style="font-size:10px; font-weight:bold; color:#666;">RELATED VARIANTS</div>
<div class="reel" id="reel"></div>
</div>
<!-- Phone action bar (hidden by CSS unless coarse pointer / standalone on a narrow screen) -->
<div id="modal-action-bar">
<button onclick="modalNav(-1)" title="Previous"><span class="mab-ico">◀</span><span class="mab-lbl">Prev</span></button>
<button id="modal-fav-btn" onclick="toggleModalFavorite()" title="Favorite"><span class="mab-ico">☆</span><span class="mab-lbl">Favorite</span></button>
<button onclick="rejectModalItem()" title="Reject"><span class="mab-ico">✕</span><span class="mab-lbl">Reject</span></button>
<button id="modal-compare-btn" onclick="openCompareMode()" title="Compare pairs"><span class="mab-ico">&#8644;</span><span class="mab-lbl">Compare</span></button>
<button id="modal-revise-btn" onclick="var c=document.querySelector('#modal .controls'); if(c) c.scrollIntoView({behavior:'smooth'});" title="Revise parameters"><span class="mab-ico">✎</span><span class="mab-lbl">Revise</span></button>
<button onclick="modalNav(1)" title="Next"><span class="mab-ico">▶</span><span class="mab-lbl">Next</span></button>
</div>
</div>
<script>
// INJECTED DATA
const TARGET_NODE_ID = "__NODE_ID__";
// true when served by /config_tester/dashboard, or whenever this page is not inside a frame
const USCG_STANDALONE = __STANDALONE_FLAG__ || (window.parent === window);
let fullManifest = __JSON_DATA__;
// INJECTED LOGIC
__JS_CONTENT__
</script>
<style>
/* Default behavior: The button is visible */
.gh-hide-on-mobile {
display: block;
}
/* CONDITION 1: Hide if screen is narrower than 768px (Mobile/Tablets) */
@media (max-width: 768px) {
.gh-hide-on-mobile {
display: none !important;
}
}
/* CONDITION 2: Hide if screen is in Portrait Mode (Taller than it is wide) */
@media (orientation: portrait) {
.gh-hide-on-mobile {
display: none !important;
}
}
</style>
<div class="gh-hide-on-mobile"
style="position: absolute; top: 2px; left: 50%; transform: translateX(-50%); z-index: 999;">
<a href="https://github.com/JasonHoku/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite" target="_blank"
style="
display: inline-flex;
align-items: center;
gap: 6px;
background-color: #313131;
border: 1px solid rgba(27, 31, 35, 0.15);
border-radius: 6px;
color: #e6e6e6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
font-size: 14px;
font-weight: 600;
line-height: 1;
padding: 3px 10px;
line-height: 20px;
text-decoration: none;
white-space: nowrap;
" onmouseover="this.style.backgroundColor='#747474'" onmouseout="this.style.backgroundColor='#313131'">
<svg height="20" viewBox="0 0 16 16" width="20" style="fill: currentColor; opacity: 0.7;">
<path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z">
</path>
</svg>
<span>Star</span>
<span id="gh-star-count" style="
background-color: rgba(27, 31, 35, 0.08);
padding: 2px 6px;
border-radius: 10px;
font-size: 12px;
display: none; /* Hidden until loaded */
"></span>
</a>
</div>
<script>
(async function () {
try {
const repo = 'JasonHoku/ComfyUI-Ultimate-Auto-Sampler-Config-Grid-Testing-Suite';
const response = await fetch(`https://api.github.com/repos/${repo}`);
if (response.ok) {
const data = await response.json();
const countSpan = document.getElementById('gh-star-count');
// Format number (e.g. 1500 -> 1.5k) if needed, or just show raw
countSpan.textContent = data.stargazers_count;
countSpan.style.display = 'inline-block'; // Show it now that we have data
}
} catch (e) {
console.log('Could not fetch GitHub stars');
}
})();
</script>
<!-- Upscale Modal -->
<div id="upscale-modal-overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000; align-items: center; justify-content: center;" onclick="if(event.target===this)closeUpscaleModal()">
<div id="upscale-modal-content" style="background: #1e1e1e; border: 1px solid #444; border-radius: 8px; padding: 20px; width: 600px; max-width: 92vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 8px 32px rgba(0,0,0,0.5);"></div>
</div>
</body>
</html>