* feat(system): resolve every library path in one place utils/library_location.py decides where the library lives: the PROMPT_MANAGER_DATA_DIR folder (validated: absolute, never inside a folder ComfyUI or Prompt Manager serves), an explicit database.default_path as before, a node-folder library that was not moved, else ComfyUI's user/__promptmanager. The database, diagnostics, config.json, logs and the LoRA image cache all ask it, so a new install never writes into the node folder and an existing one keeps every path until the user moves it. "Keep it here" is persisted as library.keep_in_node_folder. * feat(system): move the library as a verified Activity job POST /prompt_manager/system/library/move starts "Move library", an exclusive, non-cancellable job (database/library_move.py). It checks the target volume's free space, copies the database with SQLite's backup API from one read snapshot, verifies the copy (integrity check, every workflow document, the same rows in every table), copies the safety and pre-migration backups and config.json (owner-only, checked by size and SHA-256), then publishes under the old file's write lock and switches the live process (every PromptModel, the review and history observers, the API's plans and health report). The old prompts.db and config.json are renamed *.moved-to-<folder> only after the switch. A failure removes everything it wrote and leaves the old library in use; a marker in the new folder makes a restart after any crash pick the right library. GET /prompt_manager/system/library and the Overview's "location" say where the library lives and why; POST .../keep persists "Keep it here". * feat(system): show where the library lives and offer the move D129. System > Overview's Library health gains a "Library location" row (the database path, why it is there, the "Keep it here" choice and any PROMPT_MANAGER_DATA_DIR problem) with Move library for a node-folder library and Details otherwise. While a node-folder library is not kept there, the workspace shows the concept's .notice above every page: "Keep it here" saves the choice on the server and stops the notice; "Move library" opens the shared review (From, To, Size, what moves, what happens to the old files) and starts the exclusive job in Activity. The notice and the Overview read the location again when the job ends. The Overview comparison maps the inserted row; a new comparison holds the notice to the concept's Sources notice (markup, tokens, alignment, both themes and widths) with a mutation check. * feat(system): close the browser server's log files before its root goes The logs now live in the library's data folder, which for the browser test server is <root>/user/__promptmanager; Windows could not remove the temp root while the server still held prompt_manager.log open. The API test's node folder is spelled in lower case because public paths are case-folded on Windows. * feat(system): show the library location on the legacy admin page A line under the admin page's statistics names the database path and why it is there (ComfyUI's user folder, PROMPT_MANAGER_DATA_DIR, config.json, or the node folder with a pointer to Move library), read once from GET /prompt_manager/system/library. * feat(system): prove a fresh host keeps its library in user/__promptmanager The execution suite starts one pinned CPU ComfyUI with no configuration at all on an OS-chosen port: the library is created in user/__promptmanager (database and logs), nothing but byte code is added to the node folder, the location route reports the default folder, and ComfyUI's /userdata API serves none of it (traversal spellings, a listing, a system-user header). * feat(system): document where the library lives and how it moves README: where the library lives, the upgrade path (Move library / Keep it here), PROMPT_MANAGER_DATA_DIR, explicit paths unchanged, and the way back to an older version. TESTING.md: D129 and the library location contract (resolution order, move steps, crash windows, coded refusals, Windows handling). * wip(system): library move fix round in progress * fix(system): retry a fallback image link that races the library switch The image monitor's fallback and recent-prompt links went through PromptDatabase.link_image_to_prompt, which logs and returns 0 on any error, so survive_switch never saw a "database is locked" or closed connection during Move library and the link was lost in both files. The monitor now links through link_image_to_prompt_or_raise (one transaction per attempt); link_image_to_prompt keeps its log-and-return-0 contract for other callers. A link that returns 0 is no longer logged as "Successfully linked image 0". A switch that fails after repointing the models now closes the old file's detached connections instead of leaving them to GC: switch() adds them to a list the mover owns. The mocked retry test is replaced by a real-lock race asserting the image row lands in the new library only. * fix(system): say which source is the default and where a symlinked library lives Sources & scans: with no source configured, ComfyUI's output folder now reads "ComfyUI/output · watching · default" on its own row instead of a separate note line. GET /settings marks each effective source with in_comfyui (its public path is relative to the ComfyUI base) so only those get the "ComfyUI/" prefix; the scan still sends the server's public path. Library location: a library in the node folder reads through the node folder's unresolved place in custom_nodes, so a node folder symlinked to a checkout outside ComfyUI shows custom_nodes/promptmanager/prompts.db instead of prompts.db (System library row, Move review, admin Library line). Never absolute or "..", else the shared _public_path, which sources and jobs keep using unchanged.
1449 lines
90 KiB
HTML
1449 lines
90 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 - 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>
|
||
<button id="settingsBtn"
|
||
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
|
||
</button>
|
||
<button id="diagnosticsBtn"
|
||
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
|
||
</button>
|
||
<button id="maintenanceBtn"
|
||
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
|
||
</button>
|
||
<button id="backupBtn"
|
||
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
|
||
</button>
|
||
<button id="restoreBtn"
|
||
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
|
||
</button>
|
||
<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>
|
||
<button id="logsBtn"
|
||
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
|
||
</button>
|
||
<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 Settings -->
|
||
<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>
|
||
</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>←</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 → Z</option>
|
||
<option value="alpha_desc">Z → 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">Settings</h3>
|
||
|
||
<div class="space-y-4">
|
||
<!-- Gallery Settings Section -->
|
||
<div class="border-b border-pm pb-3">
|
||
<h4 class="text-xs font-semibold text-pm-secondary mb-3 uppercase tracking-wide">Gallery Settings</h4>
|
||
|
||
<div class="mb-3">
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Image Scan Directories</label>
|
||
<div id="scanPathsList" class="space-y-2">
|
||
<!-- Populated dynamically by JS -->
|
||
</div>
|
||
<button id="addScanPathBtn" type="button"
|
||
class="mt-2 flex items-center gap-1 text-xs text-pm-accent hover:text-pm-accent-hover">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||
</svg>
|
||
Add Directory
|
||
</button>
|
||
<p class="text-xs text-pm-muted mt-1">Directories to scan for images. Leave empty to use ComfyUI default output directory.</p>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="flex items-center justify-between">
|
||
<label class="block text-xs font-medium text-pm-secondary">Currently Monitoring</label>
|
||
<button id="refreshMonitoringStatus" class="text-xs text-pm-accent hover:text-pm-accent-hover">Refresh</button>
|
||
</div>
|
||
<div id="monitoringStatus" class="mt-1.5 px-2.5 py-1.5 bg-pm-input rounded-pm-sm text-xs text-pm-secondary font-mono">
|
||
Loading...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- UI Settings Section -->
|
||
<div class="border-b border-pm pb-3">
|
||
<h4 class="text-xs font-semibold text-pm-secondary mb-3 uppercase tracking-wide">UI Settings</h4>
|
||
|
||
<div class="mb-3">
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Search Results Auto-Hide (seconds)</label>
|
||
<input type="number" id="resultTimeout" min="0" max="300" value="30"
|
||
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">
|
||
<p class="text-xs text-pm-muted mt-1">Set to 0 to disable auto-hide.</p>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Web UI Display Mode</label>
|
||
<select id="webuiDisplayMode"
|
||
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="popup">Popup Window (Current)</option>
|
||
<option value="newtab">New Tab</option>
|
||
</select>
|
||
<p class="text-xs text-pm-muted mt-1">Choose how the Web UI opens from ComfyUI nodes.</p>
|
||
</div>
|
||
|
||
<div class="mt-3 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>
|
||
|
||
<!-- Performance Section -->
|
||
<div class="border-b border-pm pb-3">
|
||
<h4 class="text-xs font-semibold text-pm-secondary mb-3 uppercase tracking-wide">Performance</h4>
|
||
|
||
<div>
|
||
<div class="flex items-center justify-between mb-1.5">
|
||
<label for="workerThreads" class="text-xs font-medium text-pm-secondary">Worker threads</label>
|
||
<span id="workerThreadsValue" class="text-pm-accent text-xs font-mono">1 / 1 cores</span>
|
||
</div>
|
||
<input type="range" id="workerThreads" min="1" max="1" step="1" value="1"
|
||
class="w-full h-1.5 bg-pm-primary rounded-lg appearance-none cursor-pointer accent-[var(--pm-accent)]">
|
||
<p class="text-xs text-pm-muted mt-1">Threads used by the image rescan and thumbnail generation. More threads finish sooner; fewer leave CPU for ComfyUI while it generates.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Integrations Section -->
|
||
<div class="border-b border-pm pb-3">
|
||
<h4 class="text-xs font-semibold text-pm-secondary mb-3 uppercase tracking-wide">Integrations</h4>
|
||
|
||
<div class="mb-3">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<div class="flex items-center gap-2">
|
||
<label class="block text-xs font-medium text-pm-secondary">LoraManager</label>
|
||
<span id="loraDetectionBadge" class="text-[10px] px-1.5 py-0.5 rounded-full bg-pm-input text-pm-muted">checking...</span>
|
||
</div>
|
||
<label class="relative inline-flex items-center cursor-pointer">
|
||
<input type="checkbox" id="loraEnabled" class="sr-only peer" disabled>
|
||
<div class="w-8 h-4 bg-pm-input rounded-full peer peer-checked:bg-pm-accent transition-colors pointer-events-none after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:after:translate-x-4"></div>
|
||
</label>
|
||
</div>
|
||
|
||
<div id="loraSettings" class="hidden space-y-2 mt-2 pl-2 border-l-2 border-pm">
|
||
<div>
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Path</label>
|
||
<input type="text" id="loraManagerPath" readonly
|
||
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] text-pm-muted">
|
||
</div>
|
||
|
||
<div class="flex items-center justify-between">
|
||
<label class="block text-xs font-medium text-pm-secondary">Auto-inject trigger words</label>
|
||
<label class="relative inline-flex items-center cursor-pointer">
|
||
<input type="checkbox" id="loraTriggerWords" class="sr-only peer">
|
||
<div class="w-8 h-4 bg-pm-input rounded-full peer peer-checked:bg-pm-accent transition-colors pointer-events-none after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:after:translate-x-4"></div>
|
||
</label>
|
||
</div>
|
||
<p class="text-xs text-pm-muted">Automatically append trigger words when <code class="text-pm-accent"><lora:name:weight></code> is detected in prompts.</p>
|
||
|
||
<div class="mt-2">
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">CivitAI API Key</label>
|
||
<input type="password" id="civitaiApiKey" placeholder="Optional — required for NSFW example images"
|
||
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">
|
||
<p class="text-xs text-pm-muted mt-1">Get your key from <a href="https://civitai.com/user/account" target="_blank" class="text-pm-accent hover:underline">civitai.com/user/account</a></p>
|
||
</div>
|
||
|
||
<label class="mt-1 flex items-center gap-1.5 text-xs text-pm-secondary cursor-pointer">
|
||
<input type="checkbox" id="clearCivitaiKey" class="w-3.5 h-3.5 text-pm-accent bg-pm-input border-pm rounded focus:ring-pm-accent">
|
||
<span>Clear the stored key on save</span>
|
||
</label>
|
||
|
||
<button id="loraImportBtn" type="button"
|
||
class="mt-1 flex items-center gap-1 text-xs text-pm-accent hover:text-pm-accent-hover">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||
</svg>
|
||
Import LoRA Data
|
||
</button>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- LoRA Import Progress Modal -->
|
||
<div id="loraImportModal" 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">Importing LoRA Data</h3>
|
||
|
||
<div class="mb-3">
|
||
<p class="text-pm-secondary text-xs">Status:</p>
|
||
<p id="loraImportStatus" class="text-pm text-xs font-medium truncate">Initializing...</p>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="flex justify-between text-xs text-pm-secondary mb-1">
|
||
<span>Progress</span>
|
||
<span id="loraImportPercent">0%</span>
|
||
</div>
|
||
<div class="w-full bg-pm-input rounded-full h-1.5">
|
||
<div id="loraImportBar" 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="loraImportProcessed">0 processed</span>
|
||
<span id="loraImportImported">0 imported</span>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- Database Restore Modal -->
|
||
<div id="restoreModal" 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">Restore Database</h3>
|
||
|
||
<div class="mb-3">
|
||
<div class="bg-pm-warning-tint border border-pm-warning/40 rounded-pm-md p-3 mb-3">
|
||
<h4 class="text-pm-warning font-medium text-xs">Warning</h4>
|
||
<p class="text-pm text-xs mt-1">This will replace your current database. A backup will be created automatically.</p>
|
||
</div>
|
||
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Select Database File (.db)</label>
|
||
<input type="file" id="restoreFileInput" accept=".db"
|
||
class="w-full px-2.5 py-1.5 bg-pm-input border border-pm rounded-pm-sm text-pm text-[13px] file:mr-3 file:py-1 file:px-3 file:rounded-pm-sm file:border-0 file:text-xs file:font-medium file:bg-pm-accent file:text-pm-accent-fg">
|
||
</div>
|
||
|
||
<div class="flex justify-end gap-2 mt-4">
|
||
<button id="cancelRestore"
|
||
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="confirmRestore"
|
||
class="px-3 py-1.5 bg-pm-warning hover:opacity-80 text-black text-xs font-medium rounded-pm-sm transition-colors">
|
||
Restore Database
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Diagnostics Modal -->
|
||
<div id="diagnosticsModal" 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 max-w-4xl 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">Gallery System Diagnostics</h3>
|
||
<button onclick="window.admin.closeDiagnostics()" 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 class="flex items-center justify-between mb-3">
|
||
<button id="runDiagnosticsBtn" 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">
|
||
Run Diagnostics
|
||
</button>
|
||
<button id="testImageLinkBtn" 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">
|
||
Test Image Link
|
||
</button>
|
||
</div>
|
||
<div id="diagnosticsContent" class="space-y-3">
|
||
<div class="text-center py-6 text-pm-secondary text-xs">
|
||
Click "Run Diagnostics" to check the gallery system status
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Maintenance Modal -->
|
||
<div id="maintenanceModal" 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 max-w-4xl 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">Database Maintenance</h3>
|
||
<button onclick="window.admin.closeMaintenance()" 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)]">
|
||
<!-- Maintenance Options -->
|
||
<div class="space-y-3 mb-4">
|
||
<h4 class="text-xs font-semibold text-pm-secondary uppercase tracking-wide mb-3">Select Maintenance Operations:</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="cleanup_duplicates" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]" checked>
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Remove Duplicates</div>
|
||
<div class="text-[11px] text-pm-secondary">Find and remove duplicate prompts based on text content</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="vacuum" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]" checked>
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Vacuum Database</div>
|
||
<div class="text-[11px] text-pm-secondary">Optimize database file size and performance</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="cleanup_orphaned_images" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]" checked>
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Clean Orphaned Images</div>
|
||
<div class="text-[11px] text-pm-secondary">Remove image records for files that no longer exist</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="check_hash_duplicates" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]">
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Check Hash Duplicates</div>
|
||
<div class="text-[11px] text-pm-secondary">Identify prompts with duplicate hash values</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="check_consistency" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]">
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Consistency Check</div>
|
||
<div class="text-[11px] text-pm-secondary">Verify database integrity and data consistency</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="statistics" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]">
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Database Statistics</div>
|
||
<div class="text-[11px] text-pm-secondary">Generate detailed database statistics report</div>
|
||
</div>
|
||
</label>
|
||
|
||
<label class="flex items-center gap-2.5 p-3 bg-pm-input rounded-pm-md hover:bg-pm-hover cursor-pointer">
|
||
<input type="checkbox" id="prune_orphaned_prompts" class="maintenance-option w-3.5 h-3.5 accent-[var(--pm-accent)]">
|
||
<div class="flex-1">
|
||
<div class="text-pm text-xs font-medium">Prune Orphaned Prompts</div>
|
||
<div class="text-[11px] text-pm-secondary">Remove prompts that have no linked images</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Action Buttons -->
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex gap-2">
|
||
<button id="runMaintenanceBtn" 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">
|
||
Run Maintenance
|
||
</button>
|
||
<button id="selectAllMaintenanceBtn" 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">
|
||
Select All
|
||
</button>
|
||
<button id="clearAllMaintenanceBtn" 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">
|
||
Clear All
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Results -->
|
||
<div id="maintenanceResults" class="space-y-3">
|
||
<div class="text-center py-6 text-pm-secondary text-xs">
|
||
Click "Run Maintenance" to perform selected operations
|
||
</div>
|
||
</div>
|
||
</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>
|
||
|
||
<!-- Logs Modal -->
|
||
<div id="logsModal" 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 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">System Logs</h3>
|
||
<button onclick="window.admin.closeLogs()" 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>
|
||
|
||
<!-- Log Controls -->
|
||
<div class="p-4 border-b border-pm">
|
||
<div class="grid grid-cols-1 md:grid-cols-5 gap-3 mb-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Log Level</label>
|
||
<select id="logLevel" 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 Levels</option>
|
||
<option value="DEBUG">Debug</option>
|
||
<option value="INFO">Info</option>
|
||
<option value="WARNING">Warning</option>
|
||
<option value="ERROR">Error</option>
|
||
<option value="CRITICAL">Critical</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Limit</label>
|
||
<select id="logLimit" 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="50">Last 50</option>
|
||
<option value="100" selected>Last 100</option>
|
||
<option value="200">Last 200</option>
|
||
<option value="500">Last 500</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex items-end">
|
||
<button id="refreshLogsBtn" class="w-full 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">
|
||
Refresh
|
||
</button>
|
||
</div>
|
||
<div class="flex items-end">
|
||
<button id="downloadLogsBtn" class="w-full 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">
|
||
Download
|
||
</button>
|
||
</div>
|
||
<div class="flex items-end">
|
||
<button id="clearLogsBtn" class="w-full px-2.5 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">
|
||
Clear Logs
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log Stats -->
|
||
<div class="grid grid-cols-2 md:grid-cols-5 gap-3 mb-3">
|
||
<div class="bg-pm-input rounded-pm-md p-2">
|
||
<div class="text-[11px] text-pm-secondary">Total Logs</div>
|
||
<div class="text-sm font-semibold text-pm-accent" id="logStatsTotal">-</div>
|
||
</div>
|
||
<div class="bg-pm-input rounded-pm-md p-2">
|
||
<div class="text-[11px] text-pm-secondary">Errors</div>
|
||
<div class="text-sm font-semibold text-pm-error" id="logStatsErrors">-</div>
|
||
</div>
|
||
<div class="bg-pm-input rounded-pm-md p-2">
|
||
<div class="text-[11px] text-pm-secondary">Warnings</div>
|
||
<div class="text-sm font-semibold text-pm-warning" id="logStatsWarnings">-</div>
|
||
</div>
|
||
<div class="bg-pm-input rounded-pm-md p-2">
|
||
<div class="text-[11px] text-pm-secondary">Current Level</div>
|
||
<div class="text-sm font-semibold text-pm-success" id="logStatsLevel">-</div>
|
||
</div>
|
||
<div class="bg-pm-input rounded-pm-md p-2">
|
||
<div class="text-[11px] text-pm-secondary">Log Size</div>
|
||
<div class="text-sm font-semibold text-pm" id="logStatsSize">-</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log Settings -->
|
||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||
<div>
|
||
<label class="block text-xs font-medium text-pm-secondary mb-1">Set Log Level</label>
|
||
<select id="setLogLevel" 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="DEBUG">Debug</option>
|
||
<option value="INFO" selected>Info</option>
|
||
<option value="WARNING">Warning</option>
|
||
<option value="ERROR">Error</option>
|
||
<option value="CRITICAL">Critical</option>
|
||
</select>
|
||
</div>
|
||
<div class="flex items-center">
|
||
<label class="flex items-center gap-1.5 text-pm">
|
||
<input type="checkbox" id="consoleLogging" checked class="w-3.5 h-3.5 accent-[var(--pm-accent)]">
|
||
<span class="text-xs">Console Logging</span>
|
||
</label>
|
||
</div>
|
||
<div class="flex items-end">
|
||
<button id="updateLogConfigBtn" class="w-full 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">
|
||
Update Config
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log Viewer -->
|
||
<div class="flex-1 overflow-hidden">
|
||
<div class="h-96 overflow-y-auto bg-pm-primary font-mono text-xs">
|
||
<div id="logsContainer" class="p-3 space-y-0.5">
|
||
<div class="text-center py-6 text-pm-muted text-xs">
|
||
Click "Refresh" to load logs
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Log Files -->
|
||
<div class="p-4 border-t border-pm">
|
||
<h4 class="text-xs font-semibold text-pm-secondary uppercase tracking-wide mb-3">Log Files</h4>
|
||
<div id="logFilesList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 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 with existing tags</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>
|
||
<p class="text-pm-muted text-[11px] mt-1">Note: "auto-scanned" is not counted as a real tag</p>
|
||
</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>
|
||
</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 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">This image already has tags. Would you like to 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>
|
||
|
||
<!-- Auto Tag Download Modal -->
|
||
<div id="autoTagDownloadModal" 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">Downloading Model</h3>
|
||
|
||
<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>
|
||
|
||
<div class="mb-4">
|
||
<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-1.5">
|
||
<div id="downloadProgressBar" class="bg-pm-accent h-1.5 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="flex justify-center">
|
||
<button id="cancelDownloadBtn"
|
||
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>
|
||
|
||
<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>
|