Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_notebook_editor.js
T
DemonGatanjieuandClaude Opus 5.5 99576e60bd Fix picker jumping to the top, combo saved per tag, unreadable long-text activity
- Current node's model picker redrew the whole grid to mark the chosen card, so
  choosing one further down scrolled back to the top. The cards are repainted in
  place (paintCard) instead.
- The classic combo editor saved when its bilingual tags were shown and again for
  each tag's translation as it arrived: a long prompt wrote the combo dozens of
  times in a second (34 note_save entries in one user's log). Rendering no longer
  saves and the translations share one save half a second after the last. Its
  prompt placeholder moves into locales.js (no inline language branch).
- Activity values are capped at 400 characters from the start, so an edit late in
  a long prompt logged identical before and after. A long before/after pair is now
  cut around its first difference (test included).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 16:36:15 +08:00

599 lines
26 KiB
JavaScript

/**
* ui_notebook_editor.js
* Combo (搭配) editor with unfolded cards, sticky toolbar, and compatible-model galleries.
*/
import { translate } from './locales.js';
import { escapeHtml } from './safe_dom.js';
import { isUnetModel, UNLABELED_BASE_MODEL } from './notebook_canvas.js';
import { STRUCTURED } from './combo_slots.js';
import { renderStructuredCombo } from './ui_combo_structure.js';
const t = (key, params) => translate(key, params);
/**
* Sticky top action toolbar for the active notebook with floating dropdown menu.
*/
export function createNotebookToolbar(ctx, notebook) {
const tb = document.createElement('div');
tb.className = 'anomalous-nb-toolbar';
const titleBox = document.createElement('div');
titleBox.style.display = 'flex';
titleBox.style.alignItems = 'center';
titleBox.style.gap = '8px';
const titleIcon = document.createElement('span');
titleIcon.textContent = '📝';
titleIcon.style.fontSize = '1.2rem';
const titleArea = document.createElement('h3');
titleArea.textContent = notebook.name;
titleArea.style.margin = '0';
titleArea.style.fontSize = '1.15rem';
titleArea.style.fontWeight = '700';
titleBox.append(titleIcon, titleArea);
const rightBtns = document.createElement('div');
rightBtns.className = 'anomalous-notebook-actions';
const saveBtn = document.createElement('button');
saveBtn.type = 'button';
saveBtn.textContent = `💾 ${t('saveNotebook')}`;
saveBtn.className = 'anomalous-btn-primary';
saveBtn.onclick = async () => {
const orig = saveBtn.innerHTML;
saveBtn.innerHTML = '⏳...';
const saved = await ctx.saveCurrentNotebook();
if (!saved) { saveBtn.innerHTML = orig; return; }
saveBtn.innerHTML = '✅';
saveBtn.style.background = '#2e8b57';
setTimeout(() => {
saveBtn.innerHTML = orig;
saveBtn.style.background = '';
}, 1500);
};
// Put on the canvas: the combo as a new group of nodes.
const useBtn = Object.assign(document.createElement('button'), { type: 'button', className: 'anomalous-btn-success', textContent: t('comboUse'), title: t('comboUseHint') });
useBtn.onclick = () => ctx.sendNotebookToCanvas();
// Floating More Dropdown Menu
const moreWrapper = document.createElement('div');
moreWrapper.className = 'anomalous-nb-dropdown-wrapper';
const moreBtn = document.createElement('button');
moreBtn.type = 'button';
moreBtn.className = 'anomalous-nb-more-btn';
moreBtn.innerHTML = `<span>··· ${t('notebookMore')}</span> <span style="font-size:0.7rem;margin-left:2px;">▾</span>`;
const dropdownMenu = document.createElement('div');
dropdownMenu.className = 'anomalous-nb-dropdown-menu';
let delTimer = null;
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'anomalous-nb-dropdown-item anomalous-nb-dropdown-item-danger';
const normalDelHtml = `<span>🗑️</span> <span>${t('deleteNotebook')}</span>`;
delBtn.innerHTML = normalDelHtml;
const resetDel = () => {
clearTimeout(delTimer);
delBtn.classList.remove('confirming');
delBtn.innerHTML = normalDelHtml;
};
delBtn.onclick = (e) => {
e.stopPropagation();
if (!delBtn.classList.contains('confirming')) {
delBtn.classList.add('confirming');
const delSureText = (t('delSure') || (window.anomalous_browser_lang === 'zh' ? '确认删除?' : 'Confirm Delete?')).replace(/^⚠️\s*/, '');
delBtn.innerHTML = `<span>⚠️</span> <span>${delSureText}</span>`;
delTimer = setTimeout(resetDel, 4000);
} else {
resetDel();
dropdownMenu.classList.remove('show');
moreBtn.classList.remove('active');
ctx.deleteCurrentNotebook();
}
};
dropdownMenu.append(delBtn);
moreBtn.onclick = (e) => {
e.stopPropagation();
const isOpen = dropdownMenu.classList.toggle('show');
moreBtn.classList.toggle('active', isOpen);
if (!isOpen) resetDel();
};
const onDocClick = (e) => {
if (!moreWrapper.contains(e.target)) {
dropdownMenu.classList.remove('show');
moreBtn.classList.remove('active');
resetDel();
}
};
document.addEventListener('click', onDocClick);
moreWrapper.append(moreBtn, dropdownMenu);
rightBtns.append(saveBtn, useBtn, moreWrapper);
tb.append(titleBox, rightBtns);
return tb;
}
/**
* Flat companion models section (Base model + Checkpoints + LoRAs).
*/
function createCompanionModelsCard(ctx, data) {
const card = document.createElement('div');
card.id = 'amb-nb-sec-models';
card.className = 'anomalous-nb-card anomalous-nb-models-card anomalous-nb-models-fold';
card.open = true;
const header = document.createElement('div');
header.className = 'anomalous-nb-card-header';
const titleLeft = document.createElement('div');
titleLeft.style.display = 'flex';
titleLeft.style.alignItems = 'center';
titleLeft.style.gap = '8px';
const titleText = document.createElement('span');
titleText.textContent = `📦 ${t('notebookCompanionModels') || (window.anomalous_browser_lang === 'zh' ? '配套模型' : 'Companion Models')}`;
titleText.style.fontWeight = '600';
const modelsBadge = document.createElement('span');
modelsBadge.className = 'anomalous-nb-models-badge';
titleLeft.append(titleText, modelsBadge);
header.appendChild(titleLeft);
const mainSelectedBadge = document.createElement('span');
mainSelectedBadge.className = 'anomalous-nb-selected-badge';
mainSelectedBadge.style.color = 'var(--amb-lt-violet, #c084fc)';
const loraSelectedBadge = document.createElement('span');
loraSelectedBadge.className = 'anomalous-nb-selected-badge';
loraSelectedBadge.style.color = 'var(--amb-warn)';
const updateSummary = () => {
const baseInfo = data.baseModel === UNLABELED_BASE_MODEL ? t('notebookUnlabeledBase') : (data.baseModel || 'SDXL');
const mainInfo = data.mainModel?.filename ? ` · ${data.mainModel.filename}` : '';
const loraCount = data.loras?.length ? ` · ${data.loras.length} LoRA` : '';
modelsBadge.textContent = `${baseInfo}${mainInfo}${loraCount}`;
mainSelectedBadge.textContent = data.mainModel?.filename ? `✓ ${data.mainModel.filename}` : (t('recipeDiffNone') || '未选择');
loraSelectedBadge.textContent = data.loras?.length ? `✓ ${data.loras.length} LoRA` : (t('recipeDiffNone') || '未选择');
};
ctx.updateNotebookModelsSummary = updateSummary;
// Base model selection
const baseRow = document.createElement('div');
baseRow.className = 'anomalous-nb-row anomalous-nb-base-row';
baseRow.style.display = 'flex';
baseRow.style.alignItems = 'center';
baseRow.style.gap = '10px';
baseRow.style.marginBottom = '12px';
const baseTitle = document.createElement('span');
baseTitle.style.fontWeight = '600';
baseTitle.style.fontSize = '0.88rem';
baseTitle.style.color = 'var(--amb-lt-soft, #cbd5e1)';
baseTitle.textContent = `${t('baseModel') || (window.anomalous_browser_lang === 'zh' ? '基础模型' : 'Base Model')}:`;
baseRow.appendChild(baseTitle);
const baseSelect = document.createElement('select');
baseSelect.className = 'anomalous-nb-select';
const buildSelect = (bases) => {
baseSelect.innerHTML = '';
bases.forEach(b => {
const opt = document.createElement('option');
opt.value = b;
opt.text = b === UNLABELED_BASE_MODEL ? t('notebookUnlabeledBase') : b;
if (data.baseModel === b) opt.selected = true;
baseSelect.appendChild(opt);
});
if (!data.baseModel && bases.length > 0) data.baseModel = bases[0];
updateSummary();
};
if (ctx.baseModelsCache) {
buildSelect(ctx.baseModelsCache);
} else {
const tempBases = ['SD 1.5', 'SD 2.1', 'SDXL', 'SD 3.0', 'SD 3.5', 'Flux.1', 'Pony', 'HunyuanVideo', 'LTX-Video', 'OmniGen'];
buildSelect(tempBases);
fetch('/anomalous/base_models').then(r => r.json()).then(d => {
const bases = [...(d.base_models || []), ...(d.unlabeled ? [UNLABELED_BASE_MODEL] : [])];
if (bases.length > 0) {
ctx.baseModelsCache = bases;
buildSelect(ctx.baseModelsCache);
}
}).catch(() => { });
}
if (!data.baseModel) data.baseModel = 'SDXL';
baseSelect.onchange = () => {
data.baseModel = baseSelect.value;
data.mainModel = null;
data.loras = [];
ctx.saveCurrentNotebook();
ctx.renderNotebookEditor();
};
baseRow.appendChild(baseSelect);
// Main Model Gallery
const mainBox = document.createElement('div');
mainBox.className = 'anomalous-nb-gallery-box';
const mainRow = document.createElement('div');
mainRow.className = 'anomalous-nb-row';
mainRow.style.display = 'flex';
mainRow.style.alignItems = 'center';
mainRow.style.justifyContent = 'space-between';
mainRow.style.marginBottom = '8px';
const mainLabel = document.createElement('strong');
mainLabel.textContent = `🎨 ${t('mainModel') || (window.anomalous_browser_lang === 'zh' ? '主模型' : 'Main Model')}`;
mainRow.append(mainLabel, mainSelectedBadge);
const mainGallery = document.createElement('div');
mainGallery.className = 'anomalous-nb-gallery-wrap';
mainBox.append(mainRow, mainGallery);
// LoRA Gallery
const loraBox = document.createElement('div');
loraBox.className = 'anomalous-nb-gallery-box';
const loraRow = document.createElement('div');
loraRow.className = 'anomalous-nb-row';
loraRow.style.display = 'flex';
loraRow.style.alignItems = 'center';
loraRow.style.justifyContent = 'space-between';
loraRow.style.marginBottom = '8px';
const loraLabel = document.createElement('strong');
loraLabel.textContent = `⚡ ${t('loras') || (window.anomalous_browser_lang === 'zh' ? 'LoRA 模型' : 'LoRA Models')}`;
loraRow.append(loraLabel, loraSelectedBadge);
const loraGallery = document.createElement('div');
loraGallery.className = 'anomalous-nb-gallery-wrap';
loraBox.append(loraRow, loraGallery);
const unetHint = document.createElement('div');
unetHint.className = 'anomalous-nb-unet-hint';
unetHint.textContent = t('notebookUnetNeedsTextEncoder');
mainBox.appendChild(unetHint);
const showUnetHint = () => { unetHint.hidden = !isUnetModel(data.mainModel); };
ctx.updateNotebookUnetHint = showUnetHint;
showUnetHint();
card.append(header, baseRow, mainBox, loraBox);
updateSummary();
ctx.fillNotebookGalleries(data.baseModel, mainGallery, loraGallery, data);
return card;
}
/**
* Prompt composer section with bilingual dual pane and compact inline tools.
*/
function createPromptSection(ctx, data) {
const card = document.createElement('div');
card.id = 'amb-nb-sec-prompt';
card.className = 'anomalous-nb-card anomalous-nb-prompt-card anomalous-nb-prompt-section';
const header = document.createElement('div');
header.className = 'anomalous-nb-card-header';
const title = document.createElement('span');
title.innerHTML = `✍️ <strong>${t('notebookPromptTitle') || (window.anomalous_browser_lang === 'zh' ? '提示词' : 'Prompt')}</strong>`;
const rawBtn = document.createElement('button');
rawBtn.type = 'button';
rawBtn.className = 'anomalous-btn-primary';
rawBtn.textContent = t('notebookDoneEditing') || (window.anomalous_browser_lang === 'zh' ? '📑 查看双语分词' : '📑 View Bilingual Tags');
header.append(title, rawBtn);
const rawArea = document.createElement('textarea');
rawArea.className = 'anomalous-nb-textarea';
rawArea.value = data.promptEn || '';
rawArea.placeholder = t('notebookPromptPlaceholder');
const dualPane = document.createElement('div');
dualPane.className = 'anomalous-nb-dual-pane';
dualPane.style.display = 'none';
if (!data.translations) data.translations = {};
let visualDebounceTimer = null;
// One save after a burst (typing, or every tag's translation arriving), not one per change.
const saveSoon = () => {
clearTimeout(ctx.pTimeout);
ctx.pTimeout = setTimeout(() => ctx.saveCurrentNotebook(), 500);
};
const updateVisualTags = () => {
const txt = rawArea.value;
if (!txt.trim()) {
dualPane.replaceChildren();
return;
}
const tags = txt.split(',').map(s => s.trim()).filter(Boolean);
const fragment = document.createDocumentFragment();
tags.forEach((tag, idx) => {
const tagRow = document.createElement('div');
tagRow.className = 'anomalous-nb-tag-row';
const tagL = document.createElement('div');
tagL.className = 'anomalous-nb-visual-tag';
tagL.style.flex = '1';
tagL.style.justifyContent = 'space-between';
const txtL = document.createElement('span');
txtL.innerText = tag;
const copyL = document.createElement('span');
copyL.className = 'anomalous-nb-copy-btn';
copyL.innerHTML = '📋';
copyL.onclick = (e) => {
e.stopPropagation();
navigator.clipboard.writeText(tag).then(() => { copyL.innerHTML = '✅'; setTimeout(() => copyL.innerHTML = '📋', 1000); });
};
tagL.append(txtL, copyL);
const tagR = document.createElement('div');
tagR.className = 'anomalous-nb-visual-tag';
tagR.style.flex = '1';
tagR.style.justifyContent = 'space-between';
const transTxt = data.translations[tag] ? data.translations[tag] : '...';
const txtR = document.createElement('span');
txtR.innerText = transTxt;
const copyR = document.createElement('span');
copyR.className = 'anomalous-nb-copy-btn';
copyR.innerHTML = '📋';
copyR.onclick = (e) => {
e.stopPropagation();
navigator.clipboard.writeText(txtR.innerText).then(() => { copyR.innerHTML = '✅'; setTimeout(() => copyR.innerHTML = '📋', 1000); });
};
tagR.append(txtR, copyR);
tagL.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); };
tagL.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); };
tagR.onmouseenter = () => { tagL.classList.add('hover'); tagR.classList.add('hover'); };
tagR.onmouseleave = () => { tagL.classList.remove('hover'); tagR.classList.remove('hover'); };
tagL.onclick = () => {
const inp = document.createElement('input');
inp.value = tag; inp.className = 'anomalous-nb-tag-edit';
tagL.innerHTML = ''; tagL.appendChild(inp); inp.focus();
const finish = () => {
tags[idx] = inp.value.trim();
rawArea.value = tags.join(', ');
updateVisualTags();
};
inp.onblur = finish;
inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); };
};
tagR.onclick = () => {
const inp = document.createElement('input');
inp.value = data.translations[tag] || ''; inp.className = 'anomalous-nb-tag-edit';
tagR.innerHTML = ''; tagR.appendChild(inp); inp.focus();
const finish = () => {
data.translations[tag] = inp.value.trim();
ctx.saveCurrentNotebook();
updateVisualTags();
};
inp.onblur = finish;
inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); };
};
tagRow.append(tagL, tagR);
fragment.appendChild(tagRow);
if (!data.translations[tag]) {
fetch('/anomalous/translate', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: tag, target_lang: data.targetLang || 'zh-CN' })
}).then(r => r.json()).then(d => {
if (d.translated) {
data.translations[tag] = d.translated;
txtR.innerText = d.translated;
saveSoon();
}
}).catch(() => { });
}
});
dualPane.replaceChildren(fragment);
};
rawBtn.onclick = () => {
if (rawArea.style.display === 'none') {
rawArea.style.display = 'block';
dualPane.style.display = 'none';
rawBtn.textContent = t('notebookDoneEditing') || (window.anomalous_browser_lang === 'zh' ? '📑 查看双语分词' : '📑 View Bilingual Tags');
} else {
rawArea.style.display = 'none';
dualPane.style.display = 'flex';
rawBtn.textContent = t('editRaw') || (window.anomalous_browser_lang === 'zh' ? '✏️ 编辑原始文本' : '✏️ Edit Raw Prompt');
updateVisualTags();
}
};
rawArea.oninput = () => {
data.promptEn = rawArea.value;
saveSoon();
if (dualPane.style.display !== 'none') {
clearTimeout(visualDebounceTimer);
visualDebounceTimer = setTimeout(() => updateVisualTags(), 300);
}
};
const toolsBar = createPromptToolsBar(ctx, data, rawArea, updateVisualTags);
card.append(header, rawArea, dualPane, toolsBar);
return card;
}
/**
* Compact, unfolded single-line toolbar for Find & Replace and Language selection.
*/
function createPromptToolsBar(ctx, data, rawArea, onTagsUpdated) {
const bar = document.createElement('div');
bar.id = 'amb-nb-sec-tools';
bar.className = 'anomalous-nb-tools-bar';
const langSelect = document.createElement('select');
langSelect.className = 'anomalous-nb-select';
const langs = [
{ v: 'zh-CN', l: '🇨🇳 中文 (zh-CN)' }, { v: 'en', l: '🇬🇧 English (en)' },
{ v: 'ja', l: '🇯🇵 日本语 (ja)' }, { v: 'ko', l: '🇰🇷 한국어 (ko)' },
{ v: 'fr', l: '🇫🇷 Français (fr)' }, { v: 'de', l: '🇩🇪 Deutsch (de)' },
{ v: 'es', l: '🇪🇸 Español (es)' }, { v: 'ru', l: '🇷🇺 Русский (ru)' }
];
langs.forEach(lg => {
const opt = document.createElement('option');
opt.value = lg.v; opt.text = lg.l;
if ((data.targetLang || 'zh-CN') === lg.v) opt.selected = true;
langSelect.appendChild(opt);
});
langSelect.onchange = () => {
data.targetLang = langSelect.value;
data.translations = {};
ctx.saveCurrentNotebook();
onTagsUpdated();
};
const findInput = document.createElement('input');
findInput.className = 'anomalous-nb-select';
findInput.placeholder = t('findPlaceholder') || (window.anomalous_browser_lang === 'zh' ? '查找词条...' : 'Find tag...');
findInput.style.flex = '1';
const replaceInput = document.createElement('input');
replaceInput.className = 'anomalous-nb-select';
replaceInput.placeholder = t('replacePlaceholder') || (window.anomalous_browser_lang === 'zh' ? '替换为...' : 'Replace with...');
replaceInput.style.flex = '1';
const replaceBtn = document.createElement('button');
replaceBtn.type = 'button';
replaceBtn.className = 'anomalous-btn-primary';
replaceBtn.innerHTML = t('replaceAll') || (window.anomalous_browser_lang === 'zh' ? '全部替换' : 'Replace All');
replaceBtn.onclick = () => {
const findStr = findInput.value;
const repStr = replaceInput.value;
if (!findStr) return;
rawArea.value = rawArea.value.split(findStr).join(repStr);
data.promptEn = rawArea.value;
ctx.saveCurrentNotebook();
onTagsUpdated();
};
bar.append(langSelect, findInput, replaceInput, replaceBtn);
return bar;
}
/**
* Primary notebook editor render entrypoint.
*/
export function renderNotebookEditor() {
try {
this.nbEditor.innerHTML = '';
if (!this.currentNotebook) return;
const data = this.currentNotebook.data || (this.currentNotebook.data = {});
if (data.kind === STRUCTURED) return renderStructuredCombo(this, this.nbEditor, this.currentNotebook, createNotebookToolbar(this, this.currentNotebook));
if (!data.loras) data.loras = [];
const tb = createNotebookToolbar(this, this.currentNotebook);
const modelsCard = createCompanionModelsCard(this, data);
const promptSec = createPromptSection(this, data);
this.nbEditor.replaceChildren(tb, modelsCard, promptSec);
} catch (err) {
console.error('[AMB] Error rendering notebook editor:', err);
if (this.nbEditor) {
this.nbEditor.innerHTML = `<div style="padding:20px; color:#ef4444;">Render error: ${escapeHtml(err?.message || String(err))}</div>`;
}
}
}
/**
* Fill compatible main models and Loras into horizontal galleries.
*/
export function fillNotebookGalleries(baseModel, mainGallery, loraGallery, data) {
if (!baseModel) return;
const buildThumbHtml = (m) => {
if (m.preview_url) {
const isVid = m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i);
if (isVid) return `<video src="${m.preview_url}" muted loop playsinline></video>`;
return `<img src="${m.preview_url}" />`;
}
return `<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>`;
};
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=checkpoints,unet,diffusion_models`)
.then(r => r.json()).then(d => {
const buildMainDOM = (models) => {
mainGallery.innerHTML = '';
if (!models || !models.length) {
mainGallery.innerHTML = `<span style="color:var(--amb-lt-muted,#666);">${window.anomalous_browser_lang === 'zh' ? '未找到兼容的主模型。' : 'No compatible main models found.'}</span>`;
} else {
models.forEach(m => {
const isSelected = (data.mainModel && data.mainModel.filename === m.filename);
const card = document.createElement('div');
card.className = 'anomalous-nb-minicheck ' + (isSelected ? 'selected' : '');
card.innerHTML = `${buildThumbHtml(m)}<div class="anomalous-nb-minicheck-name" title="${escapeHtml(m.filename)}">${escapeHtml(m.filename)}</div>`;
if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) {
card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(() => { }); };
card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
}
card.onclick = () => {
data.mainModel = (data.mainModel && data.mainModel.filename === m.filename) ? null : m;
this.saveCurrentNotebook();
this.updateNotebookModelsSummary?.();
this.updateNotebookUnetHint?.();
buildMainDOM(models);
};
mainGallery.appendChild(card);
});
}
};
buildMainDOM(d.models || []);
}).catch(() => { });
fetch(`/anomalous/compatible_models?base_model=${encodeURIComponent(baseModel)}&target_type=loras`)
.then(r => r.json()).then(d => {
const buildLoraDOM = (models) => {
loraGallery.innerHTML = '';
if (!models || !models.length) {
loraGallery.innerHTML = `<span style="color:var(--amb-lt-muted,#666);">${window.anomalous_browser_lang === 'zh' ? '未找到兼容的 LoRA。' : 'No compatible Loras found.'}</span>`;
} else {
models.forEach(m => {
const loraIndex = data.loras.findIndex(l => l.filename === m.filename);
const isSelected = loraIndex !== -1;
const card = document.createElement('div');
card.className = 'anomalous-nb-minilora ' + (isSelected ? 'selected' : '');
card.style.position = 'relative';
let badgeHtml = '';
if (isSelected) {
badgeHtml = `<div style="position:absolute; top:-5px; right:-5px; background:linear-gradient(135deg, #f59e0b, #d97706); color:#180808; border-radius:50%; width:20px; height:20px; font-size:12px; display:flex; align-items:center; justify-content:center; font-weight:bold; z-index:10; box-shadow: 0 2px 8px rgba(0,0,0,0.7), 0 0 6px rgba(245,158,11,0.4); border: 1px solid var(--amb-border-strong);">${loraIndex + 1}</div>`;
}
card.innerHTML = `${badgeHtml}${buildThumbHtml(m)}<div class="anomalous-nb-minilora-name" title="${escapeHtml(m.filename)}">${escapeHtml(m.filename)}</div>`;
if (m.preview_url && (m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i))) {
card.onmouseenter = () => { const v = card.querySelector('video'); if (v) v.play().catch(() => { }); };
card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } };
}
card.onclick = () => {
if (isSelected) {
data.loras = data.loras.filter(l => l.filename !== m.filename);
} else {
data.loras.push(m);
}
this.saveCurrentNotebook();
this.updateNotebookModelsSummary?.();
buildLoraDOM(models);
};
loraGallery.appendChild(card);
});
}
};
buildLoraDOM(d.models || []);
}).catch(() => { });
}