Files
MajoorWaldi-ComfyUI-Majoor-…/js/components/sidebar/sections/GenerationSection.js
T
MajoorWaldi dc72be0139 feat: enhance JS testing and linting setup
- Added coverage reporting for Vitest with configuration in vitest.config.mjs
- Updated package.json to include new scripts for JS coverage, linting, and formatting
- Modified run_quality_gate.py to allow skipping ESLint and Prettier checks
- Introduced .prettierrc for consistent code formatting
- Created .prettierignore to exclude specific files and directories from formatting
2026-03-28 14:14:28 +01:00

1398 lines
52 KiB
JavaScript

import { createInfoBox, createParametersBox } from "../utils/dom.js";
import { buildViewURL } from "../../../api/endpoints.js";
import { t } from "../../../app/i18n.js";
import { vectorGenerateCaption, vectorGetAlignment } from "../../../api/client.js";
import { loadMajoorSettings } from "../../../app/settings.js";
import {
formatLoRAItem,
formatModelLabel,
normalizeGenerationMetadata,
normalizePromptsForDisplay,
} from "../parsers/geninfoParser.js";
const _IMAGE_EXTENSIONS = new Set([
"png",
"jpg",
"jpeg",
"webp",
"gif",
"bmp",
"tiff",
"tif",
"avif",
"heic",
"heif",
"apng",
"hdr",
"svg",
]);
function _assetExtension(asset) {
const raw = String(asset?.filename || asset?.name || asset?.filepath || asset?.path || "")
.trim()
.toLowerCase();
if (!raw || !raw.includes(".")) return "";
return raw.split(".").pop() || "";
}
function _isImageLikeAsset(asset) {
const kind = String(asset?.kind || "")
.trim()
.toLowerCase();
if (kind === "image") return true;
const mime = String(asset?.mime || asset?.mimetype || "")
.trim()
.toLowerCase();
if (mime.startsWith("image/")) return true;
const ext = _assetExtension(asset);
return _IMAGE_EXTENSIONS.has(ext);
}
function _isJpegAsset(asset) {
const ext = _assetExtension(asset);
return ext === "jpg" || ext === "jpeg";
}
function _getAlignmentColor(score) {
if (score >= 0.75) return "#4CAF50"; // green
if (score >= 0.5) return "#8BC34A"; // light green
if (score >= 0.3) return "#FF9800"; // orange
return "#F44336"; // red
}
function _getAlignmentLabel(score) {
if (score >= 0.85) return "Excellent";
if (score >= 0.7) return "Good";
if (score >= 0.5) return "Fair";
if (score >= 0.3) return "Low";
return "Very Low";
}
function _isAiEnabled() {
try {
const settings = loadMajoorSettings();
return !!(settings?.ai?.vectorSearchEnabled ?? true);
} catch {
return true;
}
}
function _createAlignmentBox(asset, options = {}) {
const showAlignment = options?.showAlignment !== false;
const aiEnabled = _isAiEnabled();
const captionLabelText = String(options?.captionLabel || "Image Description");
const emptyCaptionText = String(options?.emptyCaptionText || "No image description yet.");
const wrapper = document.createElement("div");
wrapper.classList.add("mjr-ai-generation-box");
if (!aiEnabled) {
wrapper.classList.add("mjr-ai-disabled-block");
}
wrapper.style.cssText = `
background: linear-gradient(135deg, rgba(0, 188, 212, 0.14) 0%, rgba(33, 150, 243, 0.10) 100%);
border: 1px solid rgba(0, 188, 212, 0.40);
border-radius: 6px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 10px;
`;
let scoreFill = null;
let scoreLabel = null;
let qualityBadge = null;
let aiStatusHint = null;
if (showAlignment) {
const header = document.createElement("div");
header.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #00BCD4;
text-transform: uppercase;
letter-spacing: 0.5px;
display: flex;
align-items: center;
justify-content: space-between;
`;
const titleSpan = document.createElement("span");
titleSpan.textContent = "Prompt Alignment";
titleSpan.title = "How closely the generated image matches the prompt (SigLIP2 score)";
header.appendChild(titleSpan);
wrapper.appendChild(header);
const scoreRow = document.createElement("div");
scoreRow.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
`;
const scoreBar = document.createElement("div");
scoreBar.style.cssText = `
flex: 1;
height: 8px;
background: rgba(255,255,255,0.1);
border-radius: 4px;
overflow: hidden;
`;
scoreFill = document.createElement("div");
scoreFill.style.cssText = `
height: 100%;
width: 0%;
background: #666;
border-radius: 4px;
transition: width 0.6s ease, background 0.4s ease;
`;
scoreBar.appendChild(scoreFill);
scoreLabel = document.createElement("span");
scoreLabel.textContent = "...";
scoreLabel.style.cssText = `
font-size: 13px;
font-weight: 700;
color: #888;
min-width: 60px;
text-align: right;
font-family: 'Consolas', 'Monaco', monospace;
`;
qualityBadge = document.createElement("span");
qualityBadge.style.cssText = `
font-size: 9px;
font-weight: 700;
padding: 2px 6px;
border-radius: 3px;
background: rgba(127,127,127,0.3);
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
`;
qualityBadge.textContent = "Loading";
scoreRow.appendChild(scoreBar);
scoreRow.appendChild(scoreLabel);
scoreRow.appendChild(qualityBadge);
wrapper.appendChild(scoreRow);
aiStatusHint = document.createElement("div");
aiStatusHint.style.cssText = `
display: none;
font-size: 10px;
color: rgba(255,255,255,0.65);
border: 1px dashed rgba(255,255,255,0.25);
border-radius: 4px;
padding: 6px 8px;
background: rgba(255,255,255,0.04);
`;
aiStatusHint.textContent = "AI features are disabled (enable vector search env var).";
wrapper.appendChild(aiStatusHint);
}
let currentEnhancedCaption = String(asset?.enhanced_caption || "").trim();
if (showAlignment && asset?.id && aiEnabled) {
vectorGetAlignment(asset.id)
.then((res) => {
const serviceUnavailable =
!res?.ok &&
(String(res?.code || "").toUpperCase() === "SERVICE_UNAVAILABLE" ||
/vector search is not enabled/i.test(String(res?.error || "")));
if (serviceUnavailable) {
if (scoreLabel) {
scoreLabel.textContent = "AI OFF";
scoreLabel.style.color = "#9E9E9E";
}
if (qualityBadge) {
qualityBadge.textContent = "Disabled";
qualityBadge.style.background = "rgba(158,158,158,0.25)";
qualityBadge.style.color = "#BDBDBD";
}
if (scoreFill) {
scoreFill.style.width = "0%";
scoreFill.style.background = "#777";
}
if (aiStatusHint) aiStatusHint.style.display = "block";
return;
}
const score = res?.ok && res.data != null ? Number(res.data) : null;
if (score == null || !Number.isFinite(score)) {
if (scoreLabel) scoreLabel.textContent = "N/A";
if (qualityBadge) {
qualityBadge.textContent = "N/A";
qualityBadge.style.background = "rgba(127,127,127,0.3)";
}
if (scoreFill) scoreFill.style.width = "0%";
return;
}
const pct = Math.round(score * 100);
const color = _getAlignmentColor(score);
if (scoreFill) {
scoreFill.style.width = `${pct}%`;
scoreFill.style.background = color;
}
if (scoreLabel) {
scoreLabel.textContent = `${pct}%`;
scoreLabel.style.color = color;
}
if (qualityBadge) {
qualityBadge.textContent = _getAlignmentLabel(score);
qualityBadge.style.background = `${color}33`;
qualityBadge.style.color = color;
}
})
.catch(() => {
if (scoreLabel) scoreLabel.textContent = "-";
if (qualityBadge) qualityBadge.textContent = "Unavailable";
});
}
if (showAlignment && !aiEnabled) {
if (scoreLabel) {
scoreLabel.textContent = "AI OFF";
scoreLabel.style.color = "#9E9E9E";
}
if (qualityBadge) {
qualityBadge.textContent = "Disabled";
qualityBadge.style.background = "rgba(158,158,158,0.25)";
qualityBadge.style.color = "#BDBDBD";
}
if (scoreFill) {
scoreFill.style.width = "0%";
scoreFill.style.background = "#777";
}
if (aiStatusHint) {
aiStatusHint.style.display = "block";
aiStatusHint.textContent = "AI features are disabled in settings.";
}
}
const enhancedCaptionHeader = document.createElement("div");
enhancedCaptionHeader.style.cssText = `
font-size: 10px;
font-weight: 600;
color: rgba(0, 188, 212, 0.75);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
`;
const enhancedCaptionLabel = document.createElement("span");
enhancedCaptionLabel.textContent = captionLabelText;
enhancedCaptionLabel.title = "AI caption generated by Florence-2";
const enhancedActions = document.createElement("div");
enhancedActions.style.cssText = "display:flex;align-items:center;gap:6px;";
const regenerateCaptionBtn = document.createElement("button");
regenerateCaptionBtn.type = "button";
regenerateCaptionBtn.classList.add("mjr-ai-control");
regenerateCaptionBtn.textContent = "Generate";
regenerateCaptionBtn.style.cssText = `
border: 1px solid rgba(0,188,212,0.45);
background: rgba(0,188,212,0.12);
color: #00BCD4;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
cursor: pointer;
`;
const copyCaptionBtn = document.createElement("button");
copyCaptionBtn.type = "button";
copyCaptionBtn.classList.add("mjr-ai-control");
copyCaptionBtn.textContent = "Copy";
copyCaptionBtn.style.cssText = regenerateCaptionBtn.style.cssText;
enhancedActions.appendChild(regenerateCaptionBtn);
enhancedActions.appendChild(copyCaptionBtn);
enhancedCaptionHeader.appendChild(enhancedCaptionLabel);
enhancedCaptionHeader.appendChild(enhancedActions);
const enhancedCaptionBox = document.createElement("div");
enhancedCaptionBox.style.cssText = `
margin-top: 4px;
padding: 8px;
border-radius: 6px;
border: 1px solid rgba(0, 188, 212, 0.30);
background: rgba(0, 188, 212, 0.08);
color: rgba(230, 250, 255, 0.95);
font-size: 11px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
`;
const _normaliseCaptionDisplay = (value) => {
const raw = String(value || "").trim();
if (!raw) return "";
const lines = [];
for (const line of raw.replace(/\r\n/g, "\n").split("\n")) {
let item = String(line || "").trim();
if (!item) continue;
if (/^title\s*:/i.test(item)) continue;
if (/^caption\s*:/i.test(item)) {
item = item.replace(/^caption\s*:/i, "").trim();
}
if (item) lines.push(item);
}
let out = (lines.length ? lines.join(" ") : raw).replace(/\s+/g, " ").trim();
out = out.replace(/:{2,}\s*$/, "").trim();
return out;
};
const renderEnhancedCaption = (value) => {
const txt = _normaliseCaptionDisplay(value);
enhancedCaptionBox.textContent = txt || emptyCaptionText;
};
renderEnhancedCaption(currentEnhancedCaption);
const copyCaption = async () => {
if (!aiEnabled) return;
const text = String(enhancedCaptionBox.textContent || "").trim();
if (!text || /^no (enhanced caption|image description) yet\.?$/i.test(text)) return;
try {
await navigator.clipboard.writeText(text);
const prev = copyCaptionBtn.textContent;
copyCaptionBtn.textContent = "Copied!";
setTimeout(() => {
copyCaptionBtn.textContent = prev;
}, 900);
} catch (err) {
console.debug?.(err);
}
};
copyCaptionBtn.addEventListener("click", (e) => {
e.stopPropagation();
void copyCaption();
});
enhancedCaptionBox.style.cursor = aiEnabled ? "copy" : "default";
enhancedCaptionBox.title = aiEnabled
? "Click to copy caption"
: "AI caption controls are disabled";
enhancedCaptionBox.addEventListener("click", () => {
void copyCaption();
});
const isImageAsset = String(asset?.kind || "").toLowerCase() === "image";
if (!aiEnabled || !isImageAsset || !asset?.id) {
regenerateCaptionBtn.disabled = true;
regenerateCaptionBtn.style.opacity = "0.6";
regenerateCaptionBtn.style.cursor = "default";
copyCaptionBtn.disabled = true;
copyCaptionBtn.style.opacity = "0.6";
copyCaptionBtn.style.cursor = "default";
} else {
regenerateCaptionBtn.addEventListener("click", async (e) => {
e.stopPropagation();
const prevText = regenerateCaptionBtn.textContent;
regenerateCaptionBtn.disabled = true;
regenerateCaptionBtn.textContent = "Generating...";
try {
const res = await vectorGenerateCaption(asset.id);
if (res?.ok) {
currentEnhancedCaption = String(res?.data || "").trim();
renderEnhancedCaption(currentEnhancedCaption);
}
} catch (err) {
console.debug?.(err);
} finally {
regenerateCaptionBtn.disabled = false;
regenerateCaptionBtn.textContent = prevText;
}
});
}
wrapper.appendChild(enhancedCaptionHeader);
wrapper.appendChild(enhancedCaptionBox);
return wrapper;
}
function _inputPreviewCandidates(inp) {
const filename = String(inp?.filename || "").trim();
if (!filename) return [];
const subfolder = String(inp?.subfolder || "").trim();
const preferredType = String(inp?.folder_type || "input")
.trim()
.toLowerCase();
const candidates = [];
const pushType = (t) => {
if (!t) return;
const url = buildViewURL(filename, subfolder, t);
if (url && !candidates.includes(url)) candidates.push(url);
};
if (preferredType === "input" || preferredType === "output") pushType(preferredType);
pushType("input");
pushType("output");
return candidates;
}
function _setPreviewSrcWithFallback(el, candidates) {
if (!el || !Array.isArray(candidates) || !candidates.length) return;
let idx = 0;
const apply = () => {
if (idx >= candidates.length) return;
const next = candidates[idx++];
if (!next) return apply();
el.src = next;
};
el.addEventListener("error", () => apply(), { once: false });
apply();
}
function _isSafeOpenUrl(url) {
const value = String(url || "").trim();
if (!value) return false;
if (value.startsWith("/")) return true;
try {
const parsed = new URL(value);
return parsed.protocol === "http:" || parsed.protocol === "https:";
} catch {
return false;
}
}
function _formatPipelineValue(value) {
if (value === undefined || value === null || value === "") return "-";
return String(value);
}
function _resolvePassName(passData, index) {
const explicitName = String(passData?.pass_name || "").trim();
if (explicitName) return explicitName;
const denoise = Number(passData?.denoise);
if (index === 0 || denoise === 1) return "Base";
if (Number.isFinite(denoise) && denoise < 1) return "Refine / Upscale";
return `Pass ${index + 1}`;
}
function _enableCopyOnClick(el, value) {
if (!el) return;
const text = String(value ?? "").trim();
if (!text || text === "-") return;
el.style.cursor = "copy";
el.title = `Click to copy: ${text}`;
el.addEventListener("click", async (evt) => {
evt.stopPropagation();
try {
await navigator.clipboard.writeText(text);
const prevBg = el.style.background;
el.style.background = "rgba(76, 175, 80, 0.35)";
setTimeout(() => {
el.style.background = prevBg || "transparent";
}, 450);
} catch (err) {
console.debug?.(err);
}
});
}
function _applyTabHighlightStyle(btn, isActive, colorHex) {
if (!btn) return;
btn.style.background = isActive ? `${colorHex}33` : "rgba(127,127,127,0.12)";
btn.style.borderColor = isActive ? colorHex : "var(--border-color, rgba(255,255,255,0.12))";
btn.style.color = isActive ? colorHex : "var(--fg-color, #ddd)";
btn.style.fontWeight = isActive ? "700" : "500";
btn.style.boxShadow = isActive ? `0 0 0 1px ${colorHex}55 inset` : "none";
}
function _createPromptTabsBox(title, positives, negatives, colorHex) {
const pos = Array.isArray(positives)
? positives.filter((p) => typeof p === "string" && p.trim())
: [];
if (!pos.length) return null;
const neg = Array.isArray(negatives) ? negatives : [];
const box = document.createElement("div");
box.style.cssText = `
background: linear-gradient(135deg, rgba(76, 175, 80, 0.16) 0%, rgba(33, 150, 243, 0.10) 100%);
border: 1px solid rgba(76, 175, 80, 0.45);
box-shadow: 0 0 0 1px rgba(76, 175, 80, 0.15) inset;
border-left: 3px solid ${colorHex};
border-radius: 6px;
padding: 12px;
margin-top: 4px;
`;
const header = document.createElement("div");
header.textContent = title;
header.style.cssText = `
font-size: 11px;
font-weight: 600;
color: ${colorHex};
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
`;
box.appendChild(header);
const tabs = document.createElement("div");
tabs.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;";
const panels = document.createElement("div");
panels.style.cssText = "display: flex; flex-direction: column;";
const tabButtons = [];
const tabPanels = [];
const activateTab = (activeIndex) => {
tabButtons.forEach((btn, idx) => {
const isActive = idx === activeIndex;
_applyTabHighlightStyle(btn, isActive, colorHex);
});
tabPanels.forEach((panel, idx) => {
panel.style.display = idx === activeIndex ? "flex" : "none";
});
};
pos.forEach((positiveText, index) => {
const tab = document.createElement("button");
tab.type = "button";
tab.textContent = `Prompt ${index + 1}`;
tab.style.cssText = `
appearance: none;
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
border-radius: 999px;
background: rgba(127,127,127,0.12);
color: var(--fg-color, #ddd);
font-size: 11px;
padding: 4px 10px;
cursor: pointer;
`;
tab.addEventListener("click", () => activateTab(index));
tabs.appendChild(tab);
tabButtons.push(tab);
const panel = document.createElement("div");
panel.style.cssText = `
display: none;
flex-direction: column;
gap: 8px;
border: 1px solid rgba(76, 175, 80, 0.35);
border-radius: 6px;
background: linear-gradient(135deg, rgba(76, 175, 80, 0.12) 0%, rgba(33, 150, 243, 0.08) 100%);
box-shadow: 0 0 0 1px rgba(76, 175, 80, 0.12) inset;
padding: 10px;
`;
const pLabel = document.createElement("div");
pLabel.textContent = "POSITIVE";
pLabel.style.cssText =
"font-size: 10px; font-weight: 700; color: #4CAF50; letter-spacing: 0.4px;";
const pText = document.createElement("div");
pText.textContent = positiveText;
pText.style.cssText =
"font-size: 12px; color: var(--fg-color, #ddd); white-space: pre-wrap; line-height: 1.35;";
_enableCopyOnClick(pText, positiveText);
panel.appendChild(pLabel);
panel.appendChild(pText);
const negText = typeof neg[index] === "string" ? neg[index].trim() : "";
if (negText) {
const nLabel = document.createElement("div");
nLabel.textContent = "NEGATIVE";
nLabel.style.cssText =
"font-size: 10px; font-weight: 700; color: #F44336; letter-spacing: 0.4px; margin-top: 4px;";
const nText = document.createElement("div");
nText.textContent = negText;
nText.style.cssText =
"font-size: 12px; color: var(--fg-color, #ddd); white-space: pre-wrap; line-height: 1.35;";
_enableCopyOnClick(nText, negText);
panel.appendChild(nLabel);
panel.appendChild(nText);
}
panels.appendChild(panel);
tabPanels.push(panel);
});
box.appendChild(tabs);
box.appendChild(panels);
activateTab(0);
return box;
}
function _createTabsBox(title, passesArray, colorHex) {
const safePasses = Array.isArray(passesArray)
? passesArray.filter((p) => p && typeof p === "object")
: [];
if (!safePasses.length) return null;
const box = document.createElement("div");
box.style.cssText = `
background: linear-gradient(135deg, rgba(255, 152, 0, 0.16) 0%, rgba(255, 193, 7, 0.10) 100%);
border: 1px solid rgba(255, 152, 0, 0.45);
box-shadow: 0 0 0 1px rgba(255, 152, 0, 0.15) inset;
border-left: 3px solid ${colorHex};
border-radius: 6px;
padding: 12px;
margin-top: 4px;
`;
const header = document.createElement("div");
header.textContent = title;
header.style.cssText = `
font-size: 11px;
font-weight: 600;
color: ${colorHex};
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
`;
box.appendChild(header);
const tabs = document.createElement("div");
tabs.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
`;
const panels = document.createElement("div");
panels.style.cssText = "display: flex; flex-direction: column;";
const tabButtons = [];
const tabPanels = [];
const activateTab = (activeIndex) => {
tabButtons.forEach((btn, idx) => {
const isActive = idx === activeIndex;
_applyTabHighlightStyle(btn, isActive, colorHex);
btn.setAttribute("aria-selected", String(isActive));
btn.tabIndex = isActive ? 0 : -1;
});
tabPanels.forEach((panel, idx) => {
panel.style.display = idx === activeIndex ? "grid" : "none";
});
};
safePasses.forEach((passData, index) => {
const passName = _resolvePassName(passData, index);
const tab = document.createElement("button");
tab.type = "button";
tab.textContent = passName;
tab.title = `Generation pass ${index + 1}`;
tab.style.cssText = `
appearance: none;
border: 1px solid var(--border-color, rgba(255,255,255,0.12));
border-radius: 999px;
background: rgba(127,127,127,0.12);
color: var(--fg-color, #ddd);
font-size: 11px;
padding: 4px 10px;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
`;
tab.setAttribute("role", "tab");
tab.setAttribute("aria-label", passName);
tab.addEventListener("click", () => activateTab(index));
tabs.appendChild(tab);
tabButtons.push(tab);
const panel = document.createElement("div");
panel.setAttribute("role", "tabpanel");
panel.style.cssText = `
display: none;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 8px;
padding: 8px;
border: 1px solid rgba(255, 152, 0, 0.35);
border-radius: 6px;
background: linear-gradient(135deg, rgba(255, 152, 0, 0.12) 0%, rgba(255, 193, 7, 0.08) 100%);
box-shadow: 0 0 0 1px rgba(255, 152, 0, 0.12) inset;
`;
const fields = [
{ label: "Sampler", value: passData?.sampler_name || passData?.sampler },
{ label: "Scheduler", value: passData?.scheduler },
{ label: "Steps", value: passData?.steps },
{ label: "CFG", value: passData?.cfg },
{ label: "Denoise", value: passData?.denoise },
{ label: "Seed", value: passData?.seed_val || passData?.seed },
];
fields.forEach((field) => {
const item = document.createElement("div");
item.style.cssText = "display: flex; flex-direction: column; gap: 2px; min-width: 0;";
const itemLabel = document.createElement("span");
itemLabel.textContent = field.label;
itemLabel.style.cssText = `
font-size: 10px;
font-weight: 600;
color: rgba(255,255,255,0.6);
text-transform: uppercase;
letter-spacing: 0.4px;
`;
const itemValue = document.createElement("span");
const formatted = _formatPipelineValue(field.value);
itemValue.textContent = formatted;
itemValue.style.cssText =
"font-size: 12px; color: var(--fg-color, #ddd); word-break: break-word; padding: 1px 3px; border-radius: 3px; transition: background 0.2s ease;";
_enableCopyOnClick(itemValue, formatted);
item.appendChild(itemLabel);
item.appendChild(itemValue);
panel.appendChild(item);
});
panels.appendChild(panel);
tabPanels.push(panel);
});
box.appendChild(tabs);
box.appendChild(panels);
activateTab(0);
return box;
}
export function createGenerationSection(asset) {
let metadata = null;
// Prefer backend-parsed geninfo when available (most reliable for pos/neg + model chain).
if (asset?.geninfo && typeof asset.geninfo === "object") {
metadata = { geninfo: asset.geninfo };
} else if (
asset?.metadata &&
(typeof asset.metadata === "object" || typeof asset.metadata === "string")
) {
metadata = asset.metadata;
} else if (
asset?.prompt &&
(typeof asset.prompt === "object" || typeof asset.prompt === "string")
) {
metadata = asset.prompt;
} else if (asset?.metadata_raw) {
metadata = asset.metadata_raw;
} else if (asset?.exif) {
metadata = asset.exif;
}
const normalized = normalizeGenerationMetadata(metadata);
const hasDisplayableFields = (obj) => {
try {
if (!obj || typeof obj !== "object") return false;
// Workflow Type check
if (obj.engine && typeof obj.engine === "object" && obj.engine.type) return true;
if (typeof obj.prompt === "string" && obj.prompt.trim()) return true;
if (
typeof (obj.negative_prompt || obj.negativePrompt) === "string" &&
(obj.negative_prompt || obj.negativePrompt).trim()
)
return true;
if (obj.models || obj.model || obj.checkpoint || obj.loras) return true;
if (
obj.sampler ||
obj.sampler_name ||
obj.steps ||
obj.cfg ||
obj.cfg_scale ||
obj.scheduler
)
return true;
if (Array.isArray(obj.chained_passes) && obj.chained_passes.length > 0) return true;
if (Array.isArray(obj.all_samplers) && obj.all_samplers.length > 0) return true;
if (obj.seed || obj.denoise || obj.denoising || obj.clip_skip) return true;
if (
obj.voice ||
obj.language ||
obj.temperature ||
obj.top_k ||
obj.top_p ||
obj.repetition_penalty ||
obj.max_new_tokens
)
return true;
if (
obj.device ||
obj.voice_preset ||
obj.instruct ||
obj.dtype ||
obj.attn_implementation
)
return true;
if (
obj.enable_chunking !== undefined ||
obj.max_chars_per_chunk ||
obj.chunk_combination_method ||
obj.silence_between_chunks_ms
)
return true;
if (
obj.enable_audio_cache !== undefined ||
obj.batch_size !== undefined ||
obj.use_torch_compile !== undefined ||
obj.use_cuda_graphs !== undefined ||
obj.compile_mode
)
return true;
if (typeof obj.lyrics === "string" && obj.lyrics.trim()) return true;
return false;
} catch {
return false;
}
};
if (
!normalized ||
(typeof normalized === "object" && Object.keys(normalized).length === 0) ||
!hasDisplayableFields(normalized)
) {
const status = asset?.metadata_raw?.geninfo_status || asset?.geninfo_status;
if (status && typeof status === "object" && status.kind === "media_pipeline") {
const container = document.createElement("div");
container.appendChild(
createInfoBox(
"Generation Data",
"This file looks like a media-only pipeline (e.g. LoadVideo/VideoCombine) and does not contain generation parameters.",
"#9E9E9E",
),
);
return container;
}
// If nothing generation-related is displayable, keep an image description block visible
// for image assets (especially JPEGs that often have no workflow data).
const shouldShowImageDescription = _isImageLikeAsset(asset) || _isJpegAsset(asset);
if (shouldShowImageDescription) {
const container = document.createElement("div");
container.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
`;
container.appendChild(
_createAlignmentBox(asset, {
showAlignment: false,
captionLabel: "Image Description",
emptyCaptionText: "No image description yet.",
}),
);
return container;
}
return null;
}
metadata = normalized;
const container = document.createElement("div");
container.style.cssText = `
display: flex;
flex-direction: column;
gap: 12px;
`;
// Workflow Type Header (top of panel, visually emphasized)
if (metadata.engine && metadata.engine.type) {
const typeBox = document.createElement("div");
typeBox.style.cssText = `
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background: linear-gradient(135deg, rgba(33, 150, 243, 0.18) 0%, rgba(0, 188, 212, 0.10) 100%);
border-left: 3px solid #2196f3;
border: 1px solid rgba(33, 150, 243, 0.45);
box-shadow: 0 0 0 1px rgba(33, 150, 243, 0.15) inset;
border-radius: 6px;
font-size: 11px;
color: var(--fg-color, #ccc);
`;
const badge = document.createElement("span");
badge.textContent = metadata.engine.type;
badge.title = `Workflow engine: ${metadata.engine.type}`;
badge.style.cssText = `
background: #2196f3;
color: white;
padding: 2px 6px;
border-radius: 3px;
font-weight: bold;
font-size: 10px;
`;
const label = document.createElement("span");
label.textContent = "Workflow Type";
label.style.opacity = "0.85";
typeBox.appendChild(label);
typeBox.appendChild(badge);
container.appendChild(typeBox);
}
// Check for truncated flag (backend sets this if metadata > limit)
const isTruncated =
asset?.geninfo?._truncated || asset?.metadata?._truncated || asset?.prompt?._truncated;
if (isTruncated) {
container.appendChild(
createInfoBox(
"Metadata Truncated",
"Generation data is incomplete because it exceeded the size limit.",
"#FF9800",
),
);
}
const cleaned = normalizePromptsForDisplay(
typeof metadata.prompt === "string" ? metadata.prompt : null,
typeof (metadata.negative_prompt || metadata.negativePrompt) === "string"
? metadata.negative_prompt || metadata.negativePrompt
: null,
);
const hasPromptTabs =
metadata.all_positive_prompts &&
Array.isArray(metadata.all_positive_prompts) &&
metadata.all_positive_prompts.length > 1;
if (!hasPromptTabs && typeof cleaned.positive === "string" && cleaned.positive.trim()) {
const positiveBox = createInfoBox("Positive Prompt", cleaned.positive, "#4CAF50", {
showCopyButton: false,
copyOnContentClick: true,
});
positiveBox.style.background =
"linear-gradient(135deg, rgba(76, 175, 80, 0.16) 0%, rgba(33, 150, 243, 0.10) 100%)";
positiveBox.style.borderLeft = "3px solid #4CAF50";
positiveBox.style.border = "1px solid rgba(76, 175, 80, 0.45)";
positiveBox.style.boxShadow = "0 0 0 1px rgba(76, 175, 80, 0.15) inset";
container.appendChild(positiveBox);
}
if (!hasPromptTabs && typeof cleaned.negative === "string" && cleaned.negative.trim()) {
const negativeBox = createInfoBox("Negative Prompt", cleaned.negative, "#F44336", {
showCopyButton: false,
copyOnContentClick: true,
});
negativeBox.style.background =
"linear-gradient(135deg, rgba(244, 67, 54, 0.16) 0%, rgba(255, 152, 0, 0.10) 100%)";
negativeBox.style.borderLeft = "3px solid #F44336";
negativeBox.style.border = "1px solid rgba(244, 67, 54, 0.45)";
negativeBox.style.boxShadow = "0 0 0 1px rgba(244, 67, 54, 0.15) inset";
container.appendChild(negativeBox);
}
// ── Prompt Alignment + Caption ────────────────────────────────────
if (asset?.id) {
const alignmentBox = _createAlignmentBox(asset);
container.appendChild(alignmentBox);
}
if (typeof metadata.lyrics === "string" && metadata.lyrics.trim()) {
container.appendChild(createInfoBox("Lyrics", metadata.lyrics, "#00BCD4"));
}
// Multi-output workflows: Show all distinct prompts
if (hasPromptTabs) {
const promptTabs = _createPromptTabsBox(
`Prompt Pipeline (${metadata.all_positive_prompts.length} variants)`,
metadata.all_positive_prompts,
metadata.all_negative_prompts,
"#4CAF50",
);
if (promptTabs) {
container.appendChild(promptTabs);
}
}
const modelData = [];
const models = metadata.models && typeof metadata.models === "object" ? metadata.models : null;
const pickModelName = (m) => {
if (!m) return "";
if (typeof m === "string") return formatModelLabel(m);
if (typeof m === "object") return formatModelLabel(m.name || m.value || "");
return "";
};
if (models) {
const allCkpts =
Array.isArray(metadata.all_checkpoints) && metadata.all_checkpoints.length > 1
? metadata.all_checkpoints
: null;
if (allCkpts) {
allCkpts.forEach((c, i) => {
const name = pickModelName(c);
if (name) modelData.push({ label: `Checkpoint ${i + 1}`, value: name });
});
} else {
const ckpt = pickModelName(models.checkpoint);
if (ckpt) modelData.push({ label: "Checkpoint", value: ckpt });
}
const unet = pickModelName(models.unet);
const diffusion = pickModelName(models.diffusion);
const upscaler = pickModelName(models.upscaler);
const clip = pickModelName(models.clip);
const vae = pickModelName(models.vae);
if (unet) modelData.push({ label: "UNet", value: unet });
if (diffusion) modelData.push({ label: "Diffusion", value: diffusion });
if (upscaler) modelData.push({ label: "Upscaler", value: upscaler });
if (clip) modelData.push({ label: "CLIP", value: clip });
if (vae) modelData.push({ label: "VAE", value: vae });
} else if (metadata.model || metadata.checkpoint) {
modelData.push({
label: "Model",
value: formatModelLabel(metadata.model || metadata.checkpoint),
});
}
if (metadata.loras && Array.isArray(metadata.loras) && metadata.loras.length > 0) {
const loraText = metadata.loras
.map((lora) => formatLoRAItem(lora))
.filter(Boolean)
.join("\n");
if (loraText) modelData.push({ label: "LoRA", value: loraText });
}
if (!models && metadata.clip) {
modelData.push({ label: "CLIP", value: formatModelLabel(metadata.clip) });
}
if (!models && metadata.vae) {
modelData.push({ label: "VAE", value: formatModelLabel(metadata.vae) });
}
if (!models && metadata.unet) {
modelData.push({ label: "UNet", value: formatModelLabel(metadata.unet) });
}
if (!models && metadata.diffusion) {
modelData.push({ label: "Diffusion", value: formatModelLabel(metadata.diffusion) });
}
if (modelData.length > 0) {
container.appendChild(
createParametersBox("Model & LoRA", modelData, "#9C27B0", { emphasis: true }),
);
}
const samplingData = [];
if (metadata.sampler || metadata.sampler_name) {
samplingData.push({ label: "Sampler", value: metadata.sampler || metadata.sampler_name });
}
if (metadata.steps) samplingData.push({ label: "Steps", value: metadata.steps });
if (metadata.cfg || metadata.cfg_scale)
samplingData.push({ label: "CFG Scale", value: metadata.cfg || metadata.cfg_scale });
if (metadata.scheduler) samplingData.push({ label: "Scheduler", value: metadata.scheduler });
const hasChainedPipeline =
metadata.chained_passes &&
Array.isArray(metadata.chained_passes) &&
metadata.chained_passes.length > 1;
const hasAllSamplersPipeline =
metadata.all_samplers &&
Array.isArray(metadata.all_samplers) &&
metadata.all_samplers.length > 1;
const hasPipelineTabs = !!(hasChainedPipeline || hasAllSamplersPipeline);
if (hasChainedPipeline) {
const tabsBox = _createTabsBox("Generation Pipeline", metadata.chained_passes, "#FF9800");
if (tabsBox) {
container.appendChild(tabsBox);
}
} else if (hasAllSamplersPipeline) {
const samplerPipeline = _createTabsBox(
"Generation Pipeline",
metadata.all_samplers,
"#FF9800",
);
if (samplerPipeline) {
container.appendChild(samplerPipeline);
}
}
if (!hasPipelineTabs && samplingData.length > 0) {
container.appendChild(
createParametersBox("Sampling", samplingData, "#FF9800", { emphasis: true }),
);
}
const isTTS = String(metadata?.engine?.type || "").toLowerCase() === "tts";
const ttsData = [];
if (metadata.voice) ttsData.push({ label: "Narrator Voice", value: metadata.voice });
if (metadata.language) ttsData.push({ label: "Language", value: metadata.language });
if (metadata.top_k !== undefined && metadata.top_k !== null)
ttsData.push({ label: "Top-k", value: metadata.top_k });
if (metadata.top_p !== undefined && metadata.top_p !== null)
ttsData.push({ label: "Top-p", value: metadata.top_p });
if (metadata.temperature !== undefined && metadata.temperature !== null)
ttsData.push({ label: "Temperature", value: metadata.temperature });
if (metadata.repetition_penalty !== undefined && metadata.repetition_penalty !== null)
ttsData.push({ label: "Repetition Penalty", value: metadata.repetition_penalty });
if (metadata.max_new_tokens !== undefined && metadata.max_new_tokens !== null)
ttsData.push({ label: "Max New Tokens", value: metadata.max_new_tokens });
if (ttsData.length > 0 || isTTS) {
container.appendChild(createParametersBox("TTS", ttsData, "#26A69A", { emphasis: true }));
}
const ttsEngineData = [];
if (metadata.device) ttsEngineData.push({ label: "Device", value: metadata.device });
if (metadata.voice_preset)
ttsEngineData.push({ label: "Voice Preset", value: metadata.voice_preset });
if (metadata.dtype) ttsEngineData.push({ label: "Dtype", value: metadata.dtype });
if (metadata.attn_implementation)
ttsEngineData.push({ label: "Attention", value: metadata.attn_implementation });
if (metadata.compile_mode)
ttsEngineData.push({ label: "Compile Mode", value: metadata.compile_mode });
if (metadata.use_torch_compile !== undefined && metadata.use_torch_compile !== null)
ttsEngineData.push({
label: "Torch Compile",
value: metadata.use_torch_compile ? "on" : "off",
});
if (metadata.use_cuda_graphs !== undefined && metadata.use_cuda_graphs !== null)
ttsEngineData.push({
label: "CUDA Graphs",
value: metadata.use_cuda_graphs ? "on" : "off",
});
if (metadata.x_vector_only_mode !== undefined && metadata.x_vector_only_mode !== null)
ttsEngineData.push({
label: "X-Vector Only",
value: metadata.x_vector_only_mode ? "on" : "off",
});
if (ttsEngineData.length > 0) {
container.appendChild(createParametersBox("TTS Engine", ttsEngineData, "#00897B"));
}
if (typeof metadata.instruct === "string" && metadata.instruct.trim()) {
container.appendChild(
createInfoBox("TTS Instruction", metadata.instruct, "#26A69A", {
showCopyButton: false,
copyOnContentClick: true,
}),
);
}
const ttsChunkingData = [];
if (metadata.enable_chunking !== undefined && metadata.enable_chunking !== null)
ttsChunkingData.push({ label: "Chunking", value: metadata.enable_chunking ? "on" : "off" });
if (metadata.max_chars_per_chunk !== undefined && metadata.max_chars_per_chunk !== null)
ttsChunkingData.push({ label: "Max Chars/Chunk", value: metadata.max_chars_per_chunk });
if (metadata.chunk_combination_method)
ttsChunkingData.push({ label: "Chunk Method", value: metadata.chunk_combination_method });
if (
metadata.silence_between_chunks_ms !== undefined &&
metadata.silence_between_chunks_ms !== null
)
ttsChunkingData.push({
label: "Silence Between Chunks (ms)",
value: metadata.silence_between_chunks_ms,
});
if (metadata.enable_audio_cache !== undefined && metadata.enable_audio_cache !== null)
ttsChunkingData.push({
label: "Audio Cache",
value: metadata.enable_audio_cache ? "on" : "off",
});
if (metadata.batch_size !== undefined && metadata.batch_size !== null)
ttsChunkingData.push({ label: "Batch Size", value: metadata.batch_size });
if (ttsChunkingData.length > 0) {
container.appendChild(createParametersBox("TTS Runtime", ttsChunkingData, "#00796B"));
}
if (metadata.lyrics_strength !== undefined && metadata.lyrics_strength !== null) {
container.appendChild(
createParametersBox(
"Audio",
[{ label: "Lyrics Strength", value: metadata.lyrics_strength }],
"#00BCD4",
),
);
}
// SEED - Highlighted prominently for easy comparison in A/B and side-by-side modes
if (metadata.seed) {
const seedBox = document.createElement("div");
seedBox.style.cssText = `
background: linear-gradient(135deg, rgba(233, 30, 99, 0.15) 0%, rgba(156, 39, 176, 0.15) 100%);
border: 2px solid #E91E63;
border-radius: 8px;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
`;
const seedLabel = document.createElement("div");
seedLabel.textContent = "SEED";
seedLabel.style.cssText = `
font-size: 11px;
font-weight: 700;
color: #E91E63;
text-transform: uppercase;
letter-spacing: 1px;
`;
const seedValue = document.createElement("div");
seedValue.textContent = String(metadata.seed);
seedValue.title = `Click to copy seed: ${metadata.seed}`;
seedValue.style.cssText = `
font-size: 18px;
font-weight: 700;
color: #fff;
font-family: 'Consolas', 'Monaco', monospace;
letter-spacing: 1px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
`;
seedValue.addEventListener("mouseenter", () => {
seedValue.style.background = "rgba(233, 30, 99, 0.3)";
});
seedValue.addEventListener("mouseleave", () => {
seedValue.style.background = "transparent";
});
seedValue.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(String(metadata.seed));
seedValue.style.background = "rgba(76, 175, 80, 0.4)";
setTimeout(() => {
seedValue.style.background = "transparent";
}, 500);
} catch (e) {
console.debug?.(e);
}
});
seedBox.appendChild(seedLabel);
seedBox.appendChild(seedValue);
container.appendChild(seedBox);
}
const imageData = [];
if (metadata.denoise || metadata.denoising)
imageData.push({ label: "Denoise", value: metadata.denoise || metadata.denoising });
if (metadata.clip_skip) imageData.push({ label: "Clip Skip", value: metadata.clip_skip });
if (imageData.length > 0) {
container.appendChild(createParametersBox("Image", imageData, "#2196F3"));
}
// Input Files Section
if (metadata.inputs && Array.isArray(metadata.inputs) && metadata.inputs.length > 0) {
const inputBox = document.createElement("div");
inputBox.style.cssText = `
background: linear-gradient(135deg, rgba(76, 175, 80, 0.16) 0%, rgba(33, 150, 243, 0.10) 100%);
border-left: 3px solid #4CAF50;
border: 1px solid rgba(76, 175, 80, 0.45);
box-shadow: 0 0 0 1px rgba(76, 175, 80, 0.15) inset;
border-radius: 6px;
padding: 12px;
margin-top: 10px;
`;
const header = document.createElement("div");
header.textContent = "Source Files";
header.title = t("tooltip.generationInputs", "Input files used in generation");
header.style.cssText = `
font-size: 11px;
font-weight: 600;
color: #4CAF50;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
`;
inputBox.appendChild(header);
const grid = document.createElement("div");
grid.style.cssText = "display: flex; gap: 8px; flex-wrap: wrap;";
metadata.inputs.forEach((inp) => {
const thumb = document.createElement("div");
thumb.style.cssText =
"width: 64px; height: 64px; background: #222; border-radius: 4px; overflow: hidden; position: relative; cursor: pointer; display: flex; align-items: center; justify-content: center;";
thumb.title = `${inp.filename} (click to copy, double-click to open in new tab)`;
const srcCandidates = _inputPreviewCandidates(inp);
const isVideo = inp.type === "video" || inp.filename.match(/\.(mp4|mov|webm)$/i);
if (isVideo) {
const vid = document.createElement("video");
_setPreviewSrcWithFallback(vid, srcCandidates);
vid.muted = true;
vid.loop = true;
vid.autoplay = false;
vid.playsInline = true;
vid.preload = "metadata";
vid.addEventListener("mouseover", () => vid.play().catch(() => {}));
vid.addEventListener("mouseout", () => vid.pause());
vid.style.cssText = "width: 100%; height: 100%; object-fit: cover;";
thumb.appendChild(vid);
} else {
const img = document.createElement("img");
_setPreviewSrcWithFallback(img, srcCandidates);
img.style.cssText = "width: 100%; height: 100%; object-fit: cover;";
thumb.appendChild(img);
}
// Role Badge (New)
if (inp.role && inp.role !== "secondary") {
const roleBadge = document.createElement("div");
roleBadge.textContent = inp.role.replace("_", " ");
roleBadge.style.cssText = `
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
font-size: 8px;
padding: 2px;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
thumb.appendChild(roleBadge);
}
// Play icon overlay for video if no role badge or just on top
if (isVideo && !inp.role) {
const icon = document.createElement("div");
icon.innerHTML = "▶";
icon.title = t("tooltip.videoFile", "Video file");
icon.style.cssText =
"position: absolute; color: white; opacity: 0.7; font-size: 16px; pointer-events: none;";
thumb.appendChild(icon);
}
thumb.onclick = async (e) => {
e.stopPropagation();
try {
const copyValue = String(inp?.filepath || inp?.filename || "").trim();
if (!copyValue) return;
await navigator.clipboard.writeText(copyValue);
thumb.style.outline = "2px solid rgba(76, 175, 80, 0.9)";
thumb.style.outlineOffset = "1px";
setTimeout(() => {
thumb.style.outline = "";
thumb.style.outlineOffset = "";
}, 350);
} catch (e) {
console.debug?.(e);
}
};
thumb.ondblclick = (e) => {
e.stopPropagation();
const src =
Array.isArray(srcCandidates) && srcCandidates.length
? String(srcCandidates[0] || "")
: "";
if (!_isSafeOpenUrl(src)) return;
window.open(src, "_blank", "noopener,noreferrer");
};
grid.appendChild(thumb);
});
inputBox.appendChild(grid);
container.appendChild(inputBox);
}
return container.children.length > 0 ? container : null;
}