Files
ComfyAssets-ComfyUI_PromptM…/web/gallery.html
T
Vito aa768f57dd fix: release 3.2.3 — metadata parsing, null-safe saves, auto-hide, tag search (#151)
* fix: release 3.2.3 — metadata parsing, null-safe saves, auto-hide, tag search

- fix(#75): stop SaveImage patch from writing the last-run prompt into every
  PromptManager node (negative prompts were saved as copies of the positive)
- fix(#75): replace four duplicated metadata parsers with a shared
  web/js/comfy-metadata.js that traces sampler links (KSampler, CFGGuider /
  SamplerCustomAdvanced, LoRA chains, ControlNet pass-through, any checkpoint
  loader) and recovers per-node text for images saved by older versions
- fix(#143): treat null JSON body fields as empty in prompt API handlers
- fix(#149): keep auto-hide timeout 0 (disabled) instead of falling back to
  the default; validate result_timeout on save
- feat(#150): support tag:name filters in the node search box
- docs(#142): scan dialog mentions A1111/Forge metadata support
- docs(#144): fix ComfyUI-Lora-Manager links
- ci: run JS tests with node --test

* fix: bound metadata graph traversal to prevent tab freeze on crafted PNGs

Link resolution tried every linked input recursively with only a depth cap,
so a PNG whose prompt graph has cycles and fan-out could force ~inputs^25
paths and hang the gallery/metadata page. Each lookup now shares a visited
set (each node expanded once) with a hard cap of 500 visits.

* fix: read the matching output of multi-output settings nodes

KSampler settings linked to a node like SamplerCombo all resolved to its
first value, so steps, cfg and sampler showed the sampler name. Scalar
resolution now prefers the linked node's input with the same field name.

* fix: address review — trust API text, restore workflow-only parsing

- Workflow widget text now only replaces API text for metadata showing the
  pre-3.2.3 corruption signature (2+ PromptManager nodes with identical API
  text but different widgets). Otherwise the API graph wins, so expanded
  dynamic prompts and linked text display what actually ran.
- Images with only a UI workflow are converted to the API graph shape and
  traced the same way: prompts, KSamplerAdvanced, UNET/checkpoint loaders,
  KJNodes Set/Get, Reroute, bypassed and muted nodes.
2026-10-07 11:27:40 -07:00

809 lines
51 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PromptManager - Gallery</title>
<script src="/prompt_manager/js/theme-bridge.js"></script>
<link rel="stylesheet" href="lib/tailwind/comfyui-theme.css">
<link rel="stylesheet" href="lib/tailwind/styles.css">
<link rel="stylesheet" href="lib/viewerjs/viewer.min.css">
<script src="lib/viewerjs/viewer.min.js"></script>
</head>
<body class="bg-pm-primary text-pm min-h-screen">
<div class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-pm-surface border-b border-pm px-4 py-2">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between">
<h1 class="text-sm font-semibold text-pm">PromptManager Gallery</h1>
<div class="hidden sm:flex items-center gap-2">
<div class="w-2 h-2 bg-pm-success rounded-full animate-pulse"></div>
<span class="text-pm-success text-xs font-medium">Online</span>
</div>
</div>
</div>
</header>
<!-- Controls -->
<div class="bg-pm-surface border-b border-pm-subtle px-4 py-2">
<div class="max-w-7xl mx-auto">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex items-center gap-2">
<label class="text-xs text-pm-secondary">Show:</label>
<select id="limitSelector" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="text-xs text-pm-secondary">images</span>
<span class="text-pm-secondary mx-1">|</span>
<label class="text-xs text-pm-secondary">Size:</label>
<button id="thumbSmBtn" class="px-2 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs rounded-pm-sm transition-colors" title="Small thumbnails">S</button>
<button id="thumbMdBtn" class="px-2 py-1 bg-pm-accent text-pm-accent-fg text-xs rounded-pm-sm transition-colors" title="Medium thumbnails">M</button>
<button id="thumbLgBtn" class="px-2 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs rounded-pm-sm transition-colors" title="Large thumbnails">L</button>
</div>
<div class="flex items-center gap-2">
<button id="refreshBtn"
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Refresh
</button>
<button id="gridViewBtn"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Grid View
</button>
<button id="listViewBtn"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
List View
</button>
<button id="settingsBtn"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Settings
</button>
<button id="autoTagBtn"
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Auto Tag
</button>
<button id="addPromptBtn"
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
Add Prompt
</button>
</div>
</div>
</div>
</div>
<!-- Stats Bar -->
<div class="bg-pm-surface px-4 py-2 border-b border-pm-subtle">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between text-xs">
<span class="text-pm-secondary">
Showing <span id="showingStart">0</span> to <span id="showingEnd">0</span> of <span id="totalImages">0</span> images
</span>
<span class="text-pm-secondary" id="loadingStatus">Loading...</span>
</div>
</div>
</div>
<!-- Gallery Content -->
<div class="flex-1 overflow-hidden">
<div class="max-w-full mx-auto p-3 flex gap-0" style="height: calc(100vh - 140px);">
<!-- Folder Tree Panel — sticky, scrolls independently -->
<div id="folderTreePanel" class="hidden flex-shrink-0 bg-pm-surface border border-pm rounded-pm-sm overflow-y-auto" style="width: 240px; min-width: 160px;">
<div class="px-3 py-2 border-b border-pm-subtle flex items-center justify-between sticky top-0 bg-pm-surface z-10">
<span class="text-sm font-semibold text-pm">Folders</span>
<button id="clearFolderFilterBtn" class="text-sm text-pm-secondary hover:text-pm transition-colors" title="Clear filter">&times;</button>
</div>
<div id="folderTree" class="py-1 text-sm">
<!-- Tree nodes inserted by JS -->
</div>
</div>
<!-- Resize Handle -->
<div id="folderResizeHandle" class="hidden w-1 cursor-col-resize bg-transparent hover:bg-pm-accent/30 flex-shrink-0"></div>
<!-- Main Gallery Area — scrolls independently -->
<div class="flex-1 min-w-0 overflow-y-auto pl-3">
<!-- Loading State -->
<div id="loadingState" class="flex items-center justify-center py-12">
<div class="text-center">
<div class="w-8 h-8 border-4 border-pm-accent-tint border-t-pm-accent rounded-full animate-spin mx-auto mb-3"></div>
<p class="text-pm-secondary text-xs">Loading gallery...</p>
</div>
</div>
<!-- Error State -->
<div id="errorState" class="hidden flex items-center justify-center py-12">
<div class="text-center">
<h3 class="text-sm font-semibold text-pm-error mb-2">Error Loading Gallery</h3>
<p class="text-pm-secondary text-xs mb-3" id="errorMessage">Something went wrong while loading the images.</p>
<button id="retryBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Try Again
</button>
</div>
</div>
<!-- Gallery Grid -->
<div id="galleryGrid" class="hidden grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8 gap-2">
<!-- Images will be inserted here -->
</div>
<!-- Gallery List -->
<div id="galleryList" class="hidden space-y-2">
<!-- Images will be inserted here -->
</div>
<!-- Pagination -->
<div id="paginationControls" class="hidden mt-4 flex items-center justify-center gap-2">
<button id="prevPageBtn" class="px-3 py-1 bg-pm-surface hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
Previous
</button>
<div class="flex items-center gap-2">
<span class="text-pm-secondary text-xs">Page</span>
<input id="pageInput" type="number" min="1" class="w-16 px-2 py-1 bg-pm-input text-pm text-xs text-center rounded-pm-sm border border-pm focus:border-pm-accent focus:outline-none" value="1">
<span id="pageInfo" class="text-pm-secondary text-xs">of 1</span>
</div>
<button id="nextPageBtn" class="px-3 py-1 bg-pm-surface hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
Next
</button>
</div>
</div> <!-- /Main Gallery Area -->
</div>
</div>
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md max-w-2xl w-full max-h-[90vh] overflow-hidden mx-4 border border-pm">
<div class="p-3 border-b border-pm">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-pm">Gallery Settings</h3>
<button id="closeSettingsBtn" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div class="p-3 overflow-y-auto max-h-[calc(90vh-200px)] space-y-4">
<!-- Performance Settings -->
<div class="space-y-3">
<h4 class="text-sm font-semibold text-pm">Performance Settings</h4>
<!-- Thumbnail Generation -->
<div class="bg-pm-input rounded-pm-sm p-3">
<div class="flex items-center justify-between mb-2">
<div>
<h5 class="text-pm text-xs font-medium">Generate Thumbnails</h5>
<p class="text-xs text-pm-secondary">Create optimized thumbnails for faster loading</p>
</div>
<button id="generateThumbnailsBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs rounded-pm-sm transition-colors">
Generate
</button>
</div>
<div class="text-xs text-pm-muted mb-2">
Status: <span id="thumbnailStatus">Not generated</span>
<div class="text-xs text-pm-muted mt-1">
Thumbnails speed up gallery loading. Original images always used for viewing and metadata.
</div>
</div>
<!-- Check thumbnails at startup -->
<div class="flex items-center justify-between py-2 border-t border-pm">
<div>
<label class="text-xs text-pm-secondary">Check thumbnails at startup</label>
<p class="text-xs text-pm-muted">Prompt to generate thumbnails if missing when opening gallery</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" id="checkThumbnailsAtStartup" class="sr-only peer" checked>
<div class="w-11 h-6 bg-pm-input peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-pm-accent rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-pm after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-pm-accent"></div>
</label>
</div>
<!-- Progress bar (hidden by default) -->
<div id="thumbnailProgress" class="hidden">
<div class="flex items-center justify-between text-xs text-pm-secondary mb-1">
<span id="progressText">Processing...</span>
<div class="flex items-center gap-2">
<span id="progressPercent">0%</span>
<button id="cancelThumbnailsBtn" class="text-pm-error hover:text-pm-error text-xs px-2 py-1 bg-pm-error-tint rounded-pm-sm hidden">
Cancel
</button>
</div>
</div>
<div class="w-full bg-pm-input rounded-full h-2">
<div id="progressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="flex items-center justify-between text-xs text-pm-muted mt-1">
<span id="progressDetails">Scanning images...</span>
<span id="progressETA" class="text-pm-muted"></span>
</div>
</div>
</div>
<!-- Image Loading -->
<div class="bg-pm-input rounded-pm-sm p-3">
<h5 class="text-pm text-xs font-medium mb-2">Image Loading</h5>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Lazy Loading</label>
<input type="checkbox" id="lazyLoadingToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Image Quality</label>
<select id="imageQualitySelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="high">High (Original)</option>
<option value="medium" selected>Medium (Optimized)</option>
<option value="low">Low (Fast)</option>
</select>
</div>
</div>
</div>
</div>
<!-- Display Settings -->
<div class="space-y-3">
<h4 class="text-sm font-semibold text-pm">Display Settings</h4>
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Default View Mode</label>
<select id="defaultViewModeSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="grid" selected>Grid View</option>
<option value="list">List View</option>
</select>
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Items per Page</label>
<select id="defaultLimitSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Grid Columns (Desktop)</label>
<select id="gridColumnsSelect" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="4">4 Columns</option>
<option value="6">6 Columns</option>
<option value="8" selected>8 Columns</option>
<option value="10">10 Columns</option>
</select>
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Show Media Info</label>
<input type="checkbox" id="showImageInfoToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Collapse Sidebar by Default</label>
<input type="checkbox" id="sidebarCollapsedByDefaultToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
</div>
<!-- Video Settings -->
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
<h5 class="text-pm text-xs font-medium mb-2">Video Settings</h5>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Autoplay Videos</label>
<input type="checkbox" id="videoAutoplayToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Mute Videos by Default</label>
<input type="checkbox" id="videoMuteToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Loop Videos</label>
<input type="checkbox" id="videoLoopToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
</div>
</div>
<!-- Metadata Settings -->
<div class="space-y-3">
<h4 class="text-sm font-semibold text-pm">Metadata Settings</h4>
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Auto-load Metadata</label>
<input type="checkbox" id="autoLoadMetadataToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Cache Metadata</label>
<input type="checkbox" id="cacheMetadataToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Show File Paths</label>
<input type="checkbox" id="showFilePathsToggle" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
</div>
</div>
<!-- Maintenance -->
<div class="space-y-3">
<h4 class="text-sm font-semibold text-pm">Maintenance</h4>
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
<div class="flex items-center justify-between">
<div>
<h5 class="text-pm text-xs font-medium">Clear Cache</h5>
<p class="text-xs text-pm-secondary">Clear cached metadata and thumbnails</p>
</div>
<button id="clearCacheBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs rounded-pm-sm transition-colors">
Clear
</button>
</div>
<div class="flex items-center justify-between">
<div>
<h5 class="text-pm text-xs font-medium">Rescan Output Folder</h5>
<p class="text-xs text-pm-secondary">Refresh the image list from disk</p>
</div>
<button id="rescanFolderBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs rounded-pm-sm transition-colors">
Rescan
</button>
</div>
<div class="flex items-center justify-between">
<div>
<h5 class="text-pm text-xs font-medium">Find Duplicate Images</h5>
<p class="text-xs text-pm-secondary">Scan for duplicate images based on file content</p>
</div>
<button id="scanDuplicatesBtn" class="px-3 py-1 bg-pm-warning hover:bg-pm-warning text-pm text-xs rounded-pm-sm transition-colors">
Scan
</button>
</div>
</div>
</div>
<!-- Advanced Settings -->
<div class="space-y-3">
<h4 class="text-sm font-semibold text-pm">Advanced</h4>
<div class="bg-pm-input rounded-pm-sm p-3 space-y-2">
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">Debug Mode</label>
<input type="checkbox" id="debugModeToggle" class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
</div>
<div class="flex items-center justify-between">
<label class="text-xs text-pm-secondary">API Timeout (seconds)</label>
<input type="number" id="apiTimeoutInput" value="30" min="5" max="300" class="px-2 py-1 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs w-20">
</div>
</div>
</div>
</div>
<div class="p-3 border-t border-pm flex justify-end gap-2">
<button id="resetSettingsBtn" class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Reset to Defaults
</button>
<button id="saveSettingsBtn" class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Save Settings
</button>
</div>
</div>
</div>
<!-- Duplicates Modal -->
<div id="duplicatesModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md max-w-4xl w-full max-h-[90vh] overflow-hidden mx-4 border border-pm">
<div class="p-3 border-b border-pm">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-pm">Duplicate Images Found</h3>
<button id="closeDuplicatesBtn" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<div class="p-3 overflow-y-auto max-h-[calc(90vh-200px)]">
<div id="duplicatesScanStatus" class="text-center py-6">
<div class="inline-flex items-center px-3 py-2 bg-pm-warning rounded-pm-sm text-xs text-pm">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Scanning for duplicate images...
</div>
</div>
<div id="duplicatesContent" class="hidden">
<div class="mb-3 p-3 bg-pm-error-tint border border-pm-accent rounded-pm-sm">
<div class="flex items-start">
<svg class="w-4 h-4 text-pm-error mt-0.5 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path>
</svg>
<div>
<h4 class="text-pm-error text-xs font-medium">Warning: Removing duplicate images cannot be undone!</h4>
<p class="text-pm-secondary text-xs mt-1">When you remove a duplicate image, the selected file will be permanently deleted from disk. Choose carefully which images to keep.</p>
</div>
</div>
</div>
<div id="duplicatesList" class="space-y-3">
<!-- Duplicates will be populated here -->
</div>
</div>
</div>
<div id="duplicatesFooter" class="hidden p-3 border-t border-pm flex justify-between items-center">
<div class="text-xs text-pm-secondary">
<span id="duplicatesCount">0</span> duplicate image groups found
</div>
<div class="flex gap-2">
<button id="selectAllBtn" class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Select All to Delete
</button>
<button id="removeDuplicatesBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
Delete Selected
</button>
</div>
</div>
</div>
</div>
<!-- Auto Tag Modal -->
<div id="autoTagModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md p-3 max-w-lg w-full mx-4 border border-pm">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-pm">Auto Tag Images</h3>
<button onclick="hideModal('autoTagModal')" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Model Selection -->
<div class="mb-3">
<label class="block text-xs font-medium text-pm-secondary mb-2">Model Selection</label>
<div class="space-y-2">
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="gguf" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">GGUF Model</span>
<span class="text-pm-secondary text-xs ml-2">(Recommended, ~4.5GB)</span>
</div>
</div>
<div id="ggufModelStatus" class="flex items-center gap-2">
<span class="text-pm-secondary text-xs">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="hf" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">HuggingFace Model</span>
<span class="text-pm-secondary text-xs ml-2">(~16GB)</span>
</div>
</div>
<div id="hfModelStatus" class="flex items-center gap-2">
<span class="text-pm-secondary text-xs">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="wd14-swinv2" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">WD14 SwinV2</span>
<span class="text-pm-secondary text-xs ml-2">(~400MB, fast classifier)</span>
</div>
</div>
<div id="wd14SwinV2ModelStatus" class="flex items-center gap-2">
<span class="text-pm-secondary text-xs">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="wd14-vit" class="w-4 h-4 text-pm-accent bg-pm-input border-pm focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">WD14 ViT</span>
<span class="text-pm-secondary text-xs ml-2">(~400MB, ViT architecture)</span>
</div>
</div>
<div id="wd14VitModelStatus" class="flex items-center gap-2">
<span class="text-pm-secondary text-xs">Checking...</span>
</div>
</label>
</div>
</div>
<!-- Keep Model in Memory Option -->
<div class="mb-3">
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="checkbox" id="keepModelInMemory" checked class="w-4 h-4 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">Keep model in memory</span>
<span class="text-pm-secondary text-xs ml-2">(Faster processing, uses ~4-16GB VRAM)</span>
</div>
</div>
<button id="unloadModelBtn" class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs rounded-pm-sm transition-colors hidden">
Unload Model
</button>
</label>
<p id="modelLoadedStatus" class="text-pm-muted text-xs mt-1 hidden">
<span class="text-pm-success">●</span> Model currently loaded: <span id="loadedModelType">none</span>
</p>
</div>
<!-- Prompt Template (LLM models only) -->
<div id="promptSection" class="mb-3">
<label class="block text-xs font-medium text-pm-secondary mb-2">Tag Generation Prompt</label>
<textarea id="autoTagPrompt" rows="6"
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs font-mono resize-none"
placeholder="Enter prompt template...">Generate only comma-separated Danbooru tags (lowercase_underscores, no spaces).
Include: subject_count (1girl/1boy/etc), gender, ethnicity (asian, caucasian, etc), age_approx (teen, young_woman), body_attributes, face_features, hairstyle, appearance, clothing, accessories, pose, expression, action, camera_angle, camera_shot, scene_composition (wide_shot, long_shot, dutch_angle), environment, background_elements, lighting, mood.
Prioritize visual accuracy.
No sentences, no commentary, no captions. Only tags. Keep length short but descriptive.</textarea>
</div>
<!-- WD14 Threshold Controls -->
<div id="wd14ThresholdSection" class="mb-3 hidden">
<label class="block text-xs font-medium text-pm-secondary mb-2">Tag Confidence Thresholds</label>
<div class="space-y-3">
<div class="p-2.5 bg-pm-input rounded-pm-sm border border-pm">
<div class="flex items-center justify-between mb-1">
<span class="text-pm text-xs">General tags</span>
<span id="wd14GeneralThresholdValue" class="text-pm-accent text-xs font-mono">0.35</span>
</div>
<input type="range" id="wd14GeneralThreshold" min="0" max="1" step="0.05" value="0.35"
class="w-full h-1.5 bg-pm-primary rounded-lg appearance-none cursor-pointer accent-[var(--pm-accent)]">
</div>
<div class="p-2.5 bg-pm-input rounded-pm-sm border border-pm">
<div class="flex items-center justify-between mb-1">
<span class="text-pm text-xs">Character tags</span>
<span id="wd14CharacterThresholdValue" class="text-pm-accent text-xs font-mono">0.85</span>
</div>
<input type="range" id="wd14CharacterThreshold" min="0" max="1" step="0.05" value="0.85"
class="w-full h-1.5 bg-pm-primary rounded-lg appearance-none cursor-pointer accent-[var(--pm-accent)]">
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end gap-2">
<button onclick="hideModal('autoTagModal')"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="startReviewBtn"
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Review
</button>
<button id="startAutoTagBtn"
class="px-3 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Auto Tag
</button>
</div>
</div>
</div>
<!-- Auto Tag Review Modal -->
<div id="autoTagReviewModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md p-3 max-w-4xl w-full mx-4 border border-pm">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-pm">
Review Tags
<span class="text-pm-secondary text-xs font-normal">(<span id="reviewCurrentIndex">1</span> of <span id="reviewTotalCount">0</span>)</span>
</h3>
<button onclick="window.gallery.cancelReview()" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- Image Preview -->
<div class="bg-pm-primary rounded-pm-sm p-3">
<img id="reviewImage" src="" alt="Review image" class="w-full h-64 object-contain rounded-pm-sm">
<p id="reviewImageName" class="text-pm-secondary text-xs mt-2 text-center truncate">image_name.png</p>
</div>
<!-- Generated Tags -->
<div>
<label class="block text-xs font-medium text-pm-secondary mb-2">Generated Tags (click to remove)</label>
<div id="reviewTagsContainer" class="flex flex-wrap gap-2 p-3 bg-pm-primary rounded-pm-sm min-h-[200px] max-h-[250px] overflow-y-auto">
<!-- Tag chips will be inserted here -->
</div>
<p class="text-pm-muted text-xs mt-1">Click the x on tags to remove them before applying</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-between mt-3">
<button id="skipReviewBtn"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Skip
</button>
<div class="flex gap-2">
<button onclick="window.gallery.cancelReview()"
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel All
</button>
<button id="applyReviewBtn"
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
Apply & Next
</button>
</div>
</div>
</div>
</div>
<!-- Auto Tag Progress Modal -->
<div id="autoTagProgressModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md p-3 max-w-md w-full mx-4 border border-pm">
<h3 class="text-sm font-semibold text-pm mb-3">Auto Tagging Progress</h3>
<!-- Current File -->
<div class="mb-3">
<p class="text-pm-secondary text-xs">Processing:</p>
<p id="autoTagCurrentFile" class="text-pm text-xs font-medium truncate">Initializing...</p>
</div>
<!-- Progress Bar -->
<div class="mb-3">
<div class="flex justify-between text-xs text-pm-secondary mb-1">
<span>Progress</span>
<span id="autoTagProgressPercent">0%</span>
</div>
<div class="w-full bg-pm-input rounded-full h-2">
<div id="autoTagProgressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Stats -->
<div class="grid grid-cols-3 gap-2 mb-3">
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
<div class="text-sm font-semibold text-pm-accent" id="autoTagProcessed">0</div>
<div class="text-xs text-pm-secondary">Processed</div>
</div>
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
<div class="text-sm font-semibold text-pm-success" id="autoTagApplied">0</div>
<div class="text-xs text-pm-secondary">Tags Applied</div>
</div>
<div class="bg-pm-input rounded-pm-sm p-2 text-center">
<div class="text-sm font-semibold text-pm-warning" id="autoTagSkipped">0</div>
<div class="text-xs text-pm-secondary">Skipped</div>
</div>
</div>
<!-- Cancel Button -->
<div class="flex justify-center">
<button id="cancelAutoTagBtn"
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
</div>
</div>
</div>
<!-- Auto Tag Download Modal -->
<div id="autoTagDownloadModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md p-3 max-w-md w-full mx-4 border border-pm">
<h3 class="text-sm font-semibold text-pm mb-3">Downloading Model</h3>
<!-- Model Name -->
<div class="mb-3">
<p class="text-pm-secondary text-xs">Downloading:</p>
<p id="downloadModelName" class="text-pm text-xs font-medium">GGUF Model</p>
</div>
<!-- Progress Bar -->
<div class="mb-3">
<div class="flex justify-between text-xs text-pm-secondary mb-1">
<span id="downloadStatus">Preparing...</span>
<span id="downloadProgressPercent">0%</span>
</div>
<div class="w-full bg-pm-input rounded-full h-2">
<div id="downloadProgressBar" class="bg-pm-accent h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- Cancel Button -->
<div class="flex justify-center">
<button id="cancelDownloadBtn"
class="px-3 py-1 bg-pm-error hover:bg-pm-error text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
</div>
</div>
</div>
<!-- Add Prompt Modal -->
<div id="addPromptModal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-md p-3 max-w-2xl w-full mx-4 border border-pm max-h-[90vh] overflow-y-auto">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-pm">Add New Prompt</h3>
<button onclick="window.gallery.hideAddPromptModal()" class="p-2 rounded-pm-sm hover:bg-pm-hover transition-colors">
<svg class="w-4 h-4 text-pm-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<!-- Prompt Text -->
<div class="mb-3">
<label class="block text-xs font-medium text-pm-secondary mb-1">Prompt Text <span class="text-pm-error">*</span></label>
<textarea id="addPromptText" rows="4"
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs resize-none"
placeholder="Enter your prompt text here..."></textarea>
</div>
<!-- Category and Rating Row -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-3">
<!-- Category -->
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Category</label>
<div class="relative">
<input type="text" id="addPromptCategory" list="categoryList"
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs"
placeholder="Select or type category...">
<datalist id="categoryList"></datalist>
</div>
</div>
<!-- Rating -->
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Rating</label>
<div class="flex items-center gap-1" id="addPromptRating">
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="1">☆</button>
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="2">☆</button>
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="3">☆</button>
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="4">☆</button>
<button type="button" class="rating-star text-lg text-pm-muted hover:text-pm-warning transition-colors" data-rating="5">☆</button>
<button type="button" class="ml-2 text-xs text-pm-muted hover:text-pm-secondary" id="clearRatingBtn">Clear</button>
</div>
<input type="hidden" id="addPromptRatingValue" value="">
</div>
</div>
<!-- Tags -->
<div class="mb-3">
<label class="block text-xs font-medium text-pm-secondary mb-1">Tags</label>
<div class="relative">
<div id="addPromptTagsContainer" class="flex flex-wrap gap-2 p-2 bg-pm-input border border-pm rounded-pm-sm min-h-[44px]">
<input type="text" id="addPromptTagInput"
class="flex-1 min-w-[150px] bg-transparent border-none outline-none text-pm text-xs placeholder-pm"
placeholder="Type to add tags...">
</div>
<div id="tagSuggestions" class="absolute left-0 right-0 top-full mt-1 bg-pm-surface border border-pm rounded-pm-sm shadow-pm hidden z-10 max-h-48 overflow-y-auto"></div>
</div>
<p class="text-pm-muted text-xs mt-1">Press Enter or comma to add a tag. Click a tag to remove it.</p>
</div>
<!-- Notes -->
<div class="mb-3">
<label class="block text-xs font-medium text-pm-secondary mb-1">Notes</label>
<textarea id="addPromptNotes" rows="2"
class="w-full px-3 py-2 bg-pm-input border border-pm rounded-pm-sm focus:ring-2 focus:ring-pm-accent focus:border-pm-accent text-pm text-xs resize-none"
placeholder="Optional notes about this prompt..."></textarea>
</div>
<!-- Protection Option -->
<div class="mb-3">
<label class="flex items-start p-3 bg-pm-input rounded-pm-sm cursor-pointer hover:bg-pm-hover border border-pm">
<input type="checkbox" id="addPromptProtected" checked class="w-4 h-4 mt-0.5 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
<div class="ml-3">
<span class="text-pm text-xs font-medium">Protect from orphan cleanup</span>
<p class="text-pm-secondary text-xs mt-1">Keep this prompt even if it has no linked images. Uncheck if you want this prompt to be automatically removed during maintenance when it has no images.</p>
</div>
</label>
</div>
<!-- Action Buttons -->
<div class="flex justify-end gap-2">
<button onclick="window.gallery.hideAddPromptModal()"
class="px-3 py-1 bg-pm-input hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="saveNewPromptBtn"
class="px-3 py-1 bg-pm-success hover:bg-pm-success text-pm text-xs font-medium rounded-pm-sm transition-colors">
Add Prompt
</button>
</div>
</div>
</div>
<script src="/prompt_manager/js/comfy-metadata.js"></script>
<script src="/prompt_manager/js/gallery.js"></script>
</body>
</html>