- 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
1398 lines
52 KiB
JavaScript
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;
|
|
}
|