From d596af293c983c357dffe8aac1a217d447dfae85 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Wed, 16 Sep 2026 21:18:23 +0800 Subject: [PATCH] fix(notebooks): resolve TDZ ReferenceError on updateModelsSummary and empty state rendering --- .gitignore | 2 + ARCHITECTURE.md | 3 +- tests/notebook_render_test.mjs | 53 ++++++++++++++++++++++++ tests/notebook_save_roundtrip.mjs | 30 ++++++++++++++ web/modules/ui_notebooks.js | 69 ++++++++++++++++++++----------- web/styles.css | 2 +- 6 files changed, 133 insertions(+), 26 deletions(-) create mode 100644 tests/notebook_render_test.mjs create mode 100644 tests/notebook_save_roundtrip.mjs diff --git a/.gitignore b/.gitignore index 0ba5a2f..4d2d567 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 551c050..3e10eb0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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, diff --git a/tests/notebook_render_test.mjs b/tests/notebook_render_test.mjs new file mode 100644 index 0000000..5b23b77 --- /dev/null +++ b/tests/notebook_render_test.mjs @@ -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'); diff --git a/tests/notebook_save_roundtrip.mjs b/tests/notebook_save_roundtrip.mjs new file mode 100644 index 0000000..3e8b746 --- /dev/null +++ b/tests/notebook_save_roundtrip.mjs @@ -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'); diff --git a/web/modules/ui_notebooks.js b/web/modules/ui_notebooks.js index 64f2469..bf613bb 100644 --- a/web/modules/ui_notebooks.js +++ b/web/modules/ui_notebooks.js @@ -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 = ` +
+ 📝 +

${t('promptNotes') || '提示词笔记'}

+

${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}

+
+ `; } - } 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 = `
Render error: ${escapeHtml(err?.message || String(err))}
`; + } } +} diff --git a/web/styles.css b/web/styles.css index 56c00e4..ac976ed 100644 --- a/web/styles.css +++ b/web/styles.css @@ -2741,7 +2741,7 @@ dialog.anomalous-gallery-viewer[open] { } .anomalous-nb-header-main h2 { - display: none; + font-size: 1.05rem; } }