diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 3eeb75f..d100498 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -112,13 +112,14 @@ DOM or live LiteGraph state.
own their respective workspace surfaces and persistence flows. `ui_recipes.js` owns
the Workflow Recipe studio catalog workspace with search/filter tags, grid/list layout toggle,
streamlined action header (preserving active workflow saving and closing while pruning unfinished package
- import entrypoints), and card browsing. `ui_notebooks.js` owns Prompt Note catalog and
- persistence, `ui_notebook_editor.js` owns editing/model galleries, and
+ import entrypoints), and card browsing. `ui_notebooks.js` owns Prompt Note catalog,
+ sidebar dual-group management (note list + floor quick jump anchor navigation with scrollspy active tracking),
+ and persistence; `ui_notebook_editor.js` owns unfolded card editing (modularized into single-responsibility
+ sub-functions adhering to the 50-line rule: sticky top action toolbar, unfolded companion models card,
+ prompt composer with compact inline find & replace toolbar, and flat material library archiving card with
+ clean single-icon feedback and complete bilingual dictionary coverage in `locales.js`), and
`notebook_canvas.js` owns LiteGraph creation. Prompt Notes are 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, guarded by Temporal Dead Zone (TDZ)
- safe summary initialization and responsive empty-state fallback rendering. `ui_recipe_detail.js`
+ with defensive workspace return state restoration, TDZ-safe summary initialization, and responsive empty-state fallback rendering. `ui_recipe_detail.js`
coordinates the Workflow Recipe detail session and model composition. `ui_recipe_overview.js`
owns the Overview prompt showcase (with `entry.text` fallback), and `ui_recipe_parameters.js`
owns the responsive Parameter Presets workspace (featuring default-expanded raw node parameter inspection,
diff --git a/tests/css_bundle_order.mjs b/tests/css_bundle_order.mjs
index ed5a814..6c83c16 100644
--- a/tests/css_bundle_order.mjs
+++ b/tests/css_bundle_order.mjs
@@ -15,6 +15,6 @@ assert.equal(entry.replace(/@import[^;]+;\s*/g, ''), '', 'styles.css must remain
const chunks = await Promise.all(imports.map(match => fs.readFile(path.join(root, 'web/styles', match[1]))));
const digest = crypto.createHash('sha256').update(Buffer.concat(chunks)).digest('hex');
-assert.equal(digest, 'd014a9d1826e2c48a46ac72389fe4e5bdd0b2ba85618f653d13852cd93812bc1', 'CSS rule bytes or cascade order changed');
+assert.equal(digest, '81ffe429c2512756e1614cd10aa073670c18c18f3dc84e5016ffe330d59e07fb', 'CSS rule bytes or cascade order changed');
console.log('CSS bundle: ordered imports preserve the original stylesheet byte-for-byte.');
diff --git a/web/modules/locales.js b/web/modules/locales.js
index beb9238..37ed69a 100644
--- a/web/modules/locales.js
+++ b/web/modules/locales.js
@@ -184,7 +184,28 @@ export const i18n = {
promptAdjustRole: "调整用途",
notebookCompanionModels: "配套模型",
notebookMore: "更多",
- notebookDoneEditing: "查看词条与翻译",
+ notebookDoneEditing: "📑 查看双语分词",
+ editRaw: "✏️ 编辑原始文本",
+ saveNotebook: "保存笔记",
+ deleteNotebook: "删除笔记",
+ delSure: "确认删除?",
+ sendToCanvas: "发送到画布",
+ createNotebook: "新建笔记",
+ newNotebookName: "新笔记名称...",
+ recipeSearchAndReplace: "词条替换与目标语言",
+ findPlaceholder: "查找...",
+ replacePlaceholder: "替换为...",
+ replaceAll: "全部替换",
+ recipeDiffNone: "未选择",
+ loras: "LoRA 模型",
+ baseModel: "底模",
+ mainModel: "主模型",
+ nbNavHeading: "快速直达",
+ nbNotesHeading: "我的笔记",
+ nbNavModels: "配套模型",
+ nbNavPrompt: "提示词编辑",
+ nbNavTools: "查找替换",
+ nbNavArchive: "素材归档",
notebookPromptTitle: "提示词",
materialSearchHint: "搜索名称、标签或节点类型",
materialTags: "标签",
@@ -1351,7 +1372,28 @@ export const i18n = {
promptAdjustRole: "Adjust role",
notebookCompanionModels: "Companion models",
notebookMore: "More",
- notebookDoneEditing: "View tags and translations",
+ notebookDoneEditing: "📑 View Bilingual Tags",
+ editRaw: "✏️ Edit Raw Prompt",
+ saveNotebook: "Save Note",
+ deleteNotebook: "Delete Note",
+ delSure: "Confirm Delete?",
+ sendToCanvas: "Send to Canvas",
+ createNotebook: "New Note",
+ newNotebookName: "New note name...",
+ recipeSearchAndReplace: "Find, Replace & Language",
+ findPlaceholder: "Find...",
+ replacePlaceholder: "Replace with...",
+ replaceAll: "Replace All",
+ recipeDiffNone: "None",
+ loras: "LoRA Models",
+ baseModel: "Base Model",
+ mainModel: "Main Model",
+ nbNavHeading: "Quick Jump",
+ nbNotesHeading: "My Notes",
+ nbNavModels: "Companion Models",
+ nbNavPrompt: "Prompt Editor",
+ nbNavTools: "Find & Replace",
+ nbNavArchive: "Save to Library",
notebookPromptTitle: "Prompt",
materialSearchHint: "Search names, tags or node types",
materialTags: "Tags",
diff --git a/web/modules/ui_notebook_editor.js b/web/modules/ui_notebook_editor.js
index 42303d0..001f814 100644
--- a/web/modules/ui_notebook_editor.js
+++ b/web/modules/ui_notebook_editor.js
@@ -1,4 +1,7 @@
-/** Prompt Note editor and compatible-model galleries. */
+/**
+ * ui_notebook_editor.js
+ * Prompt Note editor with unfolded cards, sticky toolbar, and compatible-model galleries.
+ */
import { translate } from './locales.js';
import { escapeHtml } from './safe_dom.js';
@@ -7,548 +10,554 @@ import { showMaterialSaved } from './material_feedback.js';
const t = (key, params) => translate(key, params);
+/**
+ * Sticky top action toolbar for the active notebook.
+ */
+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.innerHTML = `💾 ${t('saveNotebook') || (window.anomalous_browser_lang === 'zh' ? '保存笔记' : 'Save Note')}`;
+ 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);
+ };
+
+ const sendBtn = document.createElement('button');
+ sendBtn.type = 'button';
+ sendBtn.innerHTML = `🚀 ${t('sendToCanvas') || (window.anomalous_browser_lang === 'zh' ? '发送到画布' : 'Send to Canvas')}`;
+ sendBtn.className = 'anomalous-btn-success';
+ sendBtn.onclick = () => ctx.sendNotebookToCanvas();
+
+ let delTimer = null;
+ const delContainer = document.createElement('span');
+ delContainer.style.display = 'inline-flex';
+ delContainer.style.alignItems = 'center';
+
+ const delBtn = document.createElement('button');
+ delBtn.type = 'button';
+ delBtn.innerHTML = t('deleteNotebook') || (window.anomalous_browser_lang === 'zh' ? '删除笔记' : 'Delete Note');
+ delBtn.className = 'anomalous-btn-danger';
+
+ const cancelDelBtn = document.createElement('button');
+ cancelDelBtn.type = 'button';
+ cancelDelBtn.innerHTML = '✕';
+ cancelDelBtn.className = 'anomalous-btn-danger';
+ cancelDelBtn.style.display = 'none';
+ cancelDelBtn.style.background = '#555';
+ cancelDelBtn.style.marginLeft = '2px';
+ cancelDelBtn.style.padding = '6px 8px';
+
+ delContainer.appendChild(delBtn);
+ delContainer.appendChild(cancelDelBtn);
+
+ const resetDel = () => {
+ clearTimeout(delTimer);
+ delBtn.innerHTML = t('deleteNotebook') || (window.anomalous_browser_lang === 'zh' ? '删除笔记' : 'Delete Note');
+ delBtn.style.background = '';
+ cancelDelBtn.style.display = 'none';
+ };
+
+ delBtn.onclick = () => {
+ const deleteText = t('deleteNotebook') || (window.anomalous_browser_lang === 'zh' ? '删除笔记' : 'Delete Note');
+ if (delBtn.innerHTML === deleteText) {
+ delBtn.innerHTML = t('delSure') || (window.anomalous_browser_lang === 'zh' ? '确认删除?' : 'Confirm Delete?');
+ delBtn.style.background = '#800';
+ cancelDelBtn.style.display = 'block';
+ delTimer = setTimeout(resetDel, 4000);
+ } else {
+ resetDel();
+ ctx.deleteCurrentNotebook(true);
+ }
+ };
+ cancelDelBtn.onclick = resetDel;
+
+ const moreActions = document.createElement('details');
+ moreActions.className = 'anomalous-secondary-actions';
+ const moreSummary = document.createElement('summary');
+ moreSummary.textContent = t('notebookMore') || (window.anomalous_browser_lang === 'zh' ? '更多' : 'More');
+ moreActions.append(moreSummary, delContainer);
+
+ rightBtns.append(saveBtn, sendBtn, moreActions);
+ 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 = '#c084fc';
+
+ const loraSelectedBadge = document.createElement('span');
+ loraSelectedBadge.className = 'anomalous-nb-selected-badge';
+ loraSelectedBadge.style.color = '#fbbf24';
+
+ const updateSummary = () => {
+ 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}`;
+ 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 = '#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;
+ 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 => {
+ if (d.base_models && d.base_models.length > 0) {
+ ctx.baseModelsCache = d.base_models;
+ 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 = `⚡ LoRA ${t('loras') || (window.anomalous_browser_lang === 'zh' ? '模型' : 'Models')}`;
+ loraRow.append(loraLabel, loraSelectedBadge);
+ const loraGallery = document.createElement('div');
+ loraGallery.className = 'anomalous-nb-gallery-wrap';
+ loraBox.append(loraRow, loraGallery);
+
+ 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 = `✍️ ${t('notebookPromptTitle') || (window.anomalous_browser_lang === 'zh' ? '提示词' : 'Prompt')}`;
+
+ 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 = window.anomalous_browser_lang === 'zh'
+ ? '在此输入提示词内容(英文词条用逗号分隔,支持自动双语切词翻译)...'
+ : 'Enter prompt here (comma-separated for tags, bilingual translation supported)...';
+
+ const dualPane = document.createElement('div');
+ dualPane.className = 'anomalous-nb-dual-pane';
+ dualPane.style.display = 'none';
+
+ if (!data.translations) data.translations = {};
+
+ let visualDebounceTimer = null;
+ const updateVisualTags = () => {
+ const txt = rawArea.value;
+ data.promptEn = txt;
+ ctx.saveCurrentNotebook();
+ 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;
+ ctx.saveCurrentNotebook();
+ }
+ }).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 = () => {
+ clearTimeout(ctx.pTimeout);
+ data.promptEn = rawArea.value;
+ ctx.pTimeout = setTimeout(() => ctx.saveCurrentNotebook(), 500);
+
+ 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;
+}
+
+/**
+ * Flat Material Library archive card (unfolded bottom card).
+ */
+function createArchiveCard(ctx, currentNotebook, data) {
+ const card = document.createElement('div');
+ card.id = 'amb-nb-sec-archive';
+ card.className = 'anomalous-nb-card anomalous-nb-archive-card';
+
+ const header = document.createElement('div');
+ header.className = 'anomalous-nb-card-header';
+ const title = document.createElement('span');
+ const rawShort = t('materialSaveSnapshotShort') || (window.anomalous_browser_lang === 'zh' ? '保存到素材库' : 'Save to Library');
+ const cleanShort = rawShort.replace(/^💾\s*/, '');
+ title.innerHTML = `💾 ${cleanShort}`;
+ header.appendChild(title);
+
+ const hint = document.createElement('p');
+ hint.className = 'anomalous-nb-hint';
+ hint.textContent = t('materialNoteScopeHint') || (window.anomalous_browser_lang === 'zh'
+ ? '将当前笔记归档保存至素材库中,便于随处调用与复用。'
+ : 'Archive this prompt note to the material library for quick reuse.');
+
+ const actions = document.createElement('div');
+ actions.className = 'anomalous-nb-archive-actions';
+ actions.style.display = 'flex';
+ actions.style.gap = '10px';
+ actions.style.flexWrap = 'wrap';
+
+ const sourceFilename = currentNotebook.filename;
+ const sourceName = currentNotebook.name;
+
+ const scopes = [
+ ['note', 'materialSaveNoteBundle', '📦 保存整篇笔记 (含配套模型与提示词)', '📦 Save Note Bundle'],
+ ['prompt', 'materialSavePromptText', '📝 仅保存提示词文本', '📝 Save Prompt Text Only']
+ ];
+
+ for (const [scope, key, defaultZh, defaultEn] of scopes) {
+ const saveMaterial = document.createElement('button');
+ saveMaterial.type = 'button';
+ saveMaterial.className = 'anomalous-btn-primary';
+ saveMaterial.textContent = t(key) || (window.anomalous_browser_lang === 'zh' ? defaultZh : defaultEn);
+ saveMaterial.onclick = async () => {
+ saveMaterial.disabled = true;
+ const body = JSON.parse(JSON.stringify({
+ notebook_filename: sourceFilename,
+ name: String(sourceName || t('notebookPromptTitle')).slice(0, 120),
+ scope,
+ note: data
+ }));
+ const send = () => fetch('/anomalous/save_prompt_note_material', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
+ });
+ try {
+ let response = await send();
+ if (response.status === 409) {
+ const duplicate = await response.json();
+ if (duplicate.status !== 'duplicate') throw new Error('material conflict');
+ if (!await anomalousConfirm(t('materialDuplicateConfirm', { name: duplicate.name }))) return;
+ body.allow_duplicate = true;
+ response = await send();
+ }
+ const result = await response.json();
+ if (!response.ok || result.status !== 'success') throw new Error('material save failed');
+ showMaterialSaved(ctx, result.material);
+ await ctx.refreshMaterials?.();
+ } catch (error) {
+ await anomalousAlert(t('materialSaveError') || (window.anomalous_browser_lang === 'zh' ? '保存到素材库失败' : 'Failed to save to material library'));
+ } finally {
+ saveMaterial.disabled = false;
+ }
+ };
+ actions.appendChild(saveMaterial);
+ }
+
+ card.append(header, hint, actions);
+ return card;
+}
+
+/**
+ * Primary notebook editor render entrypoint.
+ */
export function renderNotebookEditor() {
try {
this.nbEditor.innerHTML = '';
if (!this.currentNotebook) return;
- const data = this.currentNotebook.data || {};
+ const data = this.currentNotebook.data || (this.currentNotebook.data = {});
if (!data.loras) data.loras = [];
- // Toolbar
- const tb = document.createElement('div');
- tb.className = 'anomalous-nb-toolbar';
+ const tb = createNotebookToolbar(this, this.currentNotebook);
+ const modelsCard = createCompanionModelsCard(this, data);
+ const promptSec = createPromptSection(this, data);
+ const archiveCard = createArchiveCard(this, this.currentNotebook, data);
- 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 = this.currentNotebook.name;
- titleArea.style.margin = '0';
- titleArea.style.fontSize = '1.2rem';
- titleArea.style.fontWeight = '700';
- titleBox.append(titleIcon, titleArea);
-
- const rightBtns = document.createElement('div');
- rightBtns.className = 'anomalous-notebook-actions';
-
- const saveBtn = document.createElement('button');
- saveBtn.innerHTML = t('saveNotebook');
- saveBtn.className = 'anomalous-btn-primary';
- saveBtn.onclick = async () => {
- const orig = saveBtn.innerHTML;
- saveBtn.innerHTML = '⏳...';
- const saved = await this.saveCurrentNotebook();
- if (!saved) { saveBtn.innerHTML = orig; return; }
- saveBtn.innerHTML = '✅';
- saveBtn.style.background = '#2e8b57';
- setTimeout(() => {
- saveBtn.innerHTML = orig;
- saveBtn.style.background = '';
- }, 1500);
- };
-
- let delTimer = null;
- const delContainer = document.createElement('span');
- delContainer.style.display = 'inline-flex';
- delContainer.style.alignItems = 'center';
-
- const delBtn = document.createElement('button');
- delBtn.innerHTML = t('deleteNotebook');
- delBtn.className = 'anomalous-btn-danger';
-
- const cancelDelBtn = document.createElement('button');
- cancelDelBtn.innerHTML = '✕';
- cancelDelBtn.className = 'anomalous-btn-danger';
- cancelDelBtn.style.display = 'none';
- cancelDelBtn.style.background = '#555';
- cancelDelBtn.style.marginLeft = '2px';
- cancelDelBtn.style.padding = '6px 8px';
-
- delContainer.appendChild(delBtn);
- delContainer.appendChild(cancelDelBtn);
-
- const resetDel = () => {
- clearTimeout(delTimer);
- delBtn.innerHTML = t('deleteNotebook');
- delBtn.style.background = '';
- cancelDelBtn.style.display = 'none';
- };
-
- delBtn.onclick = () => {
- if (delBtn.innerHTML === t('deleteNotebook')) {
- delBtn.innerHTML = t('delSure');
- delBtn.style.background = '#800';
- cancelDelBtn.style.display = 'block';
- delTimer = setTimeout(resetDel, 4000);
- } else {
- resetDel();
- this.deleteCurrentNotebook(true);
- }
- };
-
- cancelDelBtn.onclick = resetDel;
-
- const sendBtn = document.createElement('button');
- sendBtn.innerHTML = t('sendToCanvas');
- sendBtn.className = 'anomalous-btn-success';
- sendBtn.onclick = () => this.sendNotebookToCanvas();
-
- rightBtns.appendChild(saveBtn);
- rightBtns.appendChild(sendBtn);
- const moreActions = document.createElement('details');
- moreActions.className = 'anomalous-secondary-actions';
- const moreSummary = document.createElement('summary');
- moreSummary.textContent = t('notebookMore');
- moreActions.append(moreSummary, delContainer);
- rightBtns.appendChild(moreActions);
-
- tb.appendChild(titleBox);
- tb.appendChild(rightBtns);
-
- // Settings / Models
- const modelSection = document.createElement('div');
- modelSection.className = 'anomalous-nb-section';
- const modelsFold = document.createElement('details');
- modelsFold.className = 'anomalous-notebook-fold anomalous-nb-models-fold';
- modelsFold.open = true;
-
- const modelsLabel = document.createElement('summary');
- modelsLabel.className = 'anomalous-nb-fold-summary';
-
- const modelsTitleLeft = document.createElement('div');
- modelsTitleLeft.style.display = 'flex';
- modelsTitleLeft.style.alignItems = 'center';
- modelsTitleLeft.style.gap = '8px';
-
- const modelsTitleText = document.createElement('span');
- modelsTitleText.textContent = `📦 ${t('notebookCompanionModels')}`;
- modelsTitleText.style.fontWeight = '600';
-
- const modelsBadge = document.createElement('span');
- modelsBadge.className = 'anomalous-nb-models-badge';
-
- modelsTitleLeft.append(modelsTitleText, modelsBadge);
-
- const arrowIcon = document.createElement('span');
- arrowIcon.className = 'anomalous-nb-fold-arrow';
-
- 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';
- 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 = '#cbd5e1';
- baseTitle.textContent = `${t('baseModel')}:`;
- 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;
- if (data.baseModel === b) opt.selected = true;
- baseSelect.appendChild(opt);
- });
- if (!data.baseModel && bases.length > 0) data.baseModel = bases[0];
- updateModelsSummary();
- };
-
- if (this.baseModelsCache) {
- buildSelect(this.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 => {
- if (d.base_models && d.base_models.length > 0) {
- this.baseModelsCache = d.base_models;
- buildSelect(this.baseModelsCache);
- }
- }).catch(e => { });
- }
- if (!data.baseModel) data.baseModel = 'SDXL';
- baseSelect.onchange = () => {
- data.baseModel = baseSelect.value;
- data.mainModel = null;
- data.loras = [];
- this.saveCurrentNotebook();
- this.renderNotebookEditor();
- };
- baseRow.appendChild(baseSelect);
-
- // Main Model (Card Selection)
- 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')}`;
- mainRow.append(mainLabel, mainSelectedBadge);
-
- const mainGallery = document.createElement('div');
- mainGallery.className = 'anomalous-nb-gallery-wrap';
-
- mainBox.appendChild(mainRow);
- mainBox.appendChild(mainGallery);
-
- // Loras (Card Selection)
- 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 = `⚡ LoRA ${t('loras') || '模型'}`;
- loraRow.append(loraLabel, loraSelectedBadge);
-
- const loraGallery = document.createElement('div');
- loraGallery.className = 'anomalous-nb-gallery-wrap';
-
- loraBox.appendChild(loraRow);
- loraBox.appendChild(loraGallery);
-
- modelSection.appendChild(baseRow);
- modelSection.appendChild(mainBox);
- modelSection.appendChild(loraBox);
-
- updateModelsSummary();
-
- // Prompt Section
- const promptSec = document.createElement('div');
- promptSec.className = 'anomalous-nb-section anomalous-nb-prompt-section';
-
- // Toolbar
- const pToolbar = document.createElement('div');
- pToolbar.className = 'anomalous-nb-prompt-toolbar';
-
- 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 = {}; // Clear translation cache on lang change
- this.saveCurrentNotebook();
- updateVisualTags();
- };
-
- const findInput = document.createElement('input');
- findInput.className = 'anomalous-nb-select';
- findInput.placeholder = t('findPlaceholder');
- findInput.style.flex = '1';
-
- const replaceInput = document.createElement('input');
- replaceInput.className = 'anomalous-nb-select';
- replaceInput.placeholder = t('replacePlaceholder');
- replaceInput.style.flex = '1';
-
- const replaceBtn = document.createElement('button');
- replaceBtn.className = 'anomalous-btn-primary';
- replaceBtn.innerHTML = t('replaceAll');
-
- pToolbar.appendChild(langSelect);
- pToolbar.appendChild(findInput);
- pToolbar.appendChild(replaceInput);
- pToolbar.appendChild(replaceBtn);
-
- // Raw Input Toggle
- const toggleRow = document.createElement('div');
- toggleRow.style.display = 'flex';
- toggleRow.style.justifyContent = 'space-between';
- toggleRow.style.marginBottom = '5px';
- toggleRow.innerHTML = `${t('notebookPromptTitle')}`;
- const rawBtn = document.createElement('button');
- rawBtn.className = 'anomalous-btn-primary';
- rawBtn.textContent = t('notebookDoneEditing');
- toggleRow.appendChild(rawBtn);
-
- // Raw Textarea
- const rawArea = document.createElement('textarea');
- rawArea.className = 'anomalous-nb-textarea';
- rawArea.value = data.promptEn || '';
- rawArea.style.display = 'block';
- rawArea.setAttribute('aria-label', t('notebookPromptTitle'));
- rawArea.style.height = '150px';
-
- // Visual Dual Pane
- const dualPane = document.createElement('div');
- dualPane.className = 'anomalous-nb-dual-pane';
- dualPane.style.display = 'none';
-
- if (!data.translations) data.translations = {};
-
- let visualDebounceTimer = null;
- const updateVisualTags = () => {
- const txt = rawArea.value;
- data.promptEn = txt;
- this.saveCurrentNotebook();
- if (!txt.trim()) {
- dualPane.replaceChildren();
- return;
- }
-
- const tags = txt.split(',').map(s => s.trim()).filter(s => s);
- 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.appendChild(txtL);
- tagL.appendChild(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.appendChild(txtR);
- tagR.appendChild(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();
- this.saveCurrentNotebook();
- updateVisualTags();
- };
- inp.onblur = finish;
- inp.onkeydown = (e) => { if (e.key === 'Enter') inp.blur(); };
- };
-
- tagRow.appendChild(tagL);
- tagRow.appendChild(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;
- this.saveCurrentNotebook();
- }
- }).catch(() => { });
- }
- });
- dualPane.replaceChildren(fragment);
- };
-
- rawBtn.onclick = () => {
- if (rawArea.style.display === 'none') {
- rawArea.style.display = 'block';
- dualPane.style.display = 'none';
- rawBtn.textContent = t('notebookDoneEditing');
- } else {
- rawArea.style.display = 'none';
- dualPane.style.display = 'flex';
- rawBtn.textContent = t('editRaw');
- updateVisualTags();
- }
- };
-
- replaceBtn.onclick = () => {
- const findStr = findInput.value;
- const repStr = replaceInput.value;
- if (!findStr) return;
- const newTxt = rawArea.value.split(findStr).join(repStr);
- rawArea.value = newTxt;
- updateVisualTags();
- };
-
- rawArea.oninput = () => {
- clearTimeout(this.pTimeout);
- data.promptEn = rawArea.value;
- this.pTimeout = setTimeout(() => this.saveCurrentNotebook(), 500);
-
- if (dualPane.style.display !== 'none') {
- clearTimeout(visualDebounceTimer);
- visualDebounceTimer = setTimeout(() => {
- updateVisualTags();
- }, 300);
- }
- };
-
- const promptTools = document.createElement('details');
- promptTools.className = 'anomalous-notebook-fold anomalous-nb-tools-fold';
- const toolsLabel = document.createElement('summary');
- toolsLabel.textContent = `🔍 ${t('recipeSearchAndReplace') || (window.anomalous_browser_lang === 'zh' ? '词条替换与目标语言' : 'Find, Replace & Language')} ▾`;
- promptTools.append(toolsLabel, pToolbar);
-
- const capture = document.createElement('details');
- capture.className = 'anomalous-notebook-fold anomalous-nb-capture-fold';
- const captureLabel = document.createElement('summary');
- captureLabel.textContent = `💾 ${t('materialSaveSnapshotShort')} ▾`;
- const captureContent = document.createElement('div');
- captureContent.className = 'anomalous-nb-capture-content';
- const captureHint = document.createElement('p');
- captureHint.style.margin = '0 0 10px 0';
- captureHint.style.color = '#94a3b8';
- captureHint.style.fontSize = '0.82rem';
- captureHint.textContent = t('materialNoteScopeHint');
- captureContent.append(captureHint);
- const sourceFilename = this.currentNotebook.filename;
- const sourceName = this.currentNotebook.name;
- for (const [scope, key] of [['note', 'materialSaveNoteBundle'], ['prompt', 'materialSavePromptText']]) {
- const saveMaterial = document.createElement('button');
- saveMaterial.type = 'button';
- saveMaterial.className = 'anomalous-btn-ghost';
- saveMaterial.textContent = t(key);
- saveMaterial.onclick = async () => {
- saveMaterial.disabled = true;
- // Capture immediately, including text entered before the autosave timer fires.
- const body = JSON.parse(JSON.stringify({ notebook_filename: sourceFilename,
- name: String(sourceName || t('notebookPromptTitle')).slice(0, 120), scope, note: data }));
- const send = () => fetch('/anomalous/save_prompt_note_material', {
- method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
- });
- try {
- let response = await send();
- if (response.status === 409) {
- const duplicate = await response.json();
- if (duplicate.status !== 'duplicate') throw new Error('material conflict');
- if (!await anomalousConfirm(t('materialDuplicateConfirm', { name: duplicate.name }))) return;
- body.allow_duplicate = true;
- response = await send();
- }
- const result = await response.json();
- if (!response.ok || result.status !== 'success') throw new Error('material save failed');
- showMaterialSaved(this, result.material);
- await this.refreshMaterials?.();
- } catch (error) {
- await anomalousAlert(t('materialSaveError'));
- } finally { saveMaterial.disabled = false; }
- };
- captureContent.appendChild(saveMaterial);
- }
- capture.append(captureLabel, captureContent);
-
- promptSec.appendChild(toggleRow);
- promptSec.appendChild(rawArea);
- promptSec.appendChild(dualPane);
- promptSec.appendChild(promptTools);
-
- // Put companion models at the very top under the toolbar
- this.nbEditor.replaceChildren(tb, modelsFold, promptSec, capture);
-
- // Fetch compatible models and fill galleries
- let modelsLoaded = false;
- const loadModels = () => {
- if (modelsLoaded) return;
- modelsLoaded = true;
- this.fillNotebookGalleries(data.baseModel, mainGallery, loraGallery, data);
- };
- modelsFold.ontoggle = () => {
- if (this.updateNotebookModelsSummary) this.updateNotebookModelsSummary();
- if (modelsFold.open) loadModels();
- };
- if (modelsFold.open) {
- loadModels();
- }
+ this.nbEditor.replaceChildren(tb, modelsCard, promptSec, archiveCard);
} catch (err) {
console.error('[AMB] Error rendering notebook editor:', err);
if (this.nbEditor) {
@@ -557,94 +566,92 @@ export function renderNotebookEditor() {
}
}
-
-
+/**
+ * Fill compatible main models and Loras into horizontal galleries.
+ */
export function fillNotebookGalleries(baseModel, mainGallery, loraGallery, data) {
- if (!baseModel) return;
+ if (!baseModel) return;
- const buildThumbHtml = (m) => {
- let thumb = '';
- if (m.preview_url) {
- const isVid = m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i);
- if (isVid) thumb = ``;
- else thumb = ``;
- } else {
- thumb = `
${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}
-${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}
+