Files
ComfyAssets-ComfyUI_PromptM…/web/admin.html
T
Vito 95586e6395 refactor(gallery): open System for gallery settings, maintenance and tagging models (#330, PR 3) (#408)
* test(legacy): snapshot the routes and the settings response

Pins every registered route and the key paths of GET /prompt_manager/settings
while System S13 (#330) moves the legacy pages' tools to native System. Recorded
on the unchanged main; cutover (#334) removes it.

* test(legacy): prove each removed legacy control has native coverage

TESTING.md gains the "Legacy System UI removed by S13" table (no rows yet).
legacy-coverage.test.mjs fails when a removal cites a capability that is not
covered, copies another native test, lists an id or method twice, or leaves
the id or method on its page. legacy-system-links.spec.mjs holds the System
link table and guards the kept admin flows.

* refactor(admin): open System for Diagnostics and logs

The admin page's Diagnostics and View Logs buttons become links that open
System › Diagnostics & logs in a new tab. The legacy Diagnostics and System Logs
dialogs and their methods go; TESTING.md lists them against their covered
capabilities (rows B1–B7).

* refactor(admin): open System for settings and keep page options

Settings opens System › Overview in a new tab. The settings dialog becomes Page
options (opened from the list header) with only the Infinite Scrolling default,
and its Save posts {"infinite_scroll": …} alone, so a stale legacy form can no
longer overwrite scan folders or other values edited in System (#330 S13-9).
Scan folders, monitoring status, auto-hide, display mode, worker threads,
LoraManager and the LoRA import leave the page (rows B8–B14).

* refactor(admin): open System for maintenance

Maintenance opens System › Maintenance in a new tab, where each operation runs
as a reviewed Activity job. The one-click legacy Database Maintenance dialog
and its methods go (rows B15–B18); formatFileSize stays for the image modal.

* refactor(admin): open System for backup and restore

Backup DB and Restore DB open System › Backup & export in a new tab, where a
restore is staged, reviewed and run as an Activity job. The one-click Restore
dialog and its methods go (rows B19–B20); the scan dialog's quick backup stays.
The removed browser tests' data checks live on at the route: a new round trip
test restores every prompt with its tags and image links.

* test(legacy): find a removed modal id passed to showModal and settle the links spec

The removal check also finds an id passed as a string to a ...Modal() call and no
longer reads data-id= as id=. The no-request test waits for the page's own
start-up reads to finish, the sort check compares as the server sorts, and the
kept-flow test checks the added prompt shows in the list.

* test(admin): open Page options after the saved default is read

Drops the leftover LoraManager section comment above the Page options save.

* refactor(gallery): open System for settings and maintenance

The gallery's Settings opens System › Appearance in a new tab. Its settings
dialog becomes Page options (a new header button) with only the page's own
settings: thumbnail startup check, lazy loading, items per page, grid columns,
infinite scrolling, debug mode and API timeout. Save and Reset keep every other
stored key, so the Appearance values System saves survive (S13-13). Generate
Thumbnails, Clear cache, Rescan and Find duplicates leave the page; Page options
links System › Maintenance and Sources & scans (rows B21–B28).

* refactor(autotag): manage tagging models in System

In both auto-tag dialogs a model that is not downloaded shows a Manage in
System link to System › Auto-tagging (a new tab) instead of a Download button,
and the gallery's Unload Model becomes the same link. The legacy download
dialog and the download, cancel and unload methods go (rows B29–B31); the batch
and review stay. A test hook marks a fake model loaded.

* docs(system): name System as the home for settings and maintenance

README and TESTING.md send settings, maintenance, backups, diagnostics, the
gallery's tools and auto-tag model downloads to their System section and name
each legacy page's Page options.

* fix(gallery): keep the default view in Page options and save viewer choices alone

System › Appearance's Default layout is the workspace's own key, so the legacy
gallery's Default View Mode stays a page option instead of losing its only
control. The viewer's mute and loop buttons remember just that choice instead of
saving the Page options dialog's unfilled inputs over the stored page options.
The pending-thumbnails toast names System › Sources & scans.

* test(legacy): check each System link sends no request in its own test

The no-request check runs inside each link's test, so a slow page no longer
times out the whole table, and it ignores lazy thumbnail loads the layout
starts on its own.

* test(legacy): a lazy thumbnail load is not a System link's request

* fix(gallery): let the header buttons wrap on a narrow screen

With Page options beside Settings the header no longer fits a phone's width;
it wrapped off the page and covered the Page options dialog in the touch runs.

* feat(gallery): remove Default View Mode from Page options (#330)

System > Appearance's Default layout is the one default-layout control, as
the S13 design's G5 says. The legacy gallery's Page options no longer show
or save a default view and Reset no longer resets it. A stored legacy
defaultViewMode still picks the view the gallery opens in (grid otherwise),
and Save and Reset keep that key for the workspace-preferences migration.
2026-10-28 13:31:23 +00:00

998 lines
62 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PromptManager - Admin Dashboard</title>
<script src="js/theme-bridge.js"></script>
<script src="js/html-escape.js"></script>
<script src="js/image-helpers.js"></script>
<script src="js/api-client.js"></script>
<script src="js/png-metadata.js"></script>
<script src="js/list-state.js"></script>
<script src="js/tag-autocomplete.js"></script>
<script src="js/sse-stream.js"></script>
<script src="js/scan-progress.js"></script>
<script src="js/data-actions.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 text-[13px]">
<div class="min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-pm-surface border-b border-pm px-4 py-2" style="min-height: 2.5rem;">
<div class="max-w-7xl mx-auto">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h1 class="text-sm font-semibold text-pm">
PromptManager Admin
</h1>
</div>
<div class="hidden sm:flex items-center gap-1.5">
<div class="w-2 h-2 bg-pm-success rounded-full animate-pulse"></div>
<span class="text-pm-success text-xs">Online</span>
</div>
</div>
</div>
</header>
<!-- Dashboard View -->
<div id="dashboardView">
<!-- Stats Grid -->
<div class="px-4 py-3">
<div class="max-w-7xl mx-auto">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div class="bg-pm-surface rounded-pm-lg p-3 border border-pm shadow-pm-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-pm-secondary text-xs font-medium uppercase tracking-wide">Total Prompts</p>
<p class="text-lg font-bold text-pm-accent" id="totalPrompts">-</p>
</div>
</div>
</div>
<div class="bg-pm-surface rounded-pm-lg p-3 border border-pm shadow-pm-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-pm-secondary text-xs font-medium uppercase tracking-wide">Categories</p>
<p class="text-lg font-bold text-pm" id="totalCategories">-</p>
</div>
</div>
</div>
<div class="bg-pm-surface rounded-pm-lg p-3 border border-pm shadow-pm-sm cursor-pointer hover:border-pm-accent transition-colors"
onclick="window.location.hash = '#/tags'" title="Click to manage tags">
<div class="flex items-center justify-between">
<div>
<p class="text-pm-secondary text-xs font-medium uppercase tracking-wide">Unique Tags</p>
<p class="text-lg font-bold text-pm-success" id="totalTags">-</p>
</div>
</div>
</div>
<div class="bg-pm-surface rounded-pm-lg p-3 border border-pm shadow-pm-sm">
<div class="flex items-center justify-between">
<div>
<p class="text-pm-secondary text-xs font-medium uppercase tracking-wide">Avg Rating</p>
<p class="text-lg font-bold text-pm-warning" id="avgRating">-</p>
</div>
</div>
</div>
</div>
<!-- #249 S6b: where the library (database, config.json, backups) lives and why. -->
<p class="text-pm-secondary text-xs mt-2" id="libraryLocation" hidden></p>
</div>
</div>
<!-- Search Controls (Sticky) -->
<div class="sticky top-0 z-50 bg-pm-primary/95 backdrop-blur-sm border-b border-pm-subtle">
<div class="px-4 py-3">
<div class="max-w-7xl mx-auto">
<div class="bg-pm-surface rounded-pm-lg p-4 border border-pm">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-7 gap-3 mb-3">
<div class="lg:col-span-2">
<label class="block text-xs font-medium text-pm-secondary mb-1 uppercase tracking-wide">Search Text</label>
<input type="text" id="searchText" placeholder="Search prompt content..."
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] placeholder:text-pm-muted focus:outline-none">
</div>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1 uppercase tracking-wide">Category</label>
<select id="searchCategory"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none">
<option value="">All Categories</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1 uppercase tracking-wide">Folder</label>
<select id="searchFolder"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none">
<option value="">All Folders</option>
</select>
</div>
<div class="relative">
<label class="block text-xs font-medium text-pm-secondary mb-1 uppercase tracking-wide">Tags</label>
<input type="text" id="searchTags" placeholder="tag1, tag2..." autocomplete="off"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] placeholder:text-pm-muted focus:outline-none">
<!-- Autocomplete: tags that co-occur with the ones already typed -->
<div id="searchTagSuggestions" role="listbox"
class="absolute left-0 right-0 top-full mt-1 bg-pm-surface border border-pm rounded-pm-md shadow-pm hidden z-20 max-h-56 overflow-y-auto"></div>
</div>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1 uppercase tracking-wide">Sort By</label>
<select id="sortBy"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none">
<!-- Filled from PromptListSort.SORT_OPTIONS in admin.js -->
<option value="last_used_desc">Recently Used</option>
</select>
</div>
<div class="flex items-end">
<button id="searchBtn"
class="w-full bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg font-medium py-1.5 px-3 rounded-pm-sm text-[13px] transition-colors flex items-center justify-center gap-1.5">
<span>Search</span>
</button>
</div>
</div>
<!-- Bulk Actions -->
<div class="flex flex-wrap items-center gap-2 pt-3 border-t border-pm">
<label class="flex items-center gap-1.5 text-pm">
<input type="checkbox" id="selectAll" class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<span class="text-xs font-medium">Select All</span>
</label>
<button id="bulkDeleteBtn" disabled
class="px-2.5 py-1 text-pm-error bg-transparent hover:bg-pm-error-tint disabled:opacity-25 disabled:cursor-not-allowed text-xs font-medium rounded-pm-sm transition-colors border border-transparent hover:border-pm">
Delete Selected
</button>
<button id="bulkTagBtn" disabled
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover disabled:opacity-25 disabled:cursor-not-allowed text-pm text-xs font-medium rounded-pm-sm transition-colors">
Add Tags
</button>
<button id="bulkCategoryBtn" disabled
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover disabled:opacity-25 disabled:cursor-not-allowed text-pm text-xs font-medium rounded-pm-sm transition-colors">
Set Category
</button>
<div class="w-px h-4 bg-pm-border mx-1"></div>
<button id="addPromptBtn"
class="px-2.5 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Add Prompt
</button>
<button id="statsBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Stats
</button>
<button id="exportBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Export
</button>
<a id="settingsBtn" href="workspace#/system/overview" target="_blank" rel="noopener"
title="Opens System › Overview in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Settings
</a>
<a id="diagnosticsBtn" href="workspace#/system/diagnostics" target="_blank" rel="noopener"
title="Opens System › Diagnostics &amp; logs in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Diagnostics
</a>
<a id="maintenanceBtn" href="workspace#/system/maintenance" target="_blank" rel="noopener"
title="Opens System › Maintenance in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Maintenance
</a>
<a id="backupBtn" href="workspace#/system/backups" target="_blank" rel="noopener"
title="Opens System › Backup &amp; export in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Backup DB
</a>
<a id="restoreBtn" href="workspace#/system/backups" target="_blank" rel="noopener"
title="Opens System › Backup &amp; export in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Restore DB
</a>
<button id="scanBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Scan Images
</button>
<button id="autoTagBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Auto Tag
</button>
<a id="logsBtn" href="workspace#/system/diagnostics" target="_blank" rel="noopener"
title="Opens System › Diagnostics &amp; logs in a new tab"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
View Logs
</a>
<button id="metadataBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Metadata
</button>
<button id="galleryBtn"
class="px-2.5 py-1 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Gallery
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Results Section -->
<div class="flex-1 px-4 pb-4">
<div class="max-w-7xl mx-auto h-full">
<div class="bg-pm-surface rounded-pm-lg border border-pm h-full flex flex-col">
<!-- Results Header -->
<div class="flex items-center justify-between p-3 border-b border-pm">
<div class="flex items-center gap-3">
<h2 class="text-[13px] font-semibold text-pm" id="resultsTitle">Recent Prompts</h2>
<div class="flex items-center gap-1.5">
<label class="text-xs text-pm-secondary">Show:</label>
<select id="limitSelector" class="px-2 py-0.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-xs">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
<span class="text-xs text-pm-secondary">per page</span>
</div>
<!-- Session-only toggle; the saved default lives in Page options -->
<label class="flex items-center gap-1.5 text-xs text-pm-secondary cursor-pointer" title="Load the next page as you scroll (this session)">
<input type="checkbox" id="infiniteScrollSession" class="w-3.5 h-3.5 rounded border-pm bg-pm-input text-pm-accent focus:ring-pm-accent">
Infinite scroll
</label>
<button id="pageOptionsBtn" type="button" class="text-xs text-pm-secondary hover:text-pm-accent underline">Page options</button>
</div>
<span class="text-xs font-medium text-pm-secondary bg-pm-input px-2 py-0.5 rounded-full" id="resultsCount">Loading...</span>
</div>
<!-- Loading State -->
<div id="loadingState" class="flex-1 flex items-center justify-center">
<div class="text-center">
<div class="w-8 h-8 border-3 border-pm-accent/30 border-t-pm-accent rounded-full animate-spin mx-auto mb-3"></div>
<p class="text-pm-secondary text-xs">Loading prompts...</p>
</div>
</div>
<!-- Results List -->
<div id="resultsList" class="flex-1 overflow-y-auto p-3 space-y-2 hidden">
</div>
<!-- Pagination Controls -->
<div id="paginationControls" class="border-t border-pm p-3 hidden">
<div class="flex items-center justify-between">
<div class="text-xs text-pm-secondary">
Showing <span id="showingStart">1</span> to <span id="showingEnd">50</span> of <span id="totalResults">0</span> prompts
</div>
<div class="flex items-center gap-1.5">
<button id="firstPageBtn" class="px-2 py-0.5 bg-pm-input hover:bg-pm-hover text-pm rounded-pm-sm disabled:opacity-25 disabled:cursor-not-allowed text-xs">
First
</button>
<button id="prevPageBtn" class="px-2 py-0.5 bg-pm-input hover:bg-pm-hover text-pm rounded-pm-sm disabled:opacity-25 disabled:cursor-not-allowed text-xs">
Previous
</button>
<span class="text-xs text-pm-secondary">
Page <span id="currentPage">1</span> of <span id="totalPages">1</span>
</span>
<button id="nextPageBtn" class="px-2 py-0.5 bg-pm-input hover:bg-pm-hover text-pm rounded-pm-sm disabled:opacity-25 disabled:cursor-not-allowed text-xs">
Next
</button>
<button id="lastPageBtn" class="px-2 py-0.5 bg-pm-input hover:bg-pm-hover text-pm rounded-pm-sm disabled:opacity-25 disabled:cursor-not-allowed text-xs">
Last
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- /dashboardView -->
<!-- Tags Page View -->
<div id="tagsPageView" class="hidden flex-1 flex flex-col">
<!-- Tags Header -->
<div class="px-4 py-2 border-b border-pm-subtle bg-pm-surface/50">
<div class="max-w-7xl mx-auto flex items-center justify-between">
<div class="flex items-center gap-3">
<button onclick="window.location.hash=''" class="text-pm-secondary hover:text-pm transition-colors text-xs flex items-center gap-1">
<span>&larr;</span><span>Dashboard</span>
</button>
<h2 class="text-[13px] font-semibold text-pm">Tags</h2>
<span id="tagsTotalBadge" class="text-xs font-medium text-pm-secondary bg-pm-input px-2 py-0.5 rounded-full">0 tags</span>
</div>
<div id="filterModeToggle" class="hidden items-center gap-1.5">
<span class="text-xs text-pm-secondary">Filter:</span>
<button id="filterModeAnd" class="px-2 py-0.5 text-xs font-medium rounded-pm-sm bg-pm-accent text-pm-accent-fg">AND</button>
<button id="filterModeOr" class="px-2 py-0.5 text-xs font-medium rounded-pm-sm bg-pm-input text-pm hover:bg-pm-hover">OR</button>
</div>
</div>
</div>
<!-- Two-Panel Layout -->
<div class="flex-1 flex overflow-hidden">
<!-- Left Panel: Tag List -->
<div class="w-72 flex-shrink-0 border-r border-pm-subtle flex flex-col bg-pm-surface/30">
<!-- Tag Search & Sort -->
<div class="p-3 space-y-2 border-b border-pm-subtle">
<input type="text" id="tagSearchInput" placeholder="Search tags..."
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none">
<select id="tagSortSelect" class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px]">
<option value="alpha_asc">A &rarr; Z</option>
<option value="alpha_desc">Z &rarr; A</option>
<option value="count_desc">Most Used</option>
<option value="count_asc">Least Used</option>
</select>
</div>
<!-- Tag List -->
<div id="tagsListContainer" class="flex-1 overflow-y-auto">
<div id="tagsList" class="p-1.5 space-y-0.5"></div>
<div id="tagsLoader" class="text-center">
<div class="tags-loader-spinner p-3 hidden">
<div class="w-5 h-5 border-2 border-pm-accent/30 border-t-pm-accent rounded-full animate-spin mx-auto"></div>
</div>
</div>
</div>
</div>
<!-- Right Panel: Prompts Display -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Active Tags Pills -->
<div id="activeTagsPills" class="px-4 py-2 border-b border-pm-subtle hidden">
<div class="flex flex-wrap gap-1.5 items-center">
<span class="text-xs text-pm-secondary mr-0.5">Filtering:</span>
<div id="tagPillsContainer" class="flex flex-wrap gap-1.5"></div>
<span id="tagPromptsCountBadge" class="text-xs text-pm-muted ml-auto"></span>
</div>
</div>
<!-- Prompts Grid -->
<div id="tagPromptsContainer" class="flex-1 overflow-y-auto">
<div id="tagPromptsGrid" class="p-4 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3"></div>
<div id="tagPromptsLoader" class="text-center">
<div class="prompts-loader-spinner p-3 hidden">
<div class="w-5 h-5 border-2 border-pm-accent/30 border-t-pm-accent rounded-full animate-spin mx-auto"></div>
</div>
<button id="loadMorePromptsBtn" class="hidden px-3 py-1.5 my-2 text-xs text-pm-secondary hover:text-pm-accent border border-pm hover:border-pm-accent rounded-pm-sm transition-colors">
Load more prompts
</button>
</div>
</div>
<!-- Empty State -->
<div id="tagPromptsEmpty" class="flex-1 flex items-center justify-center">
<div class="text-center text-pm-muted">
<p class="text-sm font-medium">Select a tag to view prompts</p>
<p class="text-xs mt-1">Click a tag from the list or use checkboxes for multi-tag filtering</p>
</div>
</div>
</div>
</div>
<!-- Context Menu for Tag Operations -->
<div id="tagContextMenu" class="hidden fixed z-50 bg-pm-surface border border-pm rounded-pm-md shadow-pm py-1 min-w-[160px]">
</div>
</div><!-- /tagsPageView -->
</div>
<!-- Settings Modal -->
<div id="settingsModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm max-h-[90vh] overflow-y-auto shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Page options</h3>
<div class="space-y-4">
<div class="border-b border-pm pb-3">
<h4 class="text-xs font-semibold text-pm-secondary mb-3 uppercase tracking-wide">Prompt list</h4>
<div class="flex items-center justify-between">
<div>
<label for="infiniteScrollDefault" class="block text-xs font-medium text-pm-secondary">Infinite Scrolling</label>
<p class="text-xs text-pm-muted">Load the next page of prompts as you scroll. The checkbox in the list header overrides this for the current session.</p>
</div>
<input type="checkbox" id="infiniteScrollDefault" class="w-4 h-4 rounded border-pm bg-pm-input text-pm-accent focus:ring-pm-accent ml-3">
</div>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="cancelSettings"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="saveSettings"
class="px-3 py-1.5 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>
<!-- Bulk Tag Modal -->
<div id="bulkTagModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-md w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Add Tags to Selected Prompts</h3>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Tags (comma-separated)</label>
<input type="text" id="bulkTagInput" placeholder="tag1, tag2, tag3..."
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] placeholder:text-pm-muted focus:outline-none">
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="cancelBulkTag"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="confirmBulkTag"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Add Tags
</button>
</div>
</div>
</div>
<!-- Individual Tag Modal -->
<div id="individualTagModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-md w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Add Tags to Prompt</h3>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Tags (comma-separated)</label>
<input type="text" id="individualTagInput" placeholder="tag1, tag2, tag3..."
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] placeholder:text-pm-muted focus:outline-none">
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="cancelIndividualTag"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="confirmIndividualTag"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Add Tags
</button>
</div>
</div>
</div>
<!-- Bulk Category Modal -->
<div id="bulkCategoryModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-md w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Set Category for Selected Prompts</h3>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Category</label>
<input type="text" id="bulkCategoryInput" placeholder="Enter category name..."
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] placeholder:text-pm-muted focus:outline-none">
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="cancelBulkCategory"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="confirmBulkCategory"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Set Category
</button>
</div>
</div>
</div>
<!-- Gallery Modal -->
<div id="galleryModal" class="fixed inset-0 bg-black/75 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg max-w-6xl w-full max-h-[90vh] overflow-hidden mx-4 border border-pm shadow-pm">
<div class="p-4 border-b border-pm">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-pm" id="galleryTitle">Generated Images</h3>
<button onclick="window.admin.closeGallery()" class="p-1 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-4 overflow-y-auto max-h-[calc(90vh-120px)]">
<div id="galleryContent" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
</div>
<div id="galleryEmpty" class="text-center py-8 hidden">
<h4 class="text-sm font-medium text-pm mb-1">No images found</h4>
<p class="text-pm-muted text-xs">No images have been generated with this prompt yet.</p>
</div>
<div id="galleryLoading" class="text-center py-8">
<div class="w-8 h-8 border-3 border-pm-accent/30 border-t-pm-accent rounded-full animate-spin mx-auto mb-3"></div>
<p class="text-pm-secondary text-xs">Loading images...</p>
</div>
</div>
</div>
</div>
<!-- Scan Modal -->
<div id="scanModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Scan ComfyUI Output Images</h3>
<div class="space-y-3">
<div class="bg-pm-warning-tint border border-pm-warning/40 rounded-pm-md p-3">
<h4 class="text-pm-warning font-medium text-xs mb-1.5">Important Information</h4>
<ul class="text-pm text-xs space-y-0.5">
<li>This will scan all PNG files in your ComfyUI output directory</li>
<li>Depending on size, this could take several minutes</li>
<li>This is experimental — we recommend backing up first</li>
<li>Only images with ComfyUI or A1111/Forge-style metadata will be processed</li>
<li>Duplicate prompts will be automatically detected and skipped</li>
</ul>
</div>
<div class="bg-pm-info-tint border border-pm-info/40 rounded-pm-md p-3">
<div class="flex items-center justify-between">
<span class="text-pm text-xs font-medium">Backup Database First (Recommended)</span>
<button id="quickBackupBtn"
class="px-2.5 py-1 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Backup Now
</button>
</div>
</div>
<div id="scanProgress" class="hidden space-y-2">
<div class="bg-pm-input rounded-pm-md p-3">
<div class="flex items-center justify-between mb-1.5">
<span class="text-pm text-xs font-medium">Scanning Progress</span>
<span id="scanStatusText" class="text-pm-secondary text-xs">Preparing...</span>
</div>
<div class="w-full bg-pm-primary rounded-full h-1.5">
<div id="scanProgressBar" class="bg-pm-accent h-1.5 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<div class="flex justify-between text-[11px] text-pm-muted mt-1">
<span id="scanCount">0 files processed</span>
<span id="scanFound">0 prompts found</span>
</div>
</div>
</div>
</div>
<div class="flex justify-end gap-2 mt-4">
<button id="cancelScan"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
<button id="startScan"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Start Scan
</button>
</div>
</div>
</div>
<!-- Minimized scan progress: shown while a scan runs with its modal hidden -->
<button id="scanPill" type="button" title="Show scan progress"
class="hidden fixed bottom-4 right-4 z-40 px-3 py-1.5 bg-pm-surface border border-pm shadow-pm rounded-pm-md text-xs text-pm hover:bg-pm-hover transition-colors">
<span class="inline-block w-1.5 h-1.5 rounded-full bg-pm-accent mr-1.5 align-middle animate-pulse"></span><span id="scanPillText" class="align-middle">Scanning...</span>
</button>
<!-- Add Prompt Modal -->
<div id="addPromptModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-2xl w-full mx-4 border border-pm max-h-[90vh] overflow-y-auto shadow-pm">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-pm">Add New Prompt</h3>
<button onclick="window.admin.hideModal('addPromptModal')" class="p-1 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-4">
<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-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] resize-none focus:outline-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-4">
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Category</label>
<div class="relative">
<input type="text" id="addPromptCategory" list="addPromptCategoryList"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] focus:outline-none"
placeholder="Select or type category...">
<datalist id="addPromptCategoryList"></datalist>
</div>
</div>
<div>
<label class="block text-xs font-medium text-pm-secondary mb-1">Rating</label>
<div class="flex items-center gap-0.5" 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-1.5 text-[11px] text-pm-muted hover:text-pm" id="clearAddPromptRatingBtn">Clear</button>
</div>
<input type="hidden" id="addPromptRatingValue" value="">
</div>
</div>
<!-- Tags -->
<div class="mb-4">
<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-1.5 p-2 bg-pm-input border border-pm rounded-pm-sm min-h-[36px]">
<input type="text" id="addPromptTagInput"
class="flex-1 min-w-[150px] bg-transparent border-none outline-none text-pm text-[13px] placeholder:text-pm-muted"
placeholder="Type to add tags...">
</div>
<div id="addPromptTagSuggestions" class="absolute left-0 right-0 top-full mt-1 bg-pm-surface border border-pm rounded-pm-md shadow-pm hidden z-10 max-h-48 overflow-y-auto"></div>
</div>
<p class="text-pm-muted text-[11px] mt-1">Press Enter or comma to add a tag. Click a tag to remove it.</p>
</div>
<!-- Notes -->
<div class="mb-4">
<label class="block text-xs font-medium text-pm-secondary mb-1">Notes</label>
<textarea id="addPromptNotes" rows="2"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] resize-none focus:outline-none"
placeholder="Optional notes about this prompt..."></textarea>
</div>
<!-- Protection Option -->
<div class="mb-4">
<label class="flex items-start p-3 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<input type="checkbox" id="addPromptProtected" checked class="w-3.5 h-3.5 mt-0.5 accent-[var(--pm-accent)]">
<div class="ml-2.5">
<span class="text-pm text-xs font-medium">Protect from orphan cleanup</span>
<p class="text-pm-secondary text-[11px] mt-0.5">Keep this prompt even if it has no linked images.</p>
</div>
</label>
</div>
<!-- Action Buttons -->
<div class="flex justify-end gap-2">
<button onclick="window.admin.hideModal('addPromptModal')"
class="px-3 py-1.5 bg-pm-surface border border-pm 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.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Add Prompt
</button>
</div>
</div>
</div>
<!-- Auto Tag Modal -->
<div id="autoTagModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm shadow-pm">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-semibold text-pm">Auto Tag Images</h3>
<button onclick="window.admin.hideModal('autoTagModal')" class="p-1 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-4">
<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-md cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="gguf" checked class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<div class="ml-2.5">
<span class="text-pm text-xs font-medium">GGUF Model</span>
<span class="text-pm-secondary text-[11px] ml-1.5">(Recommended, ~4.5GB)</span>
</div>
</div>
<div id="ggufModelStatus" class="flex items-center gap-1.5">
<span class="text-pm-secondary text-[11px]">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="hf" class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<div class="ml-2.5">
<span class="text-pm text-xs font-medium">HuggingFace Model</span>
<span class="text-pm-secondary text-[11px] ml-1.5">(~16GB)</span>
</div>
</div>
<div id="hfModelStatus" class="flex items-center gap-1.5">
<span class="text-pm-secondary text-[11px]">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="wd14-swinv2" class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<div class="ml-2.5">
<span class="text-pm text-xs font-medium">WD14 SwinV2</span>
<span class="text-pm-secondary text-[11px] ml-1.5">(~400MB, fast classifier)</span>
</div>
</div>
<div id="wd14SwinV2ModelStatus" class="flex items-center gap-1.5">
<span class="text-pm-secondary text-[11px]">Checking...</span>
</div>
</label>
<label class="flex items-center justify-between p-3 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<div class="flex items-center">
<input type="radio" name="autoTagModel" value="wd14-vit" class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<div class="ml-2.5">
<span class="text-pm text-xs font-medium">WD14 ViT</span>
<span class="text-pm-secondary text-[11px] ml-1.5">(~400MB, ViT architecture)</span>
</div>
</div>
<div id="wd14VitModelStatus" class="flex items-center gap-1.5">
<span class="text-pm-secondary text-[11px]">Checking...</span>
</div>
</label>
</div>
</div>
<!-- Prompt Template (LLM models only) -->
<div id="promptSection" class="mb-4">
<label class="block text-xs font-medium text-pm-secondary mb-1">Tag Generation Prompt</label>
<textarea id="autoTagPrompt" rows="6"
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] font-mono resize-none focus:outline-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-4 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-md 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-md 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>
<!-- Tagged Images Handling -->
<div class="mb-4">
<label class="block text-xs font-medium text-pm-secondary mb-2">Already Tagged Images</label>
<div class="space-y-1.5">
<label class="flex items-center p-2.5 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<input type="radio" name="autoTagMode" value="skip" checked class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<span class="ml-2.5 text-pm text-xs">Skip images already auto-tagged</span>
</label>
<label class="flex items-center p-2.5 bg-pm-input rounded-pm-md cursor-pointer hover:bg-pm-hover border border-pm">
<input type="radio" name="autoTagMode" value="retag" class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
<span class="ml-2.5 text-pm text-xs">Re-tag all images</span>
</label>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-end gap-2">
<button onclick="window.admin.hideModal('autoTagModal')"
class="px-3 py-1.5 bg-pm-surface border border-pm 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.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Review
</button>
<button id="startAutoTagBtn"
class="px-3 py-1.5 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/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-4xl w-full mx-4 border border-pm shadow-pm">
<div class="flex items-center justify-between mb-4">
<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.admin.cancelReview()" class="p-1 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-4">
<!-- Image Preview -->
<div class="bg-pm-primary rounded-pm-md p-3">
<img id="reviewImage" src="" alt="Review image" class="w-full h-56 object-contain rounded-pm-sm">
<p id="reviewImageName" class="text-pm-secondary text-xs mt-1.5 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="p-3 bg-pm-primary rounded-pm-md">
<div id="reviewTagsVisible" class="flex flex-wrap gap-1.5"></div>
<div id="reviewTagsAccordion" class="hidden mt-2">
<div id="reviewTagsHidden" class="flex flex-wrap gap-1.5 max-h-[180px] overflow-y-auto"></div>
</div>
<button id="reviewTagsToggle" class="hidden mt-2 text-xs text-pm-accent hover:text-pm-accent-hover transition-colors flex items-center gap-0.5">
<span id="reviewTagsToggleIcon">▼</span>
<span id="reviewTagsToggleText">Show all tags</span>
<span id="reviewTagsCount" class="text-pm-muted"></span>
</button>
</div>
<p class="text-pm-muted text-[11px] mt-1">Click the × on tags to remove them before applying</p>
</div>
</div>
<!-- Action Buttons -->
<div class="flex justify-between mt-4">
<button id="skipReviewBtn"
class="px-3 py-1.5 bg-pm-surface border border-pm 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.admin.cancelReview()"
class="px-3 py-1.5 text-pm-error bg-transparent hover:bg-pm-error-tint border border-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel All
</button>
<button id="applyReviewBtn"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg 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/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-md w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-4">Auto Tagging Progress</h3>
<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>
<div class="mb-4">
<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-1.5">
<div id="autoTagProgressBar" class="bg-pm-accent h-1.5 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
<p id="autoTagCloseNote" class="text-pm-muted text-[11px] mt-1.5">Closing this window stops tagging. Tags already added are kept. Prompt Manager's Activity page shows the time left.</p>
</div>
<div class="grid grid-cols-3 gap-3 mb-4">
<div class="bg-pm-input rounded-pm-md p-2 text-center">
<div class="text-lg font-bold text-pm-accent" id="autoTagProcessed">0</div>
<div class="text-[11px] text-pm-secondary">Processed</div>
</div>
<div class="bg-pm-input rounded-pm-md p-2 text-center">
<div class="text-lg font-bold text-pm-success" id="autoTagApplied">0</div>
<div class="text-[11px] text-pm-secondary">Tags Applied</div>
</div>
<div class="bg-pm-input rounded-pm-md p-2 text-center">
<div class="text-lg font-bold text-pm-warning" id="autoTagSkipped">0</div>
<div class="text-[11px] text-pm-secondary">Skipped</div>
</div>
</div>
<div class="flex justify-center">
<button id="cancelAutoTagBtn"
class="px-3 py-1.5 text-pm-error bg-transparent hover:bg-pm-error-tint border border-pm text-xs font-medium rounded-pm-sm transition-colors">
Cancel
</button>
</div>
</div>
</div>
<!-- Auto Tag Loading Model Modal -->
<div id="autoTagLoadingModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-md w-full mx-4 border border-pm shadow-pm text-center">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-pm-accent mx-auto mb-3"></div>
<h3 class="text-sm font-semibold text-pm mb-1">Loading Model</h3>
<p id="autoTagLoadingStatus" class="text-pm-secondary text-xs">Initializing...</p>
<p class="text-pm-muted text-[11px] mt-3">This may take 30-60 seconds on first load</p>
</div>
</div>
<!-- Re-tag Confirmation Modal -->
<div id="retagConfirmModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm shadow-pm">
<h3 class="text-sm font-semibold text-pm mb-3">Image Already Auto-tagged</h3>
<div class="mb-3">
<img id="retagPreviewImage" src="" alt="Preview" class="w-full h-40 object-contain bg-pm-primary rounded-pm-md">
<p id="retagImageName" class="text-pm-secondary text-xs mt-1.5 text-center truncate"></p>
</div>
<div class="mb-3">
<p class="text-pm text-xs mb-1.5">Current tags:</p>
<div id="retagExistingTags" class="flex flex-wrap gap-1 max-h-20 overflow-y-auto">
</div>
</div>
<p class="text-pm-secondary text-xs mb-4">A tagging model already tagged this image. Generate new tags?</p>
<div class="flex gap-2 justify-end">
<button id="retagSkipBtn"
class="px-2.5 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Skip
</button>
<button id="retagSkipAllBtn"
class="px-2.5 py-1.5 bg-pm-warning hover:opacity-80 text-black text-xs font-medium rounded-pm-sm transition-colors">
Skip All Tagged
</button>
<button id="retagConfirmBtn"
class="px-2.5 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Re-tag
</button>
</div>
</div>
</div>
<!-- Update Notice Modal -->
<div id="updateNoticeModal" class="fixed inset-0 bg-black/60 backdrop-blur-[2px] hidden items-center justify-center z-50">
<div class="bg-pm-surface rounded-pm-lg p-4 max-w-lg w-full mx-4 border border-pm shadow-pm">
<div class="flex items-start gap-3 mb-3">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-pm-warning-tint flex items-center justify-center">
<svg class="w-4 h-4 text-pm-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<h3 class="text-sm font-semibold text-pm">Image Rescan Required</h3>
<p class="text-xs text-pm-secondary mt-0.5">Multi-directory support update</p>
</div>
</div>
<div class="bg-pm-warning-tint border border-pm-warning/40 rounded-pm-md p-3 mb-3">
<p class="text-pm text-xs leading-relaxed">
This update changes how images are stored and organized. To ensure your gallery works correctly
with the new multi-directory support, you need to <strong>rescan your images</strong>.
</p>
<ul class="text-pm text-xs mt-2 space-y-1 list-disc list-inside text-pm-secondary">
<li>Folder information will be added to existing image records</li>
<li>Your prompts and tags will not be affected</li>
<li>The scan may take a few minutes depending on your library size</li>
</ul>
</div>
<div class="flex items-center justify-between mt-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="dismissUpdateNotice" class="rounded border-pm bg-pm-input text-pm-accent focus:ring-pm-accent w-3.5 h-3.5">
<span class="text-xs text-pm-muted">Don't show this again</span>
</label>
<div class="flex gap-2">
<button id="updateNoticeDismiss"
class="px-3 py-1.5 bg-pm-surface border border-pm hover:bg-pm-hover text-pm text-xs font-medium rounded-pm-sm transition-colors">
Dismiss
</button>
<button id="updateNoticeScan"
class="px-3 py-1.5 bg-pm-accent hover:bg-pm-accent-hover text-pm-accent-fg text-xs font-medium rounded-pm-sm transition-colors">
Scan Now
</button>
</div>
</div>
</div>
</div>
<script src="js/comfy-metadata.js"></script>
<script src="js/prompt-list-sort.js"></script>
<script src="js/view-router.js"></script>
<script src="js/tags-page.js"></script>
<script src="js/admin.js"></script>
</body>
</html>