refactor(notebooks): optimize prompt notes UX with flat layout, sticky toolbar, quick jump navigation, and complete localization
This commit is contained in:
+7
-6
@@ -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,
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
+624
-617
File diff suppressed because it is too large
Load Diff
+246
-165
@@ -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 = '×';
|
||||
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 = '×';
|
||||
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
@@ -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 双栏排版) */
|
||||
|
||||
Reference in New Issue
Block a user