/** * ui_notebooks.js * Extracted Notebooks methods: modal layout, sidebar quick jump, and persistence. */ import { translate } from './locales.js'; import { escapeHtml } from './safe_dom.js'; import { anomalousAlert, anomalousConfirm } from './ui_dialog.js'; import { showMaterialSaved } from './material_feedback.js'; const t = (key, params) => translate(key, params); /** * 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; } }; btnRow.append(createInput, createBtn); const nbList = document.createElement('div'); nbList.className = 'anomalous-nb-list'; notesGroup.append(btnRow, nbList); ctx.nbListEl = nbList; // Quick Jump Floor Navigation (Bottom) const navGroup = document.createElement('div'); navGroup.className = 'anomalous-nb-sidebar-group anomalous-nb-sidebar-nav-group'; const navHeader = document.createElement('div'); navHeader.className = 'anomalous-nb-nav-header'; navHeader.textContent = `🧭 ${t('nbNavHeading') || (window.anomalous_browser_lang === 'zh' ? '快速直达' : 'Quick Jump')}`; const navList = document.createElement('div'); navList.className = 'anomalous-nb-nav-list'; 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' }, ]; navSections.forEach(sec => { const navItem = document.createElement('button'); navItem.type = 'button'; navItem.className = 'anomalous-nb-nav-item'; navItem.dataset.targetId = sec.id; const labelText = t(sec.key) || (window.anomalous_browser_lang === 'zh' ? sec.zh : sec.en); navItem.title = `${sec.icon} ${labelText}`; navItem.setAttribute('aria-label', labelText); navItem.innerHTML = `${labelText}`; navItem.onclick = () => { const target = ctx.nbEditor?.querySelector(sec.id); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }; navList.appendChild(navItem); }); navGroup.append(navHeader, navList); sidebar.append(notesGroup, navGroup); // Setup scrollspy listener setupNotebookScrollSpy(ctx, navList, navSections); return sidebar; } /** * Setup scrollspy on the editor area to highlight current anchor in sidebar. */ function setupNotebookScrollSpy(ctx, navList, navSections) { let isSpying = false; if (!ctx.nbEditor) return; 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 }); } 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 = `
${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}