fix(notebooks): resolve TDZ ReferenceError on updateModelsSummary and empty state rendering

This commit is contained in:
DemonGatanjieu
2026-09-16 21:18:23 +08:00
parent f84379c0ae
commit d596af293c
6 changed files with 133 additions and 26 deletions
+2
View File
@@ -84,6 +84,8 @@ tests/*
!tests/model_sources_hub.mjs
!tests/shortcut_layout.mjs
!tests/shortcut_organizer.mjs
!tests/notebook_render_test.mjs
!tests/notebook_save_roundtrip.mjs
# Local diagnostic/test artifacts
grep_doctor.txt
+2 -1
View File
@@ -94,7 +94,8 @@ DOM or live LiteGraph state.
owns structured Prompt Notes (提示词笔记), integrated as a standard tool in the Toolbox
with defensive workspace return state restoration, and redesigned with companion models
(Base architecture, Main checkpoint, and LoRA stack) prominently positioned at the top of the editor,
followed by the core prompt composer and material library archiving. `ui_recipe_detail.js`
followed by the core prompt composer and material library archiving, guarded by Temporal Dead Zone (TDZ)
safe summary initialization and responsive empty-state fallback rendering. `ui_recipe_detail.js`
owns the Workflow Recipe detail workspace, including Overview prompt showcase (with `entry.text` fallback),
the responsive Parameter Presets workspace (featuring default-expanded raw node parameter inspection,
a `clamp(230px, 24vw, 290px)` sidebar with guarded card actions, uncluttered console action bars with deferred status feedback,
+53
View File
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict';
import { fixture } from './ui_fixture.mjs';
const f = fixture();
let mockNotebooks = [{ filename: '1.json', name: 'Test Note', data: { baseModel: 'SDXL', loras: [] } }];
f.fetch = async (url) => {
if (String(url).includes('/anomalous/base_models')) {
return { ok: true, json: async () => ({ base_models: ['SD 1.5', 'SDXL', 'Flux.1'] }) };
}
if (String(url).includes('/anomalous/compatible_models')) {
return { ok: true, json: async () => ({ models: [] }) };
}
if (String(url).includes('/anomalous/notebooks')) {
return { ok: true, json: async () => ({ notebooks: mockNotebooks }) };
}
return { ok: true, json: async () => ({ status: 'success' }) };
};
const notes = await f.module('ui_notebooks.js');
const owner = {
currentNotebook: null,
nbListEl: f.document.createElement('div'),
nbEditor: f.document.createElement('div'),
saveCurrentNotebook: async () => true,
deleteCurrentNotebook: async () => {},
sendNotebookToCanvas: () => {},
fillNotebookGalleries: () => {},
};
owner.renderNotebookEditor = notes.renderNotebookEditor.bind(owner);
owner.refreshNotebooks = notes.refreshNotebooks.bind(owner);
// 1. Test refreshNotebooks with autoOpenFirst=true
await owner.refreshNotebooks(true);
assert.ok(owner.currentNotebook, 'First notebook should be selected');
assert.equal(owner.currentNotebook.filename, '1.json');
assert.ok(owner.nbEditor.children.length >= 4, 'nbEditor should contain at least 4 main sections');
const fold = owner.nbEditor.querySelector('.anomalous-nb-models-fold');
assert.ok(fold, 'Companion models fold should exist in nbEditor');
assert.equal(fold.open, true, 'Companion models fold should be open by default');
const promptSection = owner.nbEditor.querySelector('.anomalous-nb-prompt-section');
assert.ok(promptSection, 'Prompt section should exist');
// 2. Test refreshNotebooks when list is empty
mockNotebooks = [];
owner.currentNotebook = null;
await owner.refreshNotebooks(false);
assert.ok(owner.nbEditor.textContent.includes('提示词笔记') || owner.nbEditor.textContent.includes('Prompt Notes') || owner.nbEditor.textContent.includes('暂无笔记'), 'Empty state should be displayed when no notebooks exist');
console.log('notebook_render_test: passed successfully');
+30
View File
@@ -0,0 +1,30 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
const source = (await fs.readFile(new URL('../web/modules/ui_notebooks.js', import.meta.url), 'utf8'))
.replace(/^import .*;$/gm, '')
.replace("const t = (key, params) => translate(key, params);", 'const t = key => key;');
const { saveCurrentNotebook } = await import(`data:text/javascript;base64,${Buffer.from(source).toString('base64')}`);
const calls = [];
let release;
globalThis.fetch = async (_, options) => {
calls.push(JSON.parse(options.body));
if (calls.length === 1) await new Promise(resolve => { release = resolve; });
return { ok: true, json: async () => ({ status: 'success' }) };
};
const owner = { currentNotebook: { filename: 'one.json', data: { text: 'first' } }, refreshNotebooks: async () => {} };
const first = saveCurrentNotebook.call(owner);
owner.currentNotebook.data.text = 'second';
const second = saveCurrentNotebook.call(owner);
await new Promise(resolve => setImmediate(resolve));
assert.equal(calls.length, 1);
release();
assert.deepEqual(await Promise.all([first, second]), [true, true]);
assert.deepEqual(calls.map(item => item.data.text), ['first', 'second']);
globalThis.document = { createElement: () => ({ setAttribute() {} }) };
owner.nbEditor = { prepend(element) { element.isConnected = true; } };
globalThis.fetch = async () => ({ ok: false, json: async () => ({ status: 'error' }) });
assert.equal(await saveCurrentNotebook.call(owner), false);
assert.equal(owner.nbSaveStatus.textContent, 'notebookSaveError');
assert.equal(owner.currentNotebook.data.text, 'second');
console.log('notebook save ordering and failure: ok');
+45 -24
View File
@@ -216,8 +216,18 @@ export async function refreshNotebooks(autoOpenFirst = false) {
};
this.nbListEl.appendChild(item);
});
} else if (this.nbEditor && (!this.currentNotebook || !data.notebooks?.length)) {
this.nbEditor.innerHTML = `
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:#94a3b8; text-align:center; gap:14px; padding:40px;">
<span style="font-size:3.2rem;">📝</span>
<h3 style="margin:0; color:#f1f5f9; font-size:1.1rem;">${t('promptNotes') || '提示词笔记'}</h3>
<p style="margin:0; font-size:0.88rem; max-width:320px; line-height:1.5;">${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}</p>
</div>
`;
}
} catch (e) { }
} catch (e) {
console.error('[AMB] Error refreshing notebooks:', e);
}
}
@@ -270,6 +280,7 @@ export async function deleteCurrentNotebook(skipConfirm = false) {
export function renderNotebookEditor() {
try {
this.nbEditor.innerHTML = '';
if (!this.currentNotebook) return;
@@ -401,6 +412,33 @@ export function renderNotebookEditor() {
modelsLabel.append(modelsTitleLeft, arrowIcon);
modelsFold.append(modelsLabel, modelSection);
// Badges for main model and loras
const mainSelectedBadge = document.createElement('span');
mainSelectedBadge.className = 'anomalous-nb-selected-badge';
mainSelectedBadge.style.fontSize = '0.8rem';
mainSelectedBadge.style.color = '#c084fc';
const loraSelectedBadge = document.createElement('span');
loraSelectedBadge.className = 'anomalous-nb-selected-badge';
loraSelectedBadge.style.fontSize = '0.8rem';
loraSelectedBadge.style.color = '#fbbf24';
const updateModelsSummary = () => {
const arrow = modelsFold.open ? '▾' : '▸';
arrowIcon.textContent = arrow;
const baseInfo = 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}`;
if (mainSelectedBadge) {
mainSelectedBadge.textContent = data.mainModel?.filename ? `✓ ${data.mainModel.filename}` : (t('recipeDiffNone') || '未选择');
}
if (loraSelectedBadge) {
loraSelectedBadge.textContent = data.loras?.length ? `✓ ${data.loras.length} LoRA` : (t('recipeDiffNone') || '未选择');
}
};
this.updateNotebookModelsSummary = updateModelsSummary;
// Base Model
const baseRow = document.createElement('div');
baseRow.className = 'anomalous-nb-row anomalous-nb-base-row';
@@ -464,10 +502,6 @@ export function renderNotebookEditor() {
const mainLabel = document.createElement('strong');
mainLabel.textContent = `🎨 ${t('mainModel')}`;
const mainSelectedBadge = document.createElement('span');
mainSelectedBadge.className = 'anomalous-nb-selected-badge';
mainSelectedBadge.style.fontSize = '0.8rem';
mainSelectedBadge.style.color = '#c084fc';
mainRow.append(mainLabel, mainSelectedBadge);
const mainGallery = document.createElement('div');
@@ -488,10 +522,6 @@ export function renderNotebookEditor() {
const loraLabel = document.createElement('strong');
loraLabel.textContent = `⚡ LoRA ${t('loras') || '模型'}`;
const loraSelectedBadge = document.createElement('span');
loraSelectedBadge.className = 'anomalous-nb-selected-badge';
loraSelectedBadge.style.fontSize = '0.8rem';
loraSelectedBadge.style.color = '#fbbf24';
loraRow.append(loraLabel, loraSelectedBadge);
const loraGallery = document.createElement('div');
@@ -504,21 +534,6 @@ export function renderNotebookEditor() {
modelSection.appendChild(mainBox);
modelSection.appendChild(loraBox);
const updateModelsSummary = () => {
const arrow = modelsFold.open ? '▾' : '▸';
arrowIcon.textContent = arrow;
const baseInfo = 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}`;
if (mainSelectedBadge) {
mainSelectedBadge.textContent = data.mainModel?.filename ? `✓ ${data.mainModel.filename}` : (t('recipeDiffNone') || '未选择');
}
if (loraSelectedBadge) {
loraSelectedBadge.textContent = data.loras?.length ? `✓ ${data.loras.length} LoRA` : (t('recipeDiffNone') || '未选择');
}
};
this.updateNotebookModelsSummary = updateModelsSummary;
updateModelsSummary();
// Prompt Section
@@ -806,7 +821,13 @@ export function renderNotebookEditor() {
if (modelsFold.open) {
loadModels();
}
} 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>`;
}
}
}
+1 -1
View File
@@ -2741,7 +2741,7 @@ dialog.anomalous-gallery-viewer[open] {
}
.anomalous-nb-header-main h2 {
display: none;
font-size: 1.05rem;
}
}