Files
Vito e9fe586442 fix: repair prompt edit hashes, auto-tag review, and stale UI responses (#180)
* fix: keep edited prompt hashes consistent

Fixes #176

* fix: align gallery auto-tag review with scoped image APIs

Fixes #177

* fix: keep Tags prompt results on the current filter

Fixes #178

* fix: ignore stale metadata in admin image viewers

Fixes #179

* test: compare canonical gallery paths on macOS
2026-10-16 06:14:06 -07:00

129 lines
5.4 KiB
JavaScript
Raw Permalink 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.
/**
* Pure helpers for image entries rendered by the admin and gallery pages.
*
* Image rows come from the database (`width`, `height`, `file_size` are stored
* from a POST body and may be strings) or from the output-folder scan (`id` is
* a path digest, `path` is the file). Everything these helpers return for HTML
* is already escaped, so callers can drop it into text or a quoted attribute.
*
* Loadable in the browser (window.ImageHelpers) and in Node for tests.
*/
(function (root) {
"use strict";
const escapeHtml =
typeof module !== "undefined" && module.exports
? require("./html-escape.js").escapeHtml
: root.escapeHtml;
const UNKNOWN_SIZE = "Unknown size";
/**
* "640×480 | 1.2 MB" for a card body or a viewer caption, escaped.
*
* @param {{width?: unknown, height?: unknown, file_size?: unknown}|null|undefined} image
* @param {{formatFileSize?: (bytes: number) => string, separator?: string}} [options]
* @returns {string} escaped text; "Unknown size" when a dimension is missing
*/
function formatImageCaption(image, { formatFileSize, separator = " | " } = {}) {
const entry = image || {};
const parts = [];
if (entry.width && entry.height) {
parts.push(`${escapeHtml(entry.width)}×${escapeHtml(entry.height)}`);
} else {
parts.push(UNKNOWN_SIZE);
}
const bytes = Number(entry.file_size);
if (formatFileSize && Number.isFinite(bytes) && bytes > 0) {
parts.push(escapeHtml(formatFileSize(bytes)));
}
return parts.join(escapeHtml(separator));
}
/**
* A complete standalone page showing a workflow as pretty-printed JSON.
* Written into a blank same-origin window, so the JSON must not be able
* to close the <pre> and inject markup.
*
* @param {unknown} workflow
* @returns {string} HTML document
*/
function workflowDocumentHtml(workflow) {
const json = escapeHtml(JSON.stringify(workflow, null, 2));
return [
"<!DOCTYPE html>",
"<html>",
"<head><meta charset=\"utf-8\"><title>ComfyUI Workflow Data</title></head>",
"<body style=\"background: #111; color: #fff; font-family: monospace; padding: 20px;\">",
"<h2>ComfyUI Workflow JSON</h2>",
`<pre style="background: #222; padding: 15px; border-radius: 5px; overflow: auto;">${json}</pre>`,
"</body>",
"</html>",
].join("\n");
}
/**
* Body for POST /prompt_manager/autotag/single.
*
* Database rows (admin review, /images/all) have an integer id the server
* looks up; output-folder entries (gallery review, /images/output) have a
* path digest as id, which would 404, so those are addressed by path.
*
* @param {{id?: unknown, path?: unknown}} image
* @param {{modelType: string, generalThreshold?: number, characterThreshold?: number, prompt?: string}} settings
* @returns {object} JSON-serialisable request body
*/
function autotagSingleBody(image, settings) {
const entry = image || {};
const target =
typeof entry.path === "string" && entry.path && !Number.isInteger(entry.id)
? { path: entry.path }
: { image_id: entry.id };
if ("path" in target && Number.isInteger(entry.root_index)) {
target.root_index = entry.root_index;
}
const modelType = String(settings.modelType || "");
const params = modelType.startsWith("wd14")
? {
general_threshold: settings.generalThreshold,
character_threshold: settings.characterThreshold,
}
: { prompt: settings.prompt };
return { ...target, model_type: modelType, ...params };
}
/**
* Next source to try after an <img> fails. Cards show the thumbnail first;
* when it fails (stale cache, half-written file) the original is worth one
* attempt. Returns null when there is nothing left to try.
* @param {string} currentSrc
* @param {DOMStringMap|object|undefined} dataset `data-original`, `data-fell-back`
*/
function fallbackImageSource(currentSrc, dataset) {
if (!dataset || dataset.fellBack) return null;
const original = dataset.original;
if (!original || original === currentSrc) return null;
return original;
}
function metadataLookupUrl(imageUrl) {
try {
const url = new URL(imageUrl, 'http://prompt-manager.invalid');
const marker = '/prompt_manager/images/serve/';
const index = url.pathname.indexOf(marker);
if (index < 0) return null;
const relativePath = decodeURIComponent(url.pathname.slice(index + marker.length));
const query = new URLSearchParams();
if (url.searchParams.has('root')) query.set('root', url.searchParams.get('root'));
const suffix = query.size ? `?${query}` : '';
return `/prompt_manager/images/prompt/${encodeURIComponent(relativePath)}${suffix}`;
} catch {
return null;
}
}
const api = { formatImageCaption, workflowDocumentHtml, autotagSingleBody, fallbackImageSource, metadataLookupUrl };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.ImageHelpers = api;
})(typeof window !== "undefined" ? window : globalThis);