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>
767 lines
32 KiB
JavaScript
767 lines
32 KiB
JavaScript
/**
|
|
* Advanced model selector and model-widget path helpers.
|
|
*/
|
|
|
|
import { translate } from './locales.js';
|
|
import { escapeHtml } from './safe_dom.js';
|
|
|
|
const t = (key, params) => translate(key, params);
|
|
|
|
export function _openAdvancedModelSelector(initialSelectedMap, onConfirm) {
|
|
let localSelection = new Map();
|
|
for (const [k, v] of initialSelectedMap.entries()) {
|
|
localSelection.set(k, new Set(v));
|
|
}
|
|
|
|
const getFolderSelectionCount = (folderKey) => {
|
|
let nKey = folderKey;
|
|
const parts = folderKey.split('|');
|
|
if (parts.length >= 3 && parts[2].startsWith('/')) {
|
|
parts[2] = parts[2].substring(1);
|
|
nKey = parts.join('|');
|
|
}
|
|
return localSelection.has(nKey) ? localSelection.get(nKey).size : 0;
|
|
};
|
|
|
|
const modal = document.createElement('div');
|
|
modal.style.position = 'fixed';
|
|
modal.style.top = '0';
|
|
modal.style.left = '0';
|
|
modal.style.width = '100vw';
|
|
modal.style.height = '100vh';
|
|
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
|
|
modal.style.zIndex = '9999999';
|
|
modal.style.display = 'flex';
|
|
modal.style.justifyContent = 'center';
|
|
modal.style.alignItems = 'center';
|
|
modal.style.fontFamily = 'Roboto, Inter, sans-serif';
|
|
|
|
const contentDiv = document.createElement('div');
|
|
contentDiv.style.background = 'var(--amb-bg-card)';
|
|
contentDiv.style.borderRadius = '8px';
|
|
contentDiv.style.width = '92vw';
|
|
contentDiv.style.height = '88vh';
|
|
contentDiv.style.display = 'flex';
|
|
contentDiv.style.flexDirection = 'column';
|
|
contentDiv.style.boxShadow = '0 24px 38px 3px rgba(0,0,0,0.4), 0 9px 46px 8px rgba(0,0,0,0.3), 0 11px 15px -7px rgba(0,0,0,0.5)';
|
|
contentDiv.style.overflow = 'hidden';
|
|
|
|
// Header
|
|
const header = document.createElement('div');
|
|
header.style.padding = '16px 24px';
|
|
header.style.borderBottom = '1px solid var(--amb-border-strong)';
|
|
header.style.display = 'flex';
|
|
header.style.justifyContent = 'space-between';
|
|
header.style.alignItems = 'center';
|
|
|
|
const titleContainer = document.createElement('div');
|
|
titleContainer.style.display = 'flex';
|
|
titleContainer.style.alignItems = 'center';
|
|
titleContainer.style.gap = '12px';
|
|
|
|
const title = document.createElement('h2');
|
|
title.textContent = t('detailSelectModels');
|
|
title.style.margin = '0';
|
|
title.style.color = '#e8eaed';
|
|
title.style.fontSize = '20px';
|
|
title.style.fontWeight = '500';
|
|
|
|
titleContainer.appendChild(title);
|
|
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.innerHTML = '✕';
|
|
closeBtn.style.background = 'transparent';
|
|
closeBtn.style.color = '#9aa0a6';
|
|
closeBtn.style.border = 'none';
|
|
closeBtn.style.fontSize = '18px';
|
|
closeBtn.style.cursor = 'pointer';
|
|
closeBtn.onclick = () => document.body.removeChild(modal);
|
|
|
|
header.appendChild(titleContainer);
|
|
header.appendChild(closeBtn);
|
|
contentDiv.appendChild(header);
|
|
|
|
// Body area
|
|
const body = document.createElement('div');
|
|
body.style.display = 'flex';
|
|
body.style.flex = '1';
|
|
body.style.overflow = 'hidden';
|
|
|
|
// Left Panel
|
|
const leftPanel = document.createElement('div');
|
|
leftPanel.style.width = '280px';
|
|
leftPanel.style.borderRight = '1px solid var(--amb-border-strong)';
|
|
leftPanel.style.display = 'flex';
|
|
leftPanel.style.flexDirection = 'column';
|
|
leftPanel.style.background = 'var(--amb-bg-panel)';
|
|
|
|
const leftTitle = document.createElement('div');
|
|
leftTitle.textContent = t('detailFolders');
|
|
leftTitle.style.padding = '16px 24px';
|
|
leftTitle.style.fontWeight = '500';
|
|
leftTitle.style.color = '#9aa0a6';
|
|
leftTitle.style.fontSize = '14px';
|
|
leftTitle.style.letterSpacing = '0.5px';
|
|
leftPanel.appendChild(leftTitle);
|
|
|
|
const treeContainer = document.createElement('div');
|
|
treeContainer.style.flex = '1';
|
|
treeContainer.style.overflowY = 'auto';
|
|
treeContainer.style.padding = '0 12px 12px 12px';
|
|
leftPanel.appendChild(treeContainer);
|
|
|
|
// Right Panel
|
|
const rightPanel = document.createElement('div');
|
|
rightPanel.style.flex = '1';
|
|
rightPanel.style.display = 'flex';
|
|
rightPanel.style.flexDirection = 'column';
|
|
rightPanel.style.background = 'var(--amb-bg-card)';
|
|
|
|
// Toolbar
|
|
const toolbar = document.createElement('div');
|
|
toolbar.style.padding = '12px 24px';
|
|
toolbar.style.borderBottom = '1px solid var(--amb-border-strong)';
|
|
toolbar.style.display = 'flex';
|
|
toolbar.style.gap = '12px';
|
|
toolbar.style.alignItems = 'center';
|
|
toolbar.style.flexWrap = 'wrap';
|
|
|
|
const createTBtn = (text, onClick, primary = false) => {
|
|
const b = document.createElement('button');
|
|
b.innerText = text;
|
|
b.style.padding = '6px 16px';
|
|
b.style.background = primary ? 'rgba(255, 255, 255, 0.12)' : 'rgba(255,255,255,0.04)';
|
|
b.style.color = primary ? '#ffffff' : '#e8eaed';
|
|
b.style.border = primary ? '1px solid rgba(255, 255, 255, 0.25)' : '1px solid #5f6368';
|
|
b.style.borderRadius = '4px';
|
|
b.style.cursor = 'pointer';
|
|
b.style.fontSize = '14px';
|
|
b.style.fontWeight = '500';
|
|
b.style.transition = 'background-color 0.15s, box-shadow 0.15s';
|
|
b.onmouseover = () => {
|
|
b.style.background = primary ? 'rgba(138,180,248,0.25)' : 'rgba(255,255,255,0.1)';
|
|
};
|
|
b.onmouseout = () => {
|
|
b.style.background = primary ? 'rgba(138,180,248,0.15)' : 'rgba(255,255,255,0.04)';
|
|
};
|
|
b.onclick = onClick;
|
|
return b;
|
|
};
|
|
|
|
let currentModels = [];
|
|
let currentTotal = 0;
|
|
let currentFolderKey = 'ALL';
|
|
let currentBaseUrl = '';
|
|
let currentPage = 1;
|
|
let isLoading = false;
|
|
|
|
const updateSummaryAndBadges = () => {
|
|
let total = 0;
|
|
for (const set of localSelection.values()) total += set.size;
|
|
|
|
if (total > 0) {
|
|
const countLabel = document.createElement('span');
|
|
countLabel.textContent = total;
|
|
countLabel.style.color = '#e5e7eb';
|
|
countLabel.style.fontSize = '16px';
|
|
countLabel.style.fontWeight = '500';
|
|
summaryText.replaceChildren(
|
|
document.createTextNode(t('detailSelectedItemsPrefix')),
|
|
countLabel,
|
|
document.createTextNode(t('detailSelectedItemsSuffix')),
|
|
);
|
|
} else {
|
|
summaryText.textContent = t('detailNothingSelected');
|
|
}
|
|
|
|
treeContainer.querySelectorAll('.folder-item').forEach(fBtn => {
|
|
const fKey = fBtn.dataset.folderKey;
|
|
const count = getFolderSelectionCount(fKey);
|
|
const badge = fBtn.querySelector('.selection-badge');
|
|
if (badge) {
|
|
if (count > 0) {
|
|
badge.innerText = count;
|
|
badge.style.display = 'inline-flex';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
});
|
|
|
|
// Also update total models text in grid
|
|
const statusDiv = grid.querySelector('.status-text');
|
|
if (statusDiv && !isLoading) {
|
|
statusDiv.textContent = t('detailLoadedModels', { loaded: currentModels.length, total: currentTotal });
|
|
}
|
|
};
|
|
|
|
const updateGridCheckboxes = () => {
|
|
grid.querySelectorAll('.model-card').forEach(card => {
|
|
const filename = card.dataset.filename;
|
|
const mType = card.dataset.mtype;
|
|
const mPathIdx = card.dataset.mpathidx;
|
|
const mSubfolder = card.dataset.msubfolder;
|
|
|
|
const fKey = `${mType}|${mPathIdx}|${mSubfolder}`;
|
|
const set = localSelection.get(fKey) || new Set();
|
|
const isChecked = set.has(filename);
|
|
|
|
const cbWrapper = card.querySelector('.cb-wrapper');
|
|
const cb = card.querySelector('input[type="checkbox"]');
|
|
cb.checked = isChecked;
|
|
|
|
if (isChecked) {
|
|
|
|
|
|
card.style.border = '2px solid #ffffff';
|
|
|
|
|
|
card.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
|
|
|
|
cbWrapper.style.background = '#ffffff';
|
|
|
|
|
|
cbWrapper.style.border = '2px solid #ffffff';
|
|
|
|
|
|
cbWrapper.querySelector('span').style.color = '#202124';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
card.style.border = '1px solid var(--amb-border-strong)';
|
|
|
|
|
|
card.style.background = 'var(--amb-bg-card-hover)';
|
|
|
|
|
|
cbWrapper.style.background = 'rgba(0,0,0,0.3)';
|
|
|
|
|
|
cbWrapper.style.border = '2px solid #5f6368';
|
|
|
|
|
|
cbWrapper.querySelector('span').style.color = 'transparent';
|
|
|
|
|
|
}
|
|
});
|
|
updateSummaryAndBadges();
|
|
};
|
|
|
|
const handleBatchSelect = async (action) => {
|
|
if (action === 'none') {
|
|
if (currentFolderKey === 'ALL') {
|
|
localSelection.clear();
|
|
} else {
|
|
let nCurKey = currentFolderKey;
|
|
const cParts = currentFolderKey.split('|');
|
|
if (cParts.length >= 3 && cParts[2].startsWith('/')) {
|
|
cParts[2] = cParts[2].substring(1);
|
|
nCurKey = cParts.join('|');
|
|
}
|
|
localSelection.delete(nCurKey);
|
|
}
|
|
updateGridCheckboxes();
|
|
return;
|
|
}
|
|
|
|
summaryText.textContent = t('detailComputing');
|
|
try {
|
|
const res = await fetch(`/anomalous/batch_select?folderKey=${encodeURIComponent(currentFolderKey)}&action=${action}`);
|
|
if (!res.ok) throw new Error('API failed');
|
|
const data = await res.json();
|
|
|
|
for (const [fKey, files] of Object.entries(data.selected)) {
|
|
let normalizedKey = fKey;
|
|
const parts = fKey.split('|');
|
|
if (parts.length >= 3 && parts[2].startsWith('/')) {
|
|
parts[2] = parts[2].substring(1);
|
|
normalizedKey = parts.join('|');
|
|
}
|
|
const set = localSelection.get(normalizedKey) || new Set();
|
|
files.forEach(f => set.add(f));
|
|
localSelection.set(normalizedKey, set);
|
|
}
|
|
updateGridCheckboxes();
|
|
} catch (e) {
|
|
console.error(e);
|
|
summaryText.textContent = t('detailUploadError') + e.message;
|
|
setTimeout(() => updateGridCheckboxes(), 2000);
|
|
}
|
|
};
|
|
|
|
toolbar.appendChild(createTBtn(t('detailSelectNoPreview'), () => handleBatchSelect('no_preview'), true));
|
|
toolbar.appendChild(createTBtn(t('detailSelectNoDesc'), () => handleBatchSelect('no_desc'), true));
|
|
|
|
const divi = document.createElement('div');
|
|
divi.style.width = '1px'; divi.style.height = '24px'; divi.style.background = 'var(--amb-bg-card-hover)'; divi.style.margin = '0 8px';
|
|
toolbar.appendChild(divi);
|
|
|
|
toolbar.appendChild(createTBtn(t('detailSelectAll'), () => handleBatchSelect('all')));
|
|
toolbar.appendChild(createTBtn(t('detailClear'), () => handleBatchSelect('none')));
|
|
|
|
const summaryText = document.createElement('div');
|
|
summaryText.style.marginLeft = 'auto';
|
|
summaryText.style.color = '#9aa0a6';
|
|
summaryText.style.fontWeight = '400';
|
|
toolbar.appendChild(summaryText);
|
|
|
|
rightPanel.appendChild(toolbar);
|
|
|
|
const grid = document.createElement('div');
|
|
grid.style.flex = '1';
|
|
grid.style.overflowY = 'auto';
|
|
grid.style.padding = '24px';
|
|
grid.style.display = 'grid';
|
|
grid.style.gridTemplateColumns = 'repeat(auto-fill, minmax(180px, 1fr))';
|
|
grid.style.gridAutoRows = 'max-content';
|
|
grid.style.gap = '20px';
|
|
grid.style.alignContent = 'start';
|
|
grid.style.background = 'var(--amb-bg-panel)';
|
|
rightPanel.appendChild(grid);
|
|
|
|
body.appendChild(leftPanel);
|
|
body.appendChild(rightPanel);
|
|
contentDiv.appendChild(body);
|
|
|
|
// Footer (Confirm)
|
|
const footer = document.createElement('div');
|
|
footer.style.padding = '12px 24px';
|
|
footer.style.borderTop = '1px solid var(--amb-border-strong)';
|
|
footer.style.display = 'flex';
|
|
footer.style.justifyContent = 'flex-end';
|
|
footer.style.gap = '12px';
|
|
|
|
const cancelBtn = document.createElement('button');
|
|
cancelBtn.textContent = t('detailCancel');
|
|
cancelBtn.style.padding = '8px 24px';
|
|
cancelBtn.style.background = 'transparent';
|
|
cancelBtn.style.color = '#9ca3af';
|
|
cancelBtn.style.border = 'none';
|
|
cancelBtn.style.borderRadius = '4px';
|
|
cancelBtn.style.cursor = 'pointer';
|
|
cancelBtn.style.fontWeight = '500';
|
|
cancelBtn.style.fontSize = '14px';
|
|
cancelBtn.onmouseover = () => cancelBtn.style.background = 'rgba(255, 255, 255, 0.08)';
|
|
cancelBtn.onmouseout = () => cancelBtn.style.background = 'transparent';
|
|
cancelBtn.onclick = () => document.body.removeChild(modal);
|
|
|
|
const confirmBtn = document.createElement('button');
|
|
confirmBtn.textContent = t('detailConfirm');
|
|
confirmBtn.style.padding = '8px 24px';
|
|
confirmBtn.style.background = '#e5e7eb';
|
|
confirmBtn.style.color = '#111827'; // dark text on bright accent button
|
|
confirmBtn.style.border = 'none';
|
|
confirmBtn.style.borderRadius = '4px';
|
|
confirmBtn.style.cursor = 'pointer';
|
|
confirmBtn.style.fontWeight = '600';
|
|
confirmBtn.style.fontSize = '14px';
|
|
confirmBtn.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)';
|
|
confirmBtn.onmouseover = () => { confirmBtn.style.background = '#ffffff'; confirmBtn.style.boxShadow = '0 1px 3px 0 rgba(0,0,0,.3), 0 4px 8px 3px rgba(0,0,0,.15)'; };
|
|
confirmBtn.onmouseout = () => { confirmBtn.style.background = '#e5e7eb'; confirmBtn.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)'; };
|
|
confirmBtn.onclick = () => {
|
|
onConfirm(localSelection);
|
|
document.body.removeChild(modal);
|
|
};
|
|
|
|
footer.appendChild(cancelBtn);
|
|
footer.appendChild(confirmBtn);
|
|
contentDiv.appendChild(footer);
|
|
|
|
modal.appendChild(contentDiv);
|
|
document.body.appendChild(modal);
|
|
|
|
// Infinite Scroll Logic
|
|
const renderCards = (modelsToRender) => {
|
|
const frag = document.createDocumentFragment();
|
|
modelsToRender.forEach(m => {
|
|
const card = document.createElement('div');
|
|
card.className = 'model-card';
|
|
card.dataset.filename = m.filename;
|
|
card.dataset.mtype = m.type;
|
|
card.dataset.mpathidx = m.path_idx;
|
|
card.dataset.msubfolder = m.subfolder;
|
|
|
|
card.style.background = 'var(--amb-bg-card-hover)';
|
|
card.style.borderRadius = '8px';
|
|
card.style.overflow = 'hidden';
|
|
card.style.position = 'relative';
|
|
card.style.border = '1px solid var(--amb-border-strong)';
|
|
card.style.display = 'flex';
|
|
card.style.flexDirection = 'column';
|
|
card.style.aspectRatio = '2 / 3';
|
|
card.style.height = 'auto';
|
|
card.style.cursor = 'pointer';
|
|
card.style.boxShadow = '0 1px 2px 0 rgba(0,0,0,.3), 0 1px 3px 1px rgba(0,0,0,.15)';
|
|
|
|
const imgContainer = document.createElement('div');
|
|
imgContainer.style.flex = '1';
|
|
imgContainer.style.background = 'var(--amb-bg-card)';
|
|
imgContainer.style.display = 'flex';
|
|
imgContainer.style.justifyContent = 'center';
|
|
imgContainer.style.alignItems = 'center';
|
|
imgContainer.style.overflow = 'hidden';
|
|
|
|
if (m.preview_url) {
|
|
|
|
|
|
const isVid = m.preview_url.match(/\.(mp4|webm|mov|avi)(?:&|$)/i);
|
|
|
|
|
|
if (isVid) {
|
|
|
|
|
|
const video = document.createElement('video');
|
|
|
|
|
|
video.src = m.preview_url;
|
|
|
|
|
|
video.style.width = '100%';
|
|
|
|
|
|
video.style.height = '100%';
|
|
|
|
|
|
video.style.objectFit = 'cover';
|
|
|
|
|
|
video.muted = true;
|
|
|
|
|
|
video.loop = true;
|
|
|
|
|
|
video.playsInline = true;
|
|
|
|
|
|
card.addEventListener('mouseenter', () => video.play().catch(e => {}));
|
|
|
|
|
|
card.addEventListener('mouseleave', () => { video.pause(); video.currentTime = 0; });
|
|
|
|
|
|
imgContainer.appendChild(video);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const img = document.createElement('img');
|
|
|
|
|
|
img.src = m.preview_url;
|
|
|
|
|
|
img.style.width = '100%';
|
|
|
|
|
|
img.style.height = '100%';
|
|
|
|
|
|
img.style.objectFit = 'cover';
|
|
|
|
|
|
imgContainer.appendChild(img);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
imgContainer.innerHTML = '<span style="display:inline-block;opacity:0.25;"><svg style="width:40px;height:40px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></span>';
|
|
}
|
|
card.appendChild(imgContainer);
|
|
|
|
const nameBar = document.createElement('div');
|
|
nameBar.style.padding = '12px';
|
|
nameBar.style.background = 'var(--amb-bg-card-hover)';
|
|
nameBar.style.fontSize = '13px';
|
|
nameBar.style.color = '#e8eaed';
|
|
nameBar.style.whiteSpace = 'nowrap';
|
|
nameBar.style.overflow = 'hidden';
|
|
nameBar.style.textOverflow = 'ellipsis';
|
|
nameBar.style.borderTop = '1px solid var(--amb-border-strong)';
|
|
nameBar.innerText = m.filename;
|
|
card.appendChild(nameBar);
|
|
|
|
const cbWrapper = document.createElement('div');
|
|
cbWrapper.className = 'cb-wrapper';
|
|
cbWrapper.style.position = 'absolute';
|
|
cbWrapper.style.top = '8px';
|
|
cbWrapper.style.right = '8px';
|
|
cbWrapper.style.width = '20px';
|
|
cbWrapper.style.height = '20px';
|
|
cbWrapper.style.borderRadius = '50%';
|
|
cbWrapper.style.background = 'rgba(0,0,0,0.3)';
|
|
cbWrapper.style.border = '2px solid #5f6368';
|
|
cbWrapper.style.display = 'flex';
|
|
cbWrapper.style.justifyContent = 'center';
|
|
cbWrapper.style.alignItems = 'center';
|
|
cbWrapper.style.pointerEvents = 'none';
|
|
|
|
const cb = document.createElement('input');
|
|
cb.type = 'checkbox';
|
|
cb.style.display = 'none';
|
|
|
|
const checkIcon = document.createElement('span');
|
|
checkIcon.innerHTML = '✓';
|
|
checkIcon.style.color = 'transparent';
|
|
checkIcon.style.fontSize = '12px';
|
|
checkIcon.style.fontWeight = 'bold';
|
|
cbWrapper.appendChild(checkIcon);
|
|
cbWrapper.appendChild(cb);
|
|
card.appendChild(cbWrapper);
|
|
|
|
card.onclick = () => {
|
|
const mKey = `${m.type}|${m.path_idx}|${m.subfolder}`;
|
|
const cset = localSelection.get(mKey) || new Set();
|
|
if (!cset.has(m.filename)) {
|
|
cset.add(m.filename);
|
|
} else {
|
|
cset.delete(m.filename);
|
|
}
|
|
localSelection.set(mKey, cset);
|
|
updateGridCheckboxes();
|
|
};
|
|
|
|
frag.appendChild(card);
|
|
});
|
|
return frag;
|
|
};
|
|
|
|
const fetchModelsPage = async (isLoadMore = false) => {
|
|
if (isLoading) return;
|
|
isLoading = true;
|
|
|
|
const prevStatus = grid.querySelector('.status-text');
|
|
if (prevStatus) grid.removeChild(prevStatus);
|
|
|
|
const loadingIndicator = document.createElement('div');
|
|
loadingIndicator.className = 'status-text';
|
|
loadingIndicator.style.gridColumn = '1 / -1';
|
|
loadingIndicator.style.padding = '40px';
|
|
loadingIndicator.style.textAlign = 'center';
|
|
loadingIndicator.style.color = '#9aa0a6';
|
|
loadingIndicator.style.fontSize = '16px';
|
|
loadingIndicator.textContent = t('detailLoading');
|
|
grid.appendChild(loadingIndicator);
|
|
|
|
try {
|
|
const sep = currentBaseUrl.includes('?') ? '&' : '?';
|
|
const url = `${currentBaseUrl}${sep}page=${currentPage}&limit=50`;
|
|
const res = await fetch(url);
|
|
if (!res.ok) {
|
|
const apiError = document.createElement('div');
|
|
apiError.className = 'status-text';
|
|
apiError.style.gridColumn = '1 / -1';
|
|
apiError.style.padding = '60px';
|
|
apiError.style.textAlign = 'center';
|
|
apiError.style.color = '#f28b82';
|
|
apiError.style.fontSize = '16px';
|
|
apiError.style.whiteSpace = 'pre-line';
|
|
apiError.textContent = t('detailApiRestart');
|
|
grid.replaceChildren(apiError);
|
|
isLoading = false;
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
|
|
const fetchedModels = data.models || [];
|
|
currentTotal = data.total || 0;
|
|
|
|
if (isLoadMore) {
|
|
currentModels = currentModels.concat(fetchedModels);
|
|
} else {
|
|
currentModels = fetchedModels;
|
|
// Keep the loading indicator temporarily, we clear grid except it
|
|
Array.from(grid.children).forEach(c => {
|
|
if (c !== loadingIndicator) grid.removeChild(c);
|
|
});
|
|
}
|
|
|
|
grid.removeChild(loadingIndicator);
|
|
|
|
if (currentModels.length === 0 && !isLoadMore) {
|
|
const emptyModels = document.createElement('div');
|
|
emptyModels.className = 'status-text';
|
|
emptyModels.style.gridColumn = '1 / -1';
|
|
emptyModels.style.padding = '60px';
|
|
emptyModels.style.textAlign = 'center';
|
|
emptyModels.style.color = '#9aa0a6';
|
|
emptyModels.style.fontSize = '16px';
|
|
emptyModels.textContent = t('detailNoModels');
|
|
grid.replaceChildren(emptyModels);
|
|
} else {
|
|
grid.appendChild(renderCards(fetchedModels));
|
|
updateGridCheckboxes();
|
|
|
|
// Show status at bottom
|
|
const statusText = document.createElement('div');
|
|
statusText.className = 'status-text';
|
|
statusText.style.gridColumn = '1 / -1';
|
|
statusText.style.padding = '20px';
|
|
statusText.style.textAlign = 'center';
|
|
statusText.style.color = '#5f6368';
|
|
statusText.style.fontSize = '14px';
|
|
if (currentModels.length >= currentTotal) {
|
|
statusText.textContent = t('detailAllLoaded');
|
|
} else {
|
|
statusText.textContent = t('detailScrollMore');
|
|
}
|
|
grid.appendChild(statusText);
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
grid.removeChild(loadingIndicator);
|
|
}
|
|
|
|
isLoading = false;
|
|
};
|
|
|
|
grid.onscroll = () => {
|
|
if (isLoading || currentModels.length >= currentTotal) return;
|
|
// Near bottom detection
|
|
if (grid.scrollTop + grid.clientHeight >= grid.scrollHeight - 150) {
|
|
currentPage++;
|
|
fetchModelsPage(true);
|
|
}
|
|
};
|
|
|
|
// Render Folders
|
|
const renderFolders = async () => {
|
|
treeContainer.innerHTML = '';
|
|
|
|
const allBtn = document.createElement('div');
|
|
allBtn.className = 'folder-item';
|
|
allBtn.dataset.folderKey = 'ALL';
|
|
allBtn.style.padding = '10px 16px';
|
|
allBtn.style.cursor = 'pointer';
|
|
allBtn.style.color = '#e8eaed';
|
|
allBtn.style.fontSize = '14px';
|
|
allBtn.style.fontWeight = '500';
|
|
allBtn.style.borderRadius = '0 16px 16px 0';
|
|
allBtn.style.marginBottom = '8px';
|
|
allBtn.style.display = 'flex';
|
|
allBtn.style.justifyContent = 'space-between';
|
|
allBtn.style.alignItems = 'center';
|
|
allBtn.textContent = `🌟 ${t('models')}`;
|
|
|
|
let loadFolder = async (fBtn, folderKey, fetchUrl) => {
|
|
treeContainer.querySelectorAll('.folder-item').forEach(d => {
|
|
d.style.background = 'transparent';
|
|
d.style.color = '#9aa0a6';
|
|
d.style.fontWeight = '400';
|
|
});
|
|
fBtn.style.background = 'rgba(255, 255, 255, 0.1)';
|
|
fBtn.style.color = '#ffffff';
|
|
fBtn.style.fontWeight = '500';
|
|
|
|
currentFolderKey = folderKey;
|
|
currentBaseUrl = fetchUrl;
|
|
currentPage = 1;
|
|
currentModels = [];
|
|
currentTotal = 0;
|
|
grid.innerHTML = '';
|
|
|
|
await fetchModelsPage(false);
|
|
};
|
|
|
|
allBtn.onclick = () => loadFolder(allBtn, 'ALL', '/anomalous/all_scan_models');
|
|
treeContainer.appendChild(allBtn);
|
|
|
|
if (!this.foldersData) {
|
|
const res = await fetch('/anomalous/folders');
|
|
const data = await res.json();
|
|
this.foldersData = data.folders || [];
|
|
}
|
|
this.foldersData.forEach(tData => {
|
|
const typeItem = document.createElement('div');
|
|
typeItem.style.marginBottom = '4px';
|
|
|
|
const tTitle = document.createElement('div');
|
|
tTitle.innerText = tData.label;
|
|
tTitle.style.color = '#9aa0a6';
|
|
tTitle.style.fontWeight = '500';
|
|
tTitle.style.padding = '8px 16px';
|
|
tTitle.style.fontSize = '12px';
|
|
tTitle.style.marginTop = '8px';
|
|
typeItem.appendChild(tTitle);
|
|
|
|
for (const [path, fData] of Object.entries(tData.folders)) {
|
|
if (fData.model_count === 0 && !fData.has_models) continue;
|
|
|
|
const folderKey = `${tData.type}|${tData.path_idx}|${path}`;
|
|
const fBtn = document.createElement('div');
|
|
fBtn.className = 'folder-item';
|
|
fBtn.dataset.folderKey = folderKey;
|
|
|
|
const depth = path.split('/').length - 1;
|
|
fBtn.style.padding = `8px 16px 8px ${16 + depth * 16}px`;
|
|
fBtn.style.cursor = 'pointer';
|
|
fBtn.style.color = '#9aa0a6';
|
|
fBtn.style.fontSize = '14px';
|
|
fBtn.style.borderRadius = '0 16px 16px 0';
|
|
fBtn.style.display = 'flex';
|
|
fBtn.style.justifyContent = 'space-between';
|
|
fBtn.style.alignItems = 'center';
|
|
|
|
const leftPart = document.createElement('div');
|
|
leftPart.style.display = 'flex';
|
|
leftPart.style.alignItems = 'center';
|
|
leftPart.style.gap = '8px';
|
|
leftPart.innerHTML = `<span style="display:inline-flex;align-items:center;color:#9aa0a6"><svg style="width:14px;height:14px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span> <span style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px;">${escapeHtml(fData.name)}</span> <span style="color:#5f6368;font-size:12px">(${escapeHtml(fData.model_count)})</span>`;
|
|
|
|
const badge = document.createElement('div');
|
|
badge.className = 'selection-badge';
|
|
badge.style.background = '#e5e7eb';
|
|
badge.style.color = '#111827';
|
|
badge.style.fontSize = '11px';
|
|
badge.style.fontWeight = '500';
|
|
badge.style.padding = '1px 6px';
|
|
badge.style.borderRadius = '10px';
|
|
badge.style.display = 'none';
|
|
|
|
fBtn.appendChild(leftPart);
|
|
fBtn.appendChild(badge);
|
|
|
|
fBtn.onmouseover = () => { if (currentFolderKey !== folderKey) fBtn.style.background = 'rgba(255,255,255,0.04)'; };
|
|
fBtn.onmouseout = () => { if (currentFolderKey !== folderKey) fBtn.style.background = 'transparent'; };
|
|
|
|
fBtn.onclick = () => {
|
|
const params = new URLSearchParams({ type: tData.type, path_idx: tData.path_idx, subfolder: path });
|
|
loadFolder(fBtn, folderKey, '/anomalous/models?' + params.toString());
|
|
};
|
|
typeItem.appendChild(fBtn);
|
|
}
|
|
treeContainer.appendChild(typeItem);
|
|
});
|
|
|
|
allBtn.onclick();
|
|
};
|
|
renderFolders();
|
|
}
|
|
|
|
|
|
export function setWidgetValuePath(node, relPath) {
|
|
if (!node.widgets || node.widgets.length === 0) return;
|
|
const w = node.widgets.find(wg => wg.type === 'combo');
|
|
const targetWidget = w || node.widgets[0];
|
|
if (!targetWidget) return;
|
|
|
|
if (targetWidget.options && targetWidget.options.values) {
|
|
const normalizedTarget = relPath.replace(/\\/g, '/');
|
|
const match = targetWidget.options.values.find(v => {
|
|
return String(v).replace(/\\/g, '/') === normalizedTarget;
|
|
});
|
|
if (match) {
|
|
targetWidget.value = match;
|
|
return;
|
|
}
|
|
}
|
|
targetWidget.value = relPath;
|
|
}
|