Pages made at different times used their own greys (slate here, neutral there) and accents (sky, violet, indigo), and most ignored the Abyssal Scarlet theme. About 1,850 literal colours in web/styles and in CSS and inline styles written from JS now use the --amb-* tokens by what they are for: surfaces, text (new --amb-text-soft and --amb-text-dim), borders (new --amb-border-strong), the primary button, and one accent (new --amb-link, tinted with color-mix). Both themes define the new tokens. Literal colours stay where the colour is the meaning: status, prompt roles, voice emotions, canvas node colours, shadows and overlays; theme override rules are untouched. The share-code dialogs move from inline styles to .anomalous-share-* classes. Stylesheet versions are bumped. Checked: text contrast on every rail page, Prompt Studio, Prompt Notes, Model Sources, settings, model detail and editor, share codes, in both themes; structure check and JS tests pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
691 lines
31 KiB
JavaScript
691 lines
31 KiB
JavaScript
/**
|
|
* ui_detail.js
|
|
* Extracted Detail Panel methods.
|
|
*/
|
|
|
|
import { app } from "../../../scripts/app.js";
|
|
import { translate } from './locales.js';
|
|
import { escapeHtml, setSafeRichHtml } from './safe_dom.js';
|
|
import { renderSourceLine } from './model_source.js';
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
function createModelLocationCard(model) {
|
|
const card = document.createElement('div');
|
|
card.className = 'anomalous-model-location';
|
|
|
|
const appendValue = (labelKey, value) => {
|
|
const row = document.createElement('div');
|
|
row.className = 'anomalous-model-location-row';
|
|
const label = document.createElement('span');
|
|
label.className = 'anomalous-model-location-label';
|
|
label.textContent = t(labelKey);
|
|
const content = document.createElement('code');
|
|
content.textContent = value;
|
|
content.title = value;
|
|
row.append(label, content);
|
|
card.appendChild(row);
|
|
};
|
|
|
|
appendValue('detailPhysicalFilename', model.filename);
|
|
if (model.file_path) {
|
|
appendValue('detailFullPath', model.file_path);
|
|
const copyButton = document.createElement('button');
|
|
copyButton.type = 'button';
|
|
copyButton.className = 'anomalous-model-location-copy';
|
|
copyButton.textContent = t('detailCopyPath');
|
|
copyButton.onclick = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(model.file_path);
|
|
copyButton.textContent = t('detailPathCopied');
|
|
} catch (error) {
|
|
copyButton.textContent = t('detailPathCopyFailed');
|
|
}
|
|
setTimeout(() => { copyButton.textContent = t('detailCopyPath'); }, 1400);
|
|
};
|
|
card.appendChild(copyButton);
|
|
}
|
|
return card;
|
|
}
|
|
|
|
|
|
|
|
export function showDetail(model) {
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
if (this.detailMouseMoveHandler) window.removeEventListener('mousemove', this.detailMouseMoveHandler);
|
|
if (this.detailMouseUpHandler) window.removeEventListener('mouseup', this.detailMouseUpHandler);
|
|
if (!this.recipeModelReturn && this.grid && this.grid.style.display !== 'none') {
|
|
this.gridReturnState = {
|
|
type: this.currentType,
|
|
pathIdx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
scrollTop: this.grid.scrollTop,
|
|
scrollLeft: this.grid.scrollLeft,
|
|
};
|
|
}
|
|
this.grid.style.display = 'none';
|
|
this.detailPanel.style.display = 'flex';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
|
|
const header = document.createElement('div');
|
|
header.style.width = '100%';
|
|
header.style.padding = '8px 15px';
|
|
header.style.background = 'var(--comfy-menu-bg, var(--amb-bg-card-hover))';
|
|
header.style.borderBottom = '1px solid var(--border-color, var(--amb-border-strong))';
|
|
header.style.display = 'flex';
|
|
header.style.alignItems = 'center';
|
|
header.style.boxSizing = 'border-box';
|
|
|
|
const backBtn = document.createElement('button');
|
|
let isFromDoctor = false;
|
|
let isFromAssistant = false;
|
|
if (this.historyStack.length > 0) {
|
|
const lastHistory = this.historyStack[this.historyStack.length - 1];
|
|
if (lastHistory.type === 'doctor') {
|
|
isFromDoctor = true;
|
|
} else if (lastHistory.type === 'assistant') {
|
|
isFromAssistant = true;
|
|
}
|
|
}
|
|
|
|
if (isFromDoctor) {
|
|
backBtn.textContent = t('detailBackDoctor');
|
|
backBtn.style.background = '#e5e7eb';
|
|
backBtn.style.color = '#111827';
|
|
} else if (isFromAssistant) {
|
|
backBtn.textContent = t('detailBackAssistant');
|
|
backBtn.style.background = '#e5e7eb';
|
|
backBtn.style.color = '#111827';
|
|
} else {
|
|
backBtn.innerHTML = this.historyStack.length > 0 ? t('backToPrev') : t('back');
|
|
backBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
backBtn.style.color = '#fff';
|
|
}
|
|
backBtn.style.padding = '6px 12px';
|
|
backBtn.style.border = 'none';
|
|
backBtn.style.borderRadius = '4px';
|
|
backBtn.style.cursor = 'pointer';
|
|
backBtn.onclick = () => {
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
if (typeof this.recipeModelReturn === 'function') {
|
|
const returnToRecipe = this.recipeModelReturn;
|
|
this.recipeModelReturn = null;
|
|
returnToRecipe();
|
|
return;
|
|
}
|
|
if (this.historyStack.length > 0) {
|
|
const prev = this.historyStack.pop();
|
|
if (prev.type === 'assistant') {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.assistantPanel.style.display = 'flex';
|
|
const container = document.getElementById('anomalous-container');
|
|
if (container) container.classList.add('anomalous-sidebar-closed');
|
|
return;
|
|
}
|
|
this.currentType = prev.type;
|
|
this.currentPathIdx = prev.pathIdx;
|
|
this.currentSubfolder = prev.subfolder;
|
|
this.currentDetailModel = prev.model;
|
|
this.renderSidebar();
|
|
this.showDetail(prev.model);
|
|
} else {
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.grid.style.display = 'grid';
|
|
const gridReturnState = this.gridReturnState;
|
|
this.gridReturnState = null;
|
|
if (gridReturnState
|
|
&& gridReturnState.type === this.currentType
|
|
&& gridReturnState.pathIdx === this.currentPathIdx
|
|
&& gridReturnState.subfolder === this.currentSubfolder) {
|
|
requestAnimationFrame(() => {
|
|
this.grid.scrollTop = gridReturnState.scrollTop || 0;
|
|
this.grid.scrollLeft = gridReturnState.scrollLeft || 0;
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const title = document.createElement('h2');
|
|
title.textContent = model.filename;
|
|
title.style.margin = '0 20px 0 20px';
|
|
title.style.color = '#fff';
|
|
title.style.fontSize = '1.2em';
|
|
// 强制单行并溢出显示省略号
|
|
title.style.whiteSpace = 'nowrap';
|
|
title.style.overflow = 'hidden';
|
|
title.style.textOverflow = 'ellipsis';
|
|
title.style.flex = '1'; // 撑开剩余空间,把右侧按钮挤到最右边
|
|
|
|
const delBtn = document.createElement('button');
|
|
delBtn.innerHTML = t('delModel');
|
|
delBtn.style.padding = '6px 12px';
|
|
delBtn.style.background = '#ff4444';
|
|
delBtn.style.color = '#fff';
|
|
delBtn.style.border = 'none';
|
|
delBtn.style.borderRadius = '4px';
|
|
delBtn.style.cursor = 'pointer';
|
|
delBtn.style.marginLeft = 'auto'; // push to the right
|
|
delBtn.style.whiteSpace = 'nowrap';
|
|
delBtn.onclick = async () => {
|
|
if (!confirm(`${t('delConfirm')} ${model.filename} ${t('delConfirm2')}`)) return;
|
|
try {
|
|
const res = await fetch('/anomalous/delete_model', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
type: this.currentType,
|
|
path_idx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
filename: model.filename
|
|
})
|
|
});
|
|
const data = await res.json();
|
|
if (data.status === 'success') {
|
|
if (this.currentDetailObserver) {
|
|
this.currentDetailObserver.disconnect();
|
|
this.currentDetailObserver = null;
|
|
}
|
|
alert(t('delSuccess') + data.deleted.join('\n') + t('delNote'));
|
|
this.detailPanel.style.display = 'none';
|
|
this.stopMediaInContainer(this.detailPanel); this.detailPanel.innerHTML = '';
|
|
this.grid.style.display = 'grid';
|
|
this.loadModels(); // refresh grid
|
|
} else {
|
|
alert(t('delFail') + data.message);
|
|
}
|
|
} catch (e) {
|
|
alert(t('delFail') + e.message);
|
|
}
|
|
};
|
|
|
|
const jumpBtn = document.createElement('button');
|
|
jumpBtn.innerHTML = '⬇️';
|
|
jumpBtn.title = t('detailJumpToBottom');
|
|
jumpBtn.style.padding = '6px 12px';
|
|
jumpBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
jumpBtn.style.color = '#fff';
|
|
jumpBtn.style.border = 'none';
|
|
jumpBtn.style.borderRadius = '4px';
|
|
jumpBtn.style.cursor = 'pointer';
|
|
jumpBtn.style.marginLeft = '10px';
|
|
jumpBtn.onclick = () => {
|
|
// Find rightPanel which is created later, so we bind it dynamically
|
|
const rp = this.detailPanel.querySelector('.anomalous-split-right');
|
|
if (rp) rp.scrollTo({ top: rp.scrollHeight, behavior: 'smooth' });
|
|
};
|
|
|
|
header.appendChild(backBtn);
|
|
header.appendChild(title);
|
|
header.appendChild(delBtn);
|
|
header.appendChild(jumpBtn);
|
|
|
|
const applyDetailBtn = document.createElement('button');
|
|
applyDetailBtn.textContent = t('applyToCanvas');
|
|
applyDetailBtn.style.padding = '6px 12px';
|
|
applyDetailBtn.style.background = '#007bff';
|
|
applyDetailBtn.style.color = '#fff';
|
|
applyDetailBtn.style.border = 'none';
|
|
applyDetailBtn.style.borderRadius = '4px';
|
|
applyDetailBtn.style.cursor = 'pointer';
|
|
applyDetailBtn.style.marginLeft = '10px';
|
|
applyDetailBtn.style.fontWeight = 'bold';
|
|
applyDetailBtn.onclick = () => {
|
|
this.applyModelToCanvas(this.currentType, this.currentSubfolder, model);
|
|
};
|
|
header.appendChild(applyDetailBtn);
|
|
|
|
|
|
const splitContainer = document.createElement('div');
|
|
splitContainer.className = 'anomalous-split-container';
|
|
|
|
const leftPanel = document.createElement('div');
|
|
leftPanel.className = 'anomalous-split-left';
|
|
|
|
let isMediaRendered = null;
|
|
const renderMedia = (shouldRender) => {
|
|
if (shouldRender === isMediaRendered) return;
|
|
isMediaRendered = shouldRender;
|
|
leftPanel.innerHTML = '';
|
|
if (!shouldRender) return;
|
|
|
|
if (model.preview_url) {
|
|
const isVideo = model.preview_url.match(/\.mp4(?:&|$)/i) || model.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVideo) {
|
|
const video = document.createElement('video');
|
|
video.src = model.preview_url;
|
|
video.controls = true;
|
|
video.autoplay = true;
|
|
video.loop = true;
|
|
video.style.width = '100%';
|
|
video.style.height = '100%';
|
|
video.style.objectFit = 'contain';
|
|
leftPanel.appendChild(video);
|
|
} else {
|
|
const img = document.createElement('img');
|
|
img.src = model.preview_url;
|
|
img.style.width = '100%';
|
|
img.style.height = '100%';
|
|
img.style.objectFit = 'contain';
|
|
leftPanel.appendChild(img);
|
|
}
|
|
} else {
|
|
leftPanel.innerHTML = `<div style="color:var(--amb-text-muted); text-align:center; margin-top:50px;">${t('noPreview')}</div>`;
|
|
}
|
|
};
|
|
|
|
const containerEl = document.getElementById('anomalous-container');
|
|
this.currentDetailObserver = new ResizeObserver(entries => {
|
|
for (let entry of entries) {
|
|
renderMedia(entry.contentRect.width >= 750);
|
|
}
|
|
});
|
|
this.currentDetailObserver.observe(containerEl);
|
|
|
|
const resizer = document.createElement('div');
|
|
resizer.className = 'anomalous-resizer';
|
|
|
|
let isResizing = false;
|
|
resizer.addEventListener('mousedown', (e) => {
|
|
isResizing = true;
|
|
document.body.style.cursor = 'col-resize';
|
|
e.preventDefault();
|
|
});
|
|
|
|
this.detailMouseMoveHandler = (e) => {
|
|
if (!isResizing) return;
|
|
const containerRect = splitContainer.getBoundingClientRect();
|
|
let newWidth = ((e.clientX - containerRect.left) / containerRect.width) * 100;
|
|
if (newWidth < 20) newWidth = 20;
|
|
if (newWidth > 80) newWidth = 80;
|
|
leftPanel.style.width = newWidth + '%';
|
|
rightPanel.style.width = (100 - newWidth) + '%';
|
|
};
|
|
window.addEventListener('mousemove', this.detailMouseMoveHandler);
|
|
|
|
this.detailMouseUpHandler = () => {
|
|
if (isResizing) {
|
|
isResizing = false;
|
|
document.body.style.cursor = '';
|
|
}
|
|
};
|
|
window.addEventListener('mouseup', this.detailMouseUpHandler);
|
|
|
|
const rightPanel = document.createElement('div');
|
|
rightPanel.className = 'anomalous-split-right';
|
|
rightPanel.style.display = 'flex';
|
|
rightPanel.style.flexDirection = 'column';
|
|
rightPanel.style.height = '100%';
|
|
rightPanel.style.boxSizing = 'border-box';
|
|
rightPanel.style.overflow = 'auto';
|
|
rightPanel.style.padding = '15px';
|
|
rightPanel.style.color = '#eee';
|
|
|
|
const m = model.metadata || {};
|
|
|
|
// 1. Top Bar (Title + Size + Model + Button) in a single compact row if possible
|
|
const topRow = document.createElement('div');
|
|
topRow.style.flexShrink = '0';
|
|
topRow.style.display = 'flex';
|
|
topRow.style.flexWrap = 'wrap';
|
|
topRow.style.alignItems = 'center';
|
|
topRow.style.gap = '10px';
|
|
topRow.style.paddingBottom = '10px';
|
|
topRow.style.marginBottom = '10px';
|
|
topRow.style.borderBottom = '1px solid var(--amb-border-strong)';
|
|
|
|
const titleEl = document.createElement('h3');
|
|
titleEl.style.margin = '0';
|
|
titleEl.style.fontSize = '1.3em';
|
|
titleEl.style.marginRight = '10px';
|
|
titleEl.innerText = m.custom_name || m.name || model.filename;
|
|
topRow.appendChild(titleEl);
|
|
|
|
const metaSpan = document.createElement('span');
|
|
metaSpan.style.fontSize = '0.9em';
|
|
metaSpan.style.color = '#aaa';
|
|
renderSourceLine(metaSpan, model, m);
|
|
topRow.appendChild(metaSpan);
|
|
|
|
if (m.civitai_url) {
|
|
const cBtn = document.createElement('a');
|
|
cBtn.href = m.civitai_url;
|
|
cBtn.target = '_blank';
|
|
cBtn.rel = 'noopener noreferrer';
|
|
cBtn.innerHTML = '🌐 Civitai';
|
|
cBtn.style.marginLeft = 'auto';
|
|
cBtn.style.padding = '4px 8px';
|
|
cBtn.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
cBtn.style.border = '1px solid var(--amb-border-strong)';
|
|
cBtn.style.color = '#fff';
|
|
cBtn.style.textDecoration = 'none';
|
|
cBtn.style.borderRadius = '4px';
|
|
cBtn.style.fontSize = '0.85em';
|
|
cBtn.style.fontWeight = 'bold';
|
|
topRow.appendChild(cBtn);
|
|
}
|
|
|
|
const editMetaBtn = document.createElement('button');
|
|
editMetaBtn.textContent = t('detailEdit');
|
|
editMetaBtn.style.marginLeft = m.civitai_url ? '10px' : 'auto';
|
|
editMetaBtn.style.padding = '4px 8px';
|
|
editMetaBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
editMetaBtn.style.color = '#fff';
|
|
editMetaBtn.style.border = 'none';
|
|
editMetaBtn.style.borderRadius = '4px';
|
|
editMetaBtn.style.fontSize = '0.85em';
|
|
editMetaBtn.style.fontWeight = 'bold';
|
|
editMetaBtn.style.cursor = 'pointer';
|
|
editMetaBtn.onclick = () => {
|
|
this.showEditModal(model);
|
|
};
|
|
topRow.appendChild(editMetaBtn);
|
|
|
|
rightPanel.appendChild(topRow);
|
|
rightPanel.appendChild(createModelLocationCard(model));
|
|
|
|
// 1.5 Custom Notes Section (Google Material Card)
|
|
if (m.custom_notes) {
|
|
const notesCard = document.createElement('div');
|
|
notesCard.style.flexShrink = '0';
|
|
notesCard.style.marginBottom = '15px';
|
|
notesCard.style.padding = '12px 16px';
|
|
// Dark yellowish/khaki paper background for dark mode notebook feel
|
|
notesCard.style.background = 'linear-gradient(135deg, var(--amb-bg-card-hover) 0%, var(--amb-bg-card) 100%)';
|
|
notesCard.style.border = '1px solid var(--amb-border-strong)';
|
|
notesCard.style.borderLeft = '4px solid #a38d53';
|
|
notesCard.style.borderRadius = '4px 8px 8px 4px';
|
|
notesCard.style.boxShadow = '0 4px 12px rgba(0,0,0,0.2)';
|
|
notesCard.style.position = 'relative';
|
|
notesCard.style.fontFamily = 'Inter, Roboto, sans-serif';
|
|
// Faint notebook lines background
|
|
notesCard.style.backgroundImage = 'repeating-linear-gradient(transparent, transparent 23px, rgba(163, 141, 83, 0.04) 23px, rgba(163, 141, 83, 0.04) 24px)';
|
|
notesCard.style.backgroundAttachment = 'local'; // ensures lines scroll with text
|
|
|
|
const notesHeader = document.createElement('div');
|
|
notesHeader.style.display = 'flex';
|
|
notesHeader.style.justifyContent = 'space-between';
|
|
notesHeader.style.alignItems = 'center';
|
|
notesHeader.style.marginBottom = '8px';
|
|
|
|
const notesTitle = document.createElement('div');
|
|
notesTitle.textContent = t('detailNotesTitle');
|
|
notesTitle.style.color = '#a38d53';
|
|
notesTitle.style.fontWeight = '600';
|
|
notesTitle.style.fontSize = '0.85em';
|
|
notesTitle.style.letterSpacing = '0.5px';
|
|
|
|
const notesEditBtn = document.createElement('button');
|
|
notesEditBtn.innerHTML = '<svg style="width:13px;height:13px;vertical-align:middle;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>';
|
|
notesEditBtn.title = t('detailEditNotes');
|
|
notesEditBtn.style.background = 'transparent';
|
|
notesEditBtn.style.border = 'none';
|
|
notesEditBtn.style.color = '#a38d53';
|
|
notesEditBtn.style.cursor = 'pointer';
|
|
notesEditBtn.style.padding = '2px';
|
|
notesEditBtn.style.fontSize = '1em';
|
|
notesEditBtn.style.borderRadius = '50%';
|
|
notesEditBtn.style.display = 'flex';
|
|
notesEditBtn.style.alignItems = 'center';
|
|
notesEditBtn.style.justifyContent = 'center';
|
|
notesEditBtn.style.opacity = '0.7';
|
|
notesEditBtn.onmouseover = () => notesEditBtn.style.opacity = '1';
|
|
notesEditBtn.onmouseout = () => notesEditBtn.style.opacity = '0.7';
|
|
notesEditBtn.onclick = () => {
|
|
this.showEditModal(model);
|
|
};
|
|
|
|
notesHeader.appendChild(notesTitle);
|
|
notesHeader.appendChild(notesEditBtn);
|
|
|
|
const notesContent = document.createElement('div');
|
|
notesContent.innerText = m.custom_notes;
|
|
notesContent.style.color = '#d1c9b4'; // Warm off-white
|
|
|
|
notesContent.style.fontSize = '0.95em';
|
|
notesContent.style.lineHeight = '24px'; // Matches the repeating gradient exactly
|
|
notesContent.style.whiteSpace = 'pre-wrap';
|
|
notesContent.style.fontFamily = '"Consolas", "Courier New", monospace'; // Handwriting / typewriter feel
|
|
// Removed text shadow for cleaner look
|
|
|
|
notesCard.appendChild(notesHeader);
|
|
notesCard.appendChild(notesContent);
|
|
|
|
rightPanel.appendChild(notesCard);
|
|
}
|
|
// 1.8 Generated Gallery Button
|
|
const galleryBtnCont = document.createElement('div');
|
|
galleryBtnCont.style.flexShrink = '0';
|
|
galleryBtnCont.style.marginBottom = '15px';
|
|
|
|
const galleryBtn = document.createElement('button');
|
|
galleryBtn.className = 'anomalous-nb-add-btn';
|
|
galleryBtn.style.width = '100%';
|
|
galleryBtn.style.display = 'flex';
|
|
galleryBtn.style.justifyContent = 'center';
|
|
galleryBtn.style.alignItems = 'center';
|
|
galleryBtn.style.gap = '8px';
|
|
galleryBtn.style.padding = '10px';
|
|
galleryBtn.style.background = 'var(--amb-bg-card-hover)';
|
|
galleryBtn.style.border = '1px solid var(--amb-border-strong)';
|
|
galleryBtn.textContent = t('detailGeneratedGallery');
|
|
galleryBtn.onmouseover = () => { galleryBtn.style.background = 'var(--amb-bg-card-hover)'; galleryBtn.style.borderColor = 'var(--amb-border-strong)'; };
|
|
galleryBtn.onmouseout = () => { galleryBtn.style.background = 'var(--amb-bg-card-hover)'; galleryBtn.style.borderColor = 'var(--amb-border-strong)'; };
|
|
|
|
galleryBtn.onclick = () => {
|
|
this.showGeneratedGallery(model);
|
|
};
|
|
galleryBtnCont.appendChild(galleryBtn);
|
|
rightPanel.appendChild(galleryBtnCont);
|
|
|
|
// 2. Trained Words Section
|
|
if (m.trainedWords && m.trainedWords.length > 0) {
|
|
const twCont = document.createElement('div');
|
|
twCont.style.flexShrink = '0';
|
|
twCont.style.marginBottom = '10px';
|
|
|
|
const twHeader = document.createElement('div');
|
|
twHeader.style.display = 'flex';
|
|
twHeader.style.alignItems = 'center';
|
|
twHeader.style.marginBottom = '5px';
|
|
|
|
const twLabel = document.createElement('strong');
|
|
twLabel.textContent = t('detailTrainedWords');
|
|
twHeader.appendChild(twLabel);
|
|
|
|
const copyAll = document.createElement('button');
|
|
copyAll.innerText = t('copyAll');
|
|
copyAll.style.marginLeft = '10px';
|
|
copyAll.style.padding = '2px 6px';
|
|
copyAll.style.background = 'var(--amb-bg-card-hover)';
|
|
copyAll.style.color = '#fff';
|
|
copyAll.style.border = 'none';
|
|
copyAll.style.borderRadius = '3px';
|
|
copyAll.style.cursor = 'pointer';
|
|
copyAll.style.fontSize = '0.8em';
|
|
copyAll.onclick = () => {
|
|
const allWords = m.trainedWords.join(', ');
|
|
navigator.clipboard.writeText(allWords).then(() => {
|
|
const old = copyAll.innerText;
|
|
copyAll.innerText = t('copied');
|
|
setTimeout(() => { copyAll.innerText = old; }, 1500);
|
|
});
|
|
};
|
|
twHeader.appendChild(copyAll);
|
|
twCont.appendChild(twHeader);
|
|
|
|
const tagsCont = document.createElement('div');
|
|
tagsCont.style.display = 'flex';
|
|
tagsCont.style.flexWrap = 'wrap';
|
|
tagsCont.style.gap = '4px';
|
|
|
|
m.trainedWords.forEach(w => {
|
|
const tag = document.createElement('span');
|
|
tag.innerText = w;
|
|
tag.style.background = 'var(--amb-bg-card-hover)';
|
|
tag.style.padding = '2px 6px';
|
|
tag.style.borderRadius = '4px';
|
|
tag.style.fontSize = '0.85em';
|
|
tag.style.cursor = 'pointer';
|
|
tag.style.border = '1px solid var(--amb-border-strong)';
|
|
tag.title = t('clickToCopy') + w;
|
|
tag.onclick = () => {
|
|
navigator.clipboard.writeText(w).then(() => {
|
|
tag.style.background = '#28a745';
|
|
setTimeout(() => { tag.style.background = 'var(--amb-bg-card-hover)'; }, 500);
|
|
});
|
|
};
|
|
tagsCont.appendChild(tag);
|
|
});
|
|
twCont.appendChild(tagsCont);
|
|
rightPanel.appendChild(twCont);
|
|
}
|
|
|
|
// 3. Description Section (Expands to fill remaining height)
|
|
if (m.description) {
|
|
const descCont = document.createElement('div');
|
|
descCont.style.flex = 'none';
|
|
descCont.style.display = 'flex';
|
|
descCont.style.flexDirection = 'column';
|
|
// important for flex scroll
|
|
|
|
const descLabel = document.createElement('strong');
|
|
descLabel.textContent = t('detailDescription');
|
|
descLabel.style.marginBottom = '5px';
|
|
descCont.appendChild(descLabel);
|
|
|
|
const descText = document.createElement('div');
|
|
descText.style.flex = 'none';
|
|
|
|
descText.style.background = 'var(--amb-bg-card)';
|
|
descText.style.padding = '10px';
|
|
descText.style.borderRadius = '6px';
|
|
descText.style.border = '1px solid var(--amb-border-strong)';
|
|
descText.style.fontSize = '0.95em';
|
|
descText.style.lineHeight = '1.4';
|
|
setSafeRichHtml(descText, m.description);
|
|
descCont.appendChild(descText);
|
|
|
|
rightPanel.appendChild(descCont);
|
|
}
|
|
|
|
// 4. Notes Section
|
|
if (m.notes) {
|
|
const notesCont = document.createElement('div');
|
|
notesCont.style.flexShrink = '0';
|
|
notesCont.style.marginTop = '10px';
|
|
|
|
const notesLabel = document.createElement('strong');
|
|
notesLabel.textContent = t('detailNotes');
|
|
notesCont.appendChild(notesLabel);
|
|
|
|
const notesText = document.createElement('div');
|
|
notesText.style.background = '#332b00';
|
|
notesText.style.padding = '8px';
|
|
notesText.style.borderRadius = '6px';
|
|
notesText.style.marginTop = '5px';
|
|
notesText.style.border = '1px solid #554400';
|
|
notesText.style.fontSize = '0.9em';
|
|
setSafeRichHtml(notesText, m.notes);
|
|
notesCont.appendChild(notesText);
|
|
|
|
rightPanel.appendChild(notesCont);
|
|
}
|
|
|
|
// --- Compatible Models Section ---
|
|
if (m.baseModel) {
|
|
const compSec = document.createElement('div');
|
|
compSec.className = 'anomalous-compatible-section';
|
|
|
|
const compTitle = document.createElement('div');
|
|
compTitle.className = 'anomalous-compatible-title';
|
|
compTitle.innerHTML = `${t('compatibleModels') || '🔗 Compatible'} <span style="font-size:0.8em; opacity:0.6;">(${escapeHtml(m.baseModel)})</span>`;
|
|
|
|
const compList = document.createElement('div');
|
|
compList.className = 'anomalous-compatible-list';
|
|
compList.innerHTML = `<span style="color:var(--amb-text-muted);">${t('loadingCompatible') || 'Loading...'}</span>`;
|
|
|
|
compSec.appendChild(compTitle);
|
|
compSec.appendChild(compList);
|
|
rightPanel.appendChild(compSec);
|
|
|
|
const targetType = this.currentType === 'loras' ? 'checkpoints,unet,diffusion_models' : 'loras';
|
|
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(m.baseModel)}&target_type=${encodeURIComponent(targetType)}`)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
compList.innerHTML = '';
|
|
if (window.anomalous_update_hash_cache && d.models) {
|
|
window.anomalous_update_hash_cache(d.models);
|
|
}
|
|
if (!d.models || d.models.length === 0) {
|
|
const noCompatible = document.createElement('span');
|
|
noCompatible.style.color = '#888';
|
|
noCompatible.textContent = t('detailNoCompatibleModels');
|
|
compList.replaceChildren(noCompatible);
|
|
return;
|
|
}
|
|
d.models.forEach(m_comp => {
|
|
const mItem = document.createElement('div');
|
|
mItem.className = 'anomalous-compatible-item';
|
|
mItem.title = m_comp.filename;
|
|
|
|
let thumb = '';
|
|
if (m_comp.preview_url) {
|
|
const isVid = m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i);
|
|
if (isVid) thumb = `<video src="${m_comp.preview_url}" muted loop playsinline></video>`;
|
|
else thumb = `<img src="${m_comp.preview_url}" />`;
|
|
} else {
|
|
thumb = `<div style="width:30px; height:30px; background:var(--amb-bg-card); border-radius:4px; display:flex; align-items:center; justify-content:center; font-size:10px; color:var(--amb-text-dim);">?</div>`;
|
|
}
|
|
|
|
mItem.innerHTML = `${thumb}<div class="anomalous-compatible-item-name">${escapeHtml(m_comp.filename)}</div>`;
|
|
|
|
if (m_comp.preview_url && (m_comp.preview_url.match(/\.mp4(?:&|$)/i) || m_comp.preview_url.match(/\.webm(?:&|$)/i))) {
|
|
mItem.onmouseenter = () => { const v = mItem.querySelector('video'); if (v) v.play().catch(e => { }); };
|
|
mItem.onmouseleave = () => { const v = mItem.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
|
|
}
|
|
|
|
mItem.onclick = () => {
|
|
this.historyStack.push({
|
|
type: this.currentType,
|
|
pathIdx: this.currentPathIdx,
|
|
subfolder: this.currentSubfolder,
|
|
model: this.currentDetailModel
|
|
});
|
|
|
|
this.currentType = m_comp.type;
|
|
this.currentPathIdx = m_comp.path_idx;
|
|
this.currentSubfolder = m_comp.subfolder;
|
|
this.currentDetailModel = m_comp;
|
|
|
|
this.renderSidebar();
|
|
this.showDetail(m_comp);
|
|
};
|
|
|
|
compList.appendChild(mItem);
|
|
});
|
|
})
|
|
.catch(e => {
|
|
const compatibleError = document.createElement('span');
|
|
compatibleError.style.color = '#ff4444';
|
|
compatibleError.textContent = t('detailCompatibleLoadFailed');
|
|
compList.replaceChildren(compatibleError);
|
|
});
|
|
}
|
|
// ---------------------------------
|
|
|
|
splitContainer.appendChild(leftPanel);
|
|
splitContainer.appendChild(resizer);
|
|
splitContainer.appendChild(rightPanel);
|
|
|
|
this.detailPanel.appendChild(header);
|
|
this.detailPanel.appendChild(splitContainer);
|
|
}
|