From 44d2c824bcd6d8f1020ba2dc22f6fb37ff6a890e Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Fri, 18 Sep 2026 09:58:50 +0800 Subject: [PATCH] refactor(notebooks): optimize prompt notes UX with flat layout, sticky toolbar, quick jump navigation, and complete localization --- ARCHITECTURE.md | 13 +- tests/css_bundle_order.mjs | 2 +- web/modules/locales.js | 46 +- web/modules/ui_notebook_editor.js | 1241 +++++++++++++++-------------- web/modules/ui_notebooks.js | 411 ++++++---- web/styles/02-notebooks.css | 152 +++- 6 files changed, 1066 insertions(+), 799 deletions(-) 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 = `
?
`; - } - return thumb; - }; + const buildThumbHtml = (m) => { + if (m.preview_url) { + const isVid = m.preview_url.match(/\.mp4(?:&|$)/i) || m.preview_url.match(/\.webm(?:&|$)/i); + if (isVid) return ``; + return ``; + } + return `
?
`; + }; - 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 = 'No compatible main models found.'; - } 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)}
${escapeHtml(m.filename)}
`; + 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 = `${window.anomalous_browser_lang === 'zh' ? '未找到兼容的主模型。' : 'No compatible main models found.'}`; + } 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)}
${escapeHtml(m.filename)}
`; - 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(e => { }); }; - card.onmouseleave = () => { const v = card.querySelector('video'); if (v) { v.pause(); v.currentTime = 0; } }; - } + 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?.(); - buildMainDOM(models); // re-render just the main gallery - }; - mainGallery.appendChild(card); - }); - } - }; - buildMainDOM(d.models || []); - }); + card.onclick = () => { + data.mainModel = (data.mainModel && data.mainModel.filename === m.filename) ? null : m; + this.saveCurrentNotebook(); + this.updateNotebookModelsSummary?.(); + 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 = 'No compatible Loras found.'; - } 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'; // for badge positioning + 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 = `${window.anomalous_browser_lang === 'zh' ? '未找到兼容的 LoRA。' : 'No compatible Loras found.'}`; + } 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 = ''; + let badgeHtml = ''; + if (isSelected) { + badgeHtml = `
${loraIndex + 1}
`; + } + + card.innerHTML = `${badgeHtml}${buildThumbHtml(m)}
${escapeHtml(m.filename)}
`; + + 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) { - badgeHtml = `
${loraIndex + 1}
`; + data.loras = data.loras.filter(l => l.filename !== m.filename); + } else { + data.loras.push(m); } - - card.innerHTML = `${badgeHtml}${buildThumbHtml(m)}
${escapeHtml(m.filename)}
`; - - 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(e => { }); }; - 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); // re-render just the lora gallery - }; - loraGallery.appendChild(card); - }); - } - }; - buildLoraDOM(d.models || []); - }); - } + this.saveCurrentNotebook(); + this.updateNotebookModelsSummary?.(); + buildLoraDOM(models); + }; + loraGallery.appendChild(card); + }); + } + }; + buildLoraDOM(d.models || []); + }).catch(() => { }); +} diff --git a/web/modules/ui_notebooks.js b/web/modules/ui_notebooks.js index 5aeb9a8..27b3e26 100644 --- a/web/modules/ui_notebooks.js +++ b/web/modules/ui_notebooks.js @@ -1,9 +1,8 @@ /** * ui_notebooks.js - * Extracted Notebooks methods. + * Extracted Notebooks methods: modal layout, sidebar quick jump, and persistence. */ -import { app } from "../../../scripts/app.js"; import { translate } from './locales.js'; import { escapeHtml } from './safe_dom.js'; import { anomalousAlert, anomalousConfirm } from './ui_dialog.js'; @@ -11,173 +10,254 @@ import { showMaterialSaved } from './material_feedback.js'; const t = (key, params) => translate(key, params); -export async function showNotebooks() { - this.recipeDetailFinish?.('closed'); - this.modal?.classList.add('visible'); - if (typeof this.setActiveHeaderTab === 'function') this.setActiveHeaderTab(null); - if (this.nbPanel && this.nbPanel.style.display !== 'flex' && !this.workspaceReturnState) { - this.workspaceReturnState = Object.fromEntries([ - ['grid', this.grid], ['detail', this.detailPanel], ['gallery', this.galleryPanel], - ['doctor', this.doctorPanel], ['assistant', this.assistantPanel], - ].filter(([, panel]) => panel).map(([key, panel]) => [key, panel.style.display])); +/** + * Build sidebar containing notebook list and floor anchor navigation. + */ +function createNotebookSidebar(ctx) { + const sidebar = document.createElement('div'); + sidebar.className = 'anomalous-nb-sidebar'; + + // Notes Group (Top) + const notesGroup = document.createElement('div'); + notesGroup.className = 'anomalous-nb-sidebar-group anomalous-nb-sidebar-notes-group'; + + const btnRow = document.createElement('div'); + btnRow.className = 'anomalous-nb-create-row'; + btnRow.style.padding = '10px'; + btnRow.style.display = 'flex'; + btnRow.style.gap = '5px'; + + const createBtn = document.createElement('button'); + const createBtnHtml = `${t('createNotebook') || (window.anomalous_browser_lang === 'zh' ? '新建笔记' : 'New Note')}`; + createBtn.innerHTML = createBtnHtml; + createBtn.className = 'anomalous-btn-primary'; + + const createInput = document.createElement('input'); + createInput.className = 'anomalous-nb-create-input'; + createInput.type = 'text'; + createInput.placeholder = t('newNotebookName') || (window.anomalous_browser_lang === 'zh' ? '新笔记名称...' : 'New note name...'); + createInput.style.display = 'none'; + createInput.style.flex = '1'; + createInput.style.padding = '4px'; + createInput.style.background = '#222'; + createInput.style.color = '#fff'; + createInput.style.border = '1px solid #555'; + createInput.style.borderRadius = '4px'; + + createBtn.onclick = () => { + if (createInput.style.display === 'none') { + createInput.style.display = 'block'; + createBtn.innerHTML = '✓'; + createInput.focus(); + } else { + const name = createInput.value.trim(); + if (name) { + ctx.currentNotebook = { + filename: name + '.json', + name: name, + data: { baseModel: '', mainModel: null, loras: [], promptEn: '', promptZh: '' } + }; + ctx.saveCurrentNotebook(); + ctx.renderNotebookEditor(); + createInput.value = ''; + } + createInput.style.display = 'none'; + createBtn.innerHTML = createBtnHtml; } - for (const panel of [this.grid, this.detailPanel, this.galleryPanel, this.doctorPanel, this.assistantPanel, this.paramPanel]) { - if (panel) panel.style.display = 'none'; - } - if (this.materialContainer) this.materialContainer.style.display = 'none'; - if (this.recipeContainer) this.recipeContainer.style.display = 'none'; - if (this.notebookContainer) this.notebookContainer.style.display = 'flex'; - if (this.nbPanel) this.nbPanel.style.display = 'flex'; - if (this.nbInitialized) { - this.nbPanel.style.display = 'flex'; - if (this.notebookBody) this.notebookBody.style.display = 'flex'; - if (this.recipeView) this.recipeView.style.display = 'none'; - if (this.materialView) this.materialView.style.display = 'none'; - this.notebookNotesTab?.classList.add('active'); - this.notebookRecipesTab?.classList.remove('active'); - this.refreshNotebooks(true); - return; - } - this.nbInitialized = true; + }; - const nbContainer = document.createElement('div'); - nbContainer.className = 'anomalous-nb-container'; + btnRow.append(createInput, createBtn); - const nbHeader = document.createElement('div'); - nbHeader.className = 'anomalous-nb-header'; - const headerMain = document.createElement('div'); - headerMain.className = 'anomalous-nb-header-main'; - const heading = document.createElement('h2'); - heading.textContent = t('promptNotes') || (window.anomalous_browser_lang === 'zh' ? '提示词笔记' : 'Prompt Notes'); - headerMain.append(heading); - nbHeader.appendChild(headerMain); - const closeNb = document.createElement('span'); - closeNb.className = 'anomalous-nb-close'; - closeNb.innerHTML = '×'; - closeNb.onclick = () => this.closeWorkspace(); - nbHeader.appendChild(closeNb); + const nbList = document.createElement('div'); + nbList.className = 'anomalous-nb-list'; + notesGroup.append(btnRow, nbList); + ctx.nbListEl = nbList; - const body = document.createElement('div'); - body.className = 'anomalous-nb-body'; - this.notebookBody = body; - this.notebookContainer = nbContainer; + // Quick Jump Floor Navigation (Bottom) + const navGroup = document.createElement('div'); + navGroup.className = 'anomalous-nb-sidebar-group anomalous-nb-sidebar-nav-group'; - // Sidebar for notebooks list - const sidebar = document.createElement('div'); - sidebar.className = 'anomalous-nb-sidebar'; + const navHeader = document.createElement('div'); + navHeader.className = 'anomalous-nb-nav-header'; + navHeader.textContent = `🧭 ${t('nbNavHeading') || (window.anomalous_browser_lang === 'zh' ? '快速直达' : 'Quick Jump')}`; - const nbList = document.createElement('div'); - nbList.className = 'anomalous-nb-list'; + const navList = document.createElement('div'); + navList.className = 'anomalous-nb-nav-list'; - const btnRow = document.createElement('div'); - btnRow.className = 'anomalous-nb-create-row'; - btnRow.style.padding = '10px'; - btnRow.style.display = 'flex'; - btnRow.style.gap = '5px'; + const navSections = [ + { id: '#amb-nb-sec-models', icon: '📦', key: 'nbNavModels', zh: '配套模型', en: 'Companion Models' }, + { id: '#amb-nb-sec-prompt', icon: '✍️', key: 'nbNavPrompt', zh: '提示词编辑', en: 'Prompt Editor' }, + { id: '#amb-nb-sec-tools', icon: '🔍', key: 'nbNavTools', zh: '查找替换', en: 'Find & Replace' }, + { id: '#amb-nb-sec-archive', icon: '💾', key: 'nbNavArchive', zh: '素材归档', en: 'Save to Library' }, + ]; - const createBtn = document.createElement('button'); - const createBtnHtml = `${t('createNotebook')}`; - createBtn.innerHTML = createBtnHtml; - createBtn.className = 'anomalous-btn-primary'; - - const createInput = document.createElement('input'); - createInput.className = 'anomalous-nb-create-input'; - createInput.type = 'text'; - createInput.placeholder = t('newNotebookName'); - createInput.style.display = 'none'; - createInput.style.flex = '1'; - createInput.style.padding = '4px'; - createInput.style.background = '#222'; - createInput.style.color = '#fff'; - createInput.style.border = '1px solid #555'; - createInput.style.borderRadius = '4px'; - - createBtn.onclick = () => { - if (createInput.style.display === 'none') { - createInput.style.display = 'block'; - createBtn.innerHTML = '✓'; - createInput.focus(); - } else { - const name = createInput.value.trim(); - if (name) { - this.currentNotebook = { filename: name + '.json', name: name, data: { baseModel: '', mainModel: null, loras: [], promptEn: '', promptZh: '' } }; - this.saveCurrentNotebook(); - this.renderNotebookEditor(); - createInput.value = ''; - } - createInput.style.display = 'none'; - createBtn.innerHTML = createBtnHtml; + navSections.forEach(sec => { + const navItem = document.createElement('button'); + navItem.type = 'button'; + navItem.className = 'anomalous-nb-nav-item'; + navItem.dataset.targetId = sec.id; + navItem.innerHTML = `${sec.icon}${t(sec.key) || (window.anomalous_browser_lang === 'zh' ? sec.zh : sec.en)}`; + navItem.onclick = () => { + const target = ctx.nbEditor?.querySelector(sec.id); + if (target) { + target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; + navList.appendChild(navItem); + }); - btnRow.appendChild(createInput); - btnRow.appendChild(createBtn); - sidebar.appendChild(btnRow); - sidebar.appendChild(nbList); + navGroup.append(navHeader, navList); + sidebar.append(notesGroup, navGroup); - // Editor area - this.nbEditor = document.createElement('div'); - this.nbEditor.className = 'anomalous-nb-editor'; + // Setup scrollspy listener + setupNotebookScrollSpy(ctx, navList, navSections); + return sidebar; +} - body.appendChild(sidebar); - body.appendChild(this.nbEditor); +/** + * Setup scrollspy on the editor area to highlight current anchor in sidebar. + */ +function setupNotebookScrollSpy(ctx, navList, navSections) { + let isSpying = false; + if (!ctx.nbEditor) return; - nbContainer.appendChild(nbHeader); - nbContainer.appendChild(body); - - this.nbPanel.appendChild(nbContainer); - - this.nbListEl = nbList; - this.refreshNotebooks(true); - } - - - -export async function refreshNotebooks(autoOpenFirst = false) { - try { - const res = await fetch('/anomalous/notebooks'); - const data = await res.json(); - this.nbListEl.innerHTML = ''; - - if (data.notebooks && data.notebooks.length > 0) { - if (autoOpenFirst) { - if (!this.currentNotebook) { - this.currentNotebook = data.notebooks[0]; - } - if (this.currentNotebook) { - this.renderNotebookEditor(); + ctx.nbEditor.addEventListener('scroll', () => { + if (isSpying) return; + isSpying = true; + requestAnimationFrame(() => { + isSpying = false; + if (!ctx.nbEditor) return; + const editorRect = ctx.nbEditor.getBoundingClientRect(); + let currentSec = null; + navSections.forEach(sec => { + const el = ctx.nbEditor.querySelector(sec.id); + if (el) { + const rect = el.getBoundingClientRect(); + if (rect.top <= editorRect.top + 160) { + currentSec = sec.id; } } + }); + navList.querySelectorAll('.anomalous-nb-nav-item').forEach(btn => { + if (btn.dataset.targetId === currentSec) { + btn.classList.add('active'); + } else { + btn.classList.remove('active'); + } + }); + }); + }, { passive: true }); +} - data.notebooks.forEach(nb => { - const item = document.createElement('div'); - item.className = 'anomalous-nb-item'; - if (this.currentNotebook && this.currentNotebook.filename === nb.filename) { - item.classList.add('active'); - } - item.innerHTML = `${escapeHtml(nb.name)}`; - item.onclick = () => { - this.currentNotebook = nb; - this.renderNotebookEditor(); - this.refreshNotebooks(); - }; - 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) { - console.error('[AMB] Error refreshing notebooks:', e); - } +export async function showNotebooks() { + this.recipeDetailFinish?.('closed'); + this.modal?.classList.add('visible'); + if (typeof this.setActiveHeaderTab === 'function') this.setActiveHeaderTab(null); + if (this.nbPanel && this.nbPanel.style.display !== 'flex' && !this.workspaceReturnState) { + this.workspaceReturnState = Object.fromEntries([ + ['grid', this.grid], ['detail', this.detailPanel], ['gallery', this.galleryPanel], + ['doctor', this.doctorPanel], ['assistant', this.assistantPanel], + ].filter(([, panel]) => panel).map(([key, panel]) => [key, panel.style.display])); } + for (const panel of [this.grid, this.detailPanel, this.galleryPanel, this.doctorPanel, this.assistantPanel, this.paramPanel]) { + if (panel) panel.style.display = 'none'; + } + if (this.materialContainer) this.materialContainer.style.display = 'none'; + if (this.recipeContainer) this.recipeContainer.style.display = 'none'; + if (this.notebookContainer) this.notebookContainer.style.display = 'flex'; + if (this.nbPanel) this.nbPanel.style.display = 'flex'; + if (this.nbInitialized) { + this.nbPanel.style.display = 'flex'; + if (this.notebookBody) this.notebookBody.style.display = 'flex'; + if (this.recipeView) this.recipeView.style.display = 'none'; + if (this.materialView) this.materialView.style.display = 'none'; + this.notebookNotesTab?.classList.add('active'); + this.notebookRecipesTab?.classList.remove('active'); + this.refreshNotebooks(true); + return; + } + this.nbInitialized = true; + const nbContainer = document.createElement('div'); + nbContainer.className = 'anomalous-nb-container'; + const nbHeader = document.createElement('div'); + nbHeader.className = 'anomalous-nb-header'; + const headerMain = document.createElement('div'); + headerMain.className = 'anomalous-nb-header-main'; + const heading = document.createElement('h2'); + heading.textContent = t('promptNotes') || (window.anomalous_browser_lang === 'zh' ? '提示词笔记' : 'Prompt Notes'); + headerMain.append(heading); + nbHeader.appendChild(headerMain); + const closeNb = document.createElement('span'); + closeNb.className = 'anomalous-nb-close'; + closeNb.innerHTML = '×'; + closeNb.onclick = () => this.closeWorkspace(); + nbHeader.appendChild(closeNb); + + const body = document.createElement('div'); + body.className = 'anomalous-nb-body'; + this.notebookBody = body; + this.notebookContainer = nbContainer; + + // Editor area created first so scrollspy can attach safely + this.nbEditor = document.createElement('div'); + this.nbEditor.className = 'anomalous-nb-editor'; + + // Sidebar with dual group (notes list + quick jump) + const sidebar = createNotebookSidebar(this); + + body.append(sidebar, this.nbEditor); + nbContainer.append(nbHeader, body); + this.nbPanel.appendChild(nbContainer); + + this.refreshNotebooks(true); +} + +export async function refreshNotebooks(autoOpenFirst = false) { + try { + const res = await fetch('/anomalous/notebooks'); + const data = await res.json(); + if (!this.nbListEl) return; + this.nbListEl.innerHTML = ''; + + if (data.notebooks && data.notebooks.length > 0) { + if (autoOpenFirst) { + if (!this.currentNotebook) { + this.currentNotebook = data.notebooks[0]; + } + if (this.currentNotebook) { + this.renderNotebookEditor(); + } + } + + data.notebooks.forEach(nb => { + const item = document.createElement('div'); + item.className = 'anomalous-nb-item'; + if (this.currentNotebook && this.currentNotebook.filename === nb.filename) { + item.classList.add('active'); + } + item.innerHTML = `${escapeHtml(nb.name)}`; + item.onclick = () => { + this.currentNotebook = nb; + this.renderNotebookEditor(); + this.refreshNotebooks(); + }; + this.nbListEl.appendChild(item); + }); + } else if (this.nbEditor && (!this.currentNotebook || !data.notebooks?.length)) { + this.nbEditor.innerHTML = ` +
+ 📝 +

${t('promptNotes') || (window.anomalous_browser_lang === 'zh' ? '提示词笔记' : 'Prompt Notes')}

+

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

+
+ `; + } + } catch (e) { + console.error('[AMB] Error refreshing notebooks:', e); + } +} export async function saveCurrentNotebook() { if (!this.currentNotebook) return false; @@ -199,27 +279,28 @@ export async function saveCurrentNotebook() { this.nbSaveStatus.setAttribute('role', 'alert'); this.nbEditor.prepend(this.nbSaveStatus); } - if (this.nbSaveStatus) this.nbSaveStatus.textContent = t('notebookSaveError'); + if (this.nbSaveStatus) this.nbSaveStatus.textContent = t('notebookSaveError') || (window.anomalous_browser_lang === 'zh' ? '保存失败,请重试' : 'Failed to save note'); return false; } }); return this.notebookSaveQueue; } - export async function deleteCurrentNotebook(skipConfirm = false) { - if (!this.currentNotebook) return; - if (!skipConfirm && !confirm(t('deleteNotebook') + ' ?')) return; - try { - await this.notebookSaveQueue; - const response = await fetch('/anomalous/delete_notebook', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ filename: this.currentNotebook.filename }) - }); - if (!response.ok || (await response.json()).status !== 'success') throw new Error('notebook delete failed'); - this.currentNotebook = null; - this.nbEditor.innerHTML = ''; - this.refreshNotebooks(); - } catch (e) { } + if (!this.currentNotebook) return; + if (!skipConfirm && !confirm((t('deleteNotebook') || '删除笔记') + ' ?')) return; + try { + await this.notebookSaveQueue; + const response = await fetch('/anomalous/delete_notebook', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ filename: this.currentNotebook.filename }) + }); + if (!response.ok || (await response.json()).status !== 'success') throw new Error('notebook delete failed'); + this.currentNotebook = null; + this.nbEditor.innerHTML = ''; + this.refreshNotebooks(); + } catch (e) { + console.error('[AMB] Error deleting notebook:', e); } +} diff --git a/web/styles/02-notebooks.css b/web/styles/02-notebooks.css index f901f08..805506b 100644 --- a/web/styles/02-notebooks.css +++ b/web/styles/02-notebooks.css @@ -92,6 +92,85 @@ } } +.anomalous-nb-sidebar-group { + display: flex; + flex-direction: column; +} + +.anomalous-nb-sidebar-notes-group { + flex: 1; + min-height: 120px; + overflow: hidden; + display: flex; + flex-direction: column; +} + +.anomalous-nb-sidebar-nav-group { + flex-shrink: 0; + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 12px 10px; + background: rgba(0, 0, 0, 0.2); +} + +.anomalous-nb-nav-header { + font-size: 0.74rem; + font-weight: 700; + color: #94a3b8; + letter-spacing: 0.03em; + text-transform: uppercase; + margin-bottom: 8px; + padding-left: 4px; + user-select: none; +} + +.anomalous-nb-nav-list { + display: flex; + flex-direction: column; + gap: 4px; +} + +.anomalous-nb-nav-item { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + border-radius: 6px; + background: transparent; + border: 1px solid transparent; + color: #cbd5e1; + font-size: 0.82rem; + cursor: pointer; + text-align: left; + transition: all 0.15s ease; + user-select: none; + width: 100%; + box-sizing: border-box; +} + +.anomalous-nb-nav-item:hover { + background: rgba(255, 255, 255, 0.06); + color: #ffffff; +} + +.anomalous-nb-nav-item.active { + background: rgba(139, 92, 246, 0.16); + color: #c084fc; + font-weight: 600; + border-color: rgba(139, 92, 246, 0.35); +} + +.anomalous-nb-nav-icon { + font-size: 0.95rem; + flex-shrink: 0; +} + +.anomalous-nb-nav-text { + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .anomalous-nb-list { flex: 1; overflow-y: auto; @@ -148,7 +227,14 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + position: sticky; + top: 0; + z-index: 20; + background: var(--amb-bg-page, #18181b); + padding: 12px 20px; + margin: -20px -20px 16px -20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); } .anomalous-nb-prompt-toolbar { @@ -359,15 +445,65 @@ .anomalous-nb-textarea { width: 100%; - height: 60px; - background: #111; - color: #fff; - border: 1px solid #555; - border-radius: 4px; - padding: 8px; - margin-top: 10px; + min-height: 140px; + max-height: 320px; + background: #121316; + color: #f1f5f9; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + padding: 10px 12px; + margin-top: 6px; resize: vertical; box-sizing: border-box; + font-family: inherit; + font-size: 0.88rem; + line-height: 1.5; + outline: none; + transition: border-color 0.18s ease; +} + +.anomalous-nb-textarea:focus { + border-color: rgba(139, 92, 246, 0.5); +} + +/* Flat Card Containers */ +.anomalous-nb-card { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 10px; + padding: 16px; + margin-bottom: 16px; + scroll-margin-top: 70px; +} + +.anomalous-nb-card-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; + font-size: 0.92rem; + font-weight: 600; + color: #f1f5f9; +} + +.anomalous-nb-hint { + margin: 0 0 12px 0; + color: #94a3b8; + font-size: 0.82rem; + line-height: 1.4; +} + +.anomalous-nb-tools-bar { + display: flex; + gap: 8px; + margin-top: 10px; + align-items: center; + flex-wrap: wrap; + padding: 8px 12px; + background: rgba(0, 0, 0, 0.22); + border: 1px solid rgba(255, 255, 255, 0.06); + border-radius: 8px; + scroll-margin-top: 70px; } /* 提示词双语对照面板 (CSS Grid 双栏排版) */