refactor(notebooks): optimize prompt notes UX with flat layout, sticky toolbar, quick jump navigation, and complete localization

This commit is contained in:
DemonGatanjieu
2026-09-18 09:58:50 +08:00
parent 4463f2406b
commit 44d2c824bc
6 changed files with 1066 additions and 799 deletions
+7 -6
View File
@@ -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,
+1 -1
View File
@@ -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.');
+44 -2
View File
@@ -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",
File diff suppressed because it is too large Load Diff
+246 -165
View File
@@ -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 = `<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 = '#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 = '&times;';
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 = `<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')}</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');
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 = `<span class="anomalous-nb-nav-icon">${sec.icon}</span><span class="anomalous-nb-nav-text">${t(sec.key) || (window.anomalous_browser_lang === 'zh' ? sec.zh : sec.en)}</span>`;
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 = `<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') || '提示词笔记'}</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 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 = '&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;
@@ -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);
}
}
+144 -8
View File
@@ -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 双栏排版) */