Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_notebooks.js
T
DemonGatanjieuandClaude Opus 5.5 054cb91b9b style: one palette for every page, from the theme tokens
Pages made at different times used their own greys (slate here, neutral
there) and accents (sky, violet, indigo), and most ignored the Abyssal
Scarlet theme. About 1,850 literal colours in web/styles and in CSS and
inline styles written from JS now use the --amb-* tokens by what they are
for: surfaces, text (new --amb-text-soft and --amb-text-dim), borders (new
--amb-border-strong), the primary button, and one accent (new --amb-link,
tinted with color-mix). Both themes define the new tokens.

Literal colours stay where the colour is the meaning: status, prompt
roles, voice emotions, canvas node colours, shadows and overlays; theme
override rules are untouched. The share-code dialogs move from inline
styles to .anomalous-share-* classes. Stylesheet versions are bumped.

Checked: text contrast on every rail page, Prompt Studio, Prompt Notes,
Model Sources, settings, model detail and editor, share codes, in both
themes; structure check and JS tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:19:55 +08:00

311 lines
14 KiB
JavaScript

/**
* 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 = `<svg style="width:13px;height:13px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span class="anomalous-nb-create-text">${t('createNotebook') || (window.anomalous_browser_lang === 'zh' ? '新建笔记' : 'New Note')}</span>`;
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 = 'var(--amb-bg-card)';
createInput.style.color = '#fff';
createInput.style.border = '1px solid var(--amb-border-strong)';
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 = `<span class="anomalous-nb-nav-icon">${sec.icon}</span><span class="anomalous-nb-nav-text">${labelText}</span>`;
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], ['home', this.homePanel], ['activity', this.activityPanel],
['scan', this.scanPanel],
].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.homePanel, this.activityPanel, this.scanPanel, 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 = '&times;';
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 = `<span class="anomalous-nb-item-icon"><svg style="width:13px;height:13px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></span><span class="anomalous-nb-item-text">${escapeHtml(nb.name)}</span>`;
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 = `
<div style="display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:#94a3b8; text-align:center; gap:14px; padding:40px;">
<span style="font-size:3.2rem;">📝</span>
<h3 style="margin:0; color:#f1f5f9; font-size:1.1rem;">${t('promptNotes') || (window.anomalous_browser_lang === 'zh' ? '提示词笔记' : 'Prompt Notes')}</h3>
<p style="margin:0; font-size:0.88rem; max-width:320px; line-height:1.5;">${window.anomalous_browser_lang === 'zh' ? '暂无笔记。点击左侧「+」按钮即可创建新的提示词笔记,支持模型绑定与提示词整理。' : 'No notebooks found. Click "+" on the left sidebar to create your first prompt note.'}</p>
</div>
`;
}
} catch (e) {
console.error('[AMB] Error refreshing notebooks:', e);
}
}
export async function saveCurrentNotebook() {
if (!this.currentNotebook) return false;
const body = JSON.stringify(this.currentNotebook);
this.notebookSaveQueue = (this.notebookSaveQueue || Promise.resolve()).then(async () => {
try {
const response = await fetch('/anomalous/save_notebook', {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body,
});
const result = await response.json();
if (!response.ok || result.status !== 'success') throw new Error('notebook save failed');
this.nbSaveStatus?.remove();
this.nbSaveStatus = null;
await this.refreshNotebooks();
return true;
} catch (error) {
if (this.nbEditor && !this.nbSaveStatus?.isConnected) {
this.nbSaveStatus = document.createElement('p');
this.nbSaveStatus.setAttribute('role', 'alert');
this.nbEditor.prepend(this.nbSaveStatus);
}
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) {
console.error('[AMB] Error deleting notebook:', e);
}
}