checkpoint before gfm ast parser design

This commit is contained in:
lix-2026
2026-05-08 00:41:03 +08:00
parent e8ba12e461
commit c620b9e40c
41 changed files with 12270 additions and 263 deletions
@@ -70,10 +70,7 @@ pub fn DocumentPage(
.and_then(Value::as_str)
.unwrap_or("default")
.to_string();
let page_show_heading_numbers = page_options
.get("showHeadingNumbers")
.and_then(Value::as_bool)
.unwrap_or(true);
let page_show_heading_numbers = false;
view! {
<PageLayout current_nav="documents" sidebar_tree_html={sidebar_tree_html.unwrap_or_default()} workspace_name={workspace_name.unwrap_or_default()} workspace_sidebar_html={workspace_sidebar_html.unwrap_or_default()} topbar_title={title.clone()}>
<main
+667 -23
View File
@@ -10,11 +10,18 @@ const SIDEBAR_TREE_JS: &str = r##"
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
var MNOTE_RECENT_LOCAL_ROOTS_KEY = 'mnote.localFolder.recentRoots';
var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
var mnoteNavigationInFlight = '';
var draggingPageNodeId = '';
var activePageDropRow = null;
var draggingFileTreeRowIds = [];
var activeFileTreeDropRow = null;
var sidebarFileTreeSelection = {
selectedRowIds: new Set(),
anchorRowId: null,
focusedRowId: null
};
var projectionRefreshTimer = 0;
var activeTreeContextMenu = null;
var pageUiState = {
@@ -24,7 +31,11 @@ const SIDEBAR_TREE_JS: &str = r##"
pageAiOpen: false,
pageAiBusy: false,
pageAiMessages: [],
pageAiSuggestionIndex: 0
pageAiSuggestionIndex: 0,
pageAiProvider: 'hermes',
pageAiPage: 'chat',
pageAiSessions: [],
pageAiActiveSessionId: ''
};
function closestAction(target, selector) {
@@ -69,7 +80,7 @@ const SIDEBAR_TREE_JS: &str = r##"
wideLayout: false,
smallText: false,
layoutDensity: 'normal',
showHeadingNumbers: true,
showHeadingNumbers: false,
showToc: false,
showStructure: false,
protectEditing: false,
@@ -95,6 +106,28 @@ const SIDEBAR_TREE_JS: &str = r##"
return pageUiState.pageOptions;
}
function readGlobalShowHeadingNumbers() {
try {
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
return raw === 'true' || raw === '1';
} catch (_) {
return false;
}
}
function writeGlobalShowHeadingNumbers(value) {
try {
if (window.localStorage) {
window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
}
} catch (_) {}
document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
}
function effectiveShowHeadingNumbers(options) {
return readGlobalShowHeadingNumbers();
}
function pageOptionIsSupported(name) {
return name === 'wideLayout'
|| name === 'smallText'
@@ -187,6 +220,8 @@ const SIDEBAR_TREE_JS: &str = r##"
function applyPageOptionsToShell() {
var options = currentPageOptions();
var globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
var showHeadingNumbers = effectiveShowHeadingNumbers(options);
var shell = document.querySelector('.document-shell');
var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
@@ -195,26 +230,27 @@ const SIDEBAR_TREE_JS: &str = r##"
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
shell.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
shell.style.width = '100%';
shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
}
if (editorRoot instanceof HTMLElement) {
editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
editorRoot.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
editorRoot.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
}
if (editorSurface instanceof HTMLElement) {
editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
editorSurface.setAttribute('data-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
}
document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
}
function normalizeSidebarTreeMode(value) {
@@ -262,6 +298,280 @@ const SIDEBAR_TREE_JS: &str = r##"
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
}
function copyWorkspaceSourceParams(targetUrl) {
var params = new URLSearchParams(window.location.search);
['sourceKind', 'rootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) targetUrl.searchParams.set(name, value);
});
}
function currentWorkspaceSourcePayload() {
var params = new URLSearchParams(window.location.search);
var payload = {};
['sourceKind', 'rootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) payload[name] = value;
});
return payload;
}
function readRecentLocalRoots() {
try {
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_RECENT_LOCAL_ROOTS_KEY) : '';
var parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter(function(value) {
return typeof value === 'string' && value.trim();
}).slice(0, 10) : [];
} catch (_) {
return [];
}
}
function rememberLocalRoot(rootUri) {
try {
if (!window.localStorage) return;
var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
roots.unshift(rootUri);
window.localStorage.setItem(MNOTE_RECENT_LOCAL_ROOTS_KEY, JSON.stringify(roots.slice(0, 10)));
} catch (_) {}
}
function pathToFileRootUri(value) {
var trimmed = String(value || '').trim();
if (!trimmed) return '';
if (/^file:\/\//i.test(trimmed)) return trimmed;
if (trimmed.charAt(0) !== '/') return '';
return 'file://' + trimmed.split('/').map(function(part, index) {
return index === 0 ? '' : encodeURIComponent(part);
}).join('/');
}
function openLocalFolderRoot(rootUri) {
rememberLocalRoot(rootUri);
var targetUrl = new URL('/', window.location.origin);
targetUrl.searchParams.set('treeView', 'filetree');
targetUrl.searchParams.set('sourceKind', 'local_folder');
targetUrl.searchParams.set('rootUri', rootUri);
window.location.href = targetUrl.toString();
}
function closeLocalFolderDialog() {
var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
}
function readPathFromDirectoryFiles(files) {
var first = files && files.length ? files[0] : null;
if (!first) return '';
var rawPath = typeof first.path === 'string' ? first.path : '';
var relative = typeof first.webkitRelativePath === 'string' ? first.webkitRelativePath : '';
if (rawPath && relative) {
var suffix = relative.split('/').filter(Boolean).join('/');
if (suffix && rawPath.endsWith(suffix)) {
return rawPath.slice(0, rawPath.length - suffix.length).replace(/[\/\\]$/, '');
}
}
if (rawPath) return rawPath;
return '';
}
function requestBrowserFolderChoice(statusNode) {
return new Promise(function(resolve) {
var input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.addEventListener('change', function() {
var selectedPath = readPathFromDirectoryFiles(input.files || []);
if (input.parentElement) input.parentElement.removeChild(input);
if (!selectedPath && statusNode instanceof HTMLElement) {
statusNode.textContent = '当前浏览器没有暴露本机绝对路径,请在下方输入路径。';
}
resolve(selectedPath);
}, { once: true });
document.body.appendChild(input);
input.click();
});
}
async function requestNativeFolderChoice(statusNode) {
var desktopPicker = window.__mnoteDesktop && typeof window.__mnoteDesktop.selectLocalFolder === 'function'
? window.__mnoteDesktop.selectLocalFolder
: null;
if (desktopPicker) {
var selected = await desktopPicker();
return typeof selected === 'string' ? selected : '';
}
if (typeof window.showDirectoryPicker === 'function') {
var handle = await window.showDirectoryPicker({ mode: 'read' });
var handlePath = handle && (handle.path || handle.mnotePath || handle.nativePath);
if (typeof handlePath === 'string' && handlePath.trim()) return handlePath;
if (statusNode instanceof HTMLElement) {
statusNode.textContent = '已选择“' + (handle && handle.name ? handle.name : '文件夹') + '”,但浏览器没有暴露本机绝对路径,请在下方确认路径。';
}
return '';
}
return requestBrowserFolderChoice(statusNode);
}
function openLocalFolderDialog(initialMessage) {
closeLocalFolderDialog();
var recent = readRecentLocalRoots();
var dialog = document.createElement('div');
dialog.className = 'mnote-local-folder-dialog';
dialog.setAttribute('data-testid', 'mnote-local-folder-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.style.position = 'fixed';
dialog.style.inset = '0';
dialog.style.zIndex = '2147483646';
dialog.style.background = 'rgba(15, 23, 42, 0.28)';
dialog.style.display = 'flex';
dialog.style.alignItems = 'center';
dialog.style.justifyContent = 'center';
var card = document.createElement('div');
card.className = 'mnote-local-folder-dialog__card';
card.style.width = 'min(640px, calc(100vw - 32px))';
card.style.background = '#fff';
card.style.border = '1px solid rgba(27, 28, 28, 0.12)';
card.style.borderRadius = '8px';
card.style.padding = '16px';
card.style.boxShadow = '0 18px 48px rgba(15, 23, 42, 0.22)';
card.style.display = 'grid';
card.style.gap = '12px';
var title = document.createElement('h2');
title.textContent = '打开本地文件夹';
title.style.margin = '0';
title.style.fontSize = '18px';
title.style.lineHeight = '1.4';
var status = document.createElement('p');
status.className = 'mnote-local-folder-dialog__status';
status.setAttribute('data-testid', 'mnote-local-folder-status');
status.textContent = initialMessage || '选择一个本机文件夹,或输入绝对路径。';
status.style.margin = '0';
status.style.color = '#4b5563';
status.style.fontSize = '13px';
var input = document.createElement('input');
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
input.setAttribute('data-testid', 'mnote-local-folder-path-input');
input.value = recent.length ? recent[0].replace(/^file:\/\//, '') : '';
input.style.width = '100%';
input.style.boxSizing = 'border-box';
input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
input.style.borderRadius = '6px';
input.style.padding = '10px 12px';
input.style.fontSize = '14px';
var actions = document.createElement('div');
actions.className = 'mnote-local-folder-dialog__actions';
actions.style.display = 'flex';
actions.style.gap = '8px';
actions.style.justifyContent = 'flex-end';
var choose = document.createElement('button');
choose.type = 'button';
choose.textContent = '选择文件夹';
choose.setAttribute('data-testid', 'mnote-local-folder-native-picker');
var cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = '取消';
var confirm = document.createElement('button');
confirm.type = 'button';
confirm.textContent = '打开';
confirm.setAttribute('data-testid', 'mnote-local-folder-open-confirm');
[choose, cancel, confirm].forEach(function(button) {
button.style.border = '1px solid rgba(27, 28, 28, 0.14)';
button.style.borderRadius = '6px';
button.style.padding = '8px 12px';
button.style.background = '#fff';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
});
function submit() {
var rootUri = pathToFileRootUri(input.value);
if (!rootUri) {
status.textContent = '请输入以 / 开头的绝对路径,或 file:// URI。';
input.focus();
return;
}
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
}
choose.addEventListener('click', function(event) {
event.preventDefault();
requestNativeFolderChoice(status).then(function(selectedPath) {
if (selectedPath) {
input.value = selectedPath;
submit();
}
}).catch(function(error) {
status.textContent = error && error.name === 'AbortError'
? '已取消选择。'
: '无法打开系统文件夹选择器,请输入路径。';
});
});
cancel.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
});
confirm.addEventListener('click', function(event) {
event.preventDefault();
submit();
});
input.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
});
actions.appendChild(choose);
actions.appendChild(cancel);
actions.appendChild(confirm);
card.appendChild(title);
card.appendChild(status);
card.appendChild(input);
if (recent.length) {
var recentList = document.createElement('div');
recentList.className = 'mnote-local-folder-dialog__recent';
recent.slice(0, 5).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.textContent = rootUri.replace(/^file:\/\//, '');
button.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
});
recentList.appendChild(button);
});
card.appendChild(recentList);
}
if (recent.length) {
var recentTitle = document.createElement('div');
recentTitle.style.fontSize = '12px';
recentTitle.style.color = '#6b7280';
recentTitle.textContent = '最近使用';
card.insertBefore(recentTitle, actions);
}
card.appendChild(actions);
dialog.appendChild(card);
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closeLocalFolderDialog();
});
document.body.appendChild(dialog);
input.focus();
input.select();
}
function requestOpenLocalFolder() {
openLocalFolderDialog('');
}
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
@@ -273,17 +583,18 @@ const SIDEBAR_TREE_JS: &str = r##"
async function dispatchTreeCommand(trigger, body) {
setCommandPending(trigger, true);
var commandBody = Object.assign({}, currentWorkspaceSourcePayload(), body || {});
try {
var response = await fetch('/api/tree/commands', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
body: JSON.stringify(commandBody)
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || !payload.result) {
throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status);
}
window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: body, result: payload.result } }));
window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: commandBody, result: payload.result } }));
return payload.result;
} catch (error) {
setCommandPending(trigger, false);
@@ -319,6 +630,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree');
copyWorkspaceSourceParams(targetUrl);
var url = targetUrl.pathname + targetUrl.search;
if (mnoteNavigationInFlight === url) return;
mnoteNavigationInFlight = url;
@@ -611,6 +923,7 @@ const SIDEBAR_TREE_JS: &str = r##"
function documentHref(documentId, workspaceId) {
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
copyWorkspaceSourceParams(url);
return url.toString();
}
@@ -827,6 +1140,67 @@ const SIDEBAR_TREE_JS: &str = r##"
}, x, y, trigger || row);
}
function visibleFileTreeRows() {
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
.filter(function(row) { return row instanceof HTMLElement && row.offsetParent !== null; });
}
function syncSidebarFileTreeSelection() {
var rows = visibleFileTreeRows();
rows.forEach(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId))));
row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId));
});
window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', {
detail: {
selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds),
anchorRowId: sidebarFileTreeSelection.anchorRowId,
focusedRowId: sidebarFileTreeSelection.focusedRowId
}
}));
}
function selectSidebarFileTreeRow(row, modifiers) {
if (!(row instanceof HTMLElement)) return [];
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return [];
var rows = visibleFileTreeRows();
var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
var selected = new Set(sidebarFileTreeSelection.selectedRowIds);
var shiftKey = Boolean(modifiers && modifiers.shiftKey);
var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
if (shiftKey && sidebarFileTreeSelection.anchorRowId) {
var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId);
var targetIndex = visibleRowIds.indexOf(rowId);
if (anchorIndex >= 0 && targetIndex >= 0) {
selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
} else if (ctrlKey) {
if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
else selected.add(rowId);
sidebarFileTreeSelection.anchorRowId = rowId;
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
sidebarFileTreeSelection.selectedRowIds = selected;
sidebarFileTreeSelection.focusedRowId = rowId;
syncSidebarFileTreeSelection();
return Array.from(selected);
}
function selectedSidebarFileTreeRowIdsForDrag(row) {
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
return Array.from(sidebarFileTreeSelection.selectedRowIds);
}
return rowId ? [rowId] : [];
}
function ensureSearchModal() {
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
if (existing instanceof HTMLElement) return existing;
@@ -1066,6 +1440,23 @@ const SIDEBAR_TREE_JS: &str = r##"
'</label>';
}
function createGlobalHeadingNumbersRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">标题自动编号</span>' +
'</span>' +
'<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
'</label>';
}
function renderGlobalOptions(popover) {
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
input.checked = globalHeadingNumbers;
});
}
function createPageFontRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
@@ -1199,6 +1590,147 @@ const SIDEBAR_TREE_JS: &str = r##"
];
}
function pageAiStorageKey() {
return 'doc_ai_sessions:' + currentDocumentId();
}
function pageAiNewSession(title) {
var now = Date.now();
return {
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
title: title || '新会话',
createdAt: now,
updatedAt: now,
messages: []
};
}
function pageAiNormalizeSessions(sessions) {
return (Array.isArray(sessions) ? sessions : [])
.slice(0, 20)
.map(function(session) {
return {
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '新会话',
createdAt: Number(session && session.createdAt || Date.now()),
updatedAt: Number(session && session.updatedAt || Date.now()),
messages: Array.isArray(session && session.messages) ? session.messages.slice(-40) : []
};
})
.sort(function(a, b) {
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
});
}
function pageAiLoadSessions() {
try {
var raw = window.localStorage.getItem(pageAiStorageKey());
if (!raw) {
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
pageUiState.pageAiActiveSessionId = fresh.id;
pageUiState.pageAiMessages = [];
return;
}
var parsed = JSON.parse(raw);
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions ? parsed.sessions : []);
if (!sessions.length) {
var fallback = pageAiNewSession();
pageUiState.pageAiSessions = [fallback];
pageUiState.pageAiActiveSessionId = fallback.id;
pageUiState.pageAiMessages = [];
return;
}
pageUiState.pageAiSessions = sessions;
var activeId = String(parsed && parsed.activeSessionId || '').trim();
var active = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = active.id;
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : [];
} catch (_) {
var reset = pageAiNewSession();
pageUiState.pageAiSessions = [reset];
pageUiState.pageAiActiveSessionId = reset.id;
pageUiState.pageAiMessages = [];
}
}
function pageAiPersistSessions() {
try {
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
activeSessionId: pageUiState.pageAiActiveSessionId,
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
}));
} catch (_) {}
}
function pageAiCurrentSession() {
return pageUiState.pageAiSessions.find(function(session) {
return session.id === pageUiState.pageAiActiveSessionId;
}) || null;
}
function pageAiSetActiveSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
pageUiState.pageAiActiveSessionId = session.id;
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
pageUiState.pageAiPage = 'chat';
pageAiPersistSessions();
renderPageAiConversation();
}
function pageAiStartNewSession() {
var session = pageAiNewSession();
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
pageUiState.pageAiActiveSessionId = session.id;
pageUiState.pageAiMessages = [];
pageUiState.pageAiPage = 'chat';
pageAiPersistSessions();
renderPageAiConversation();
}
function pageAiProviderLabel(provider) {
if (provider === 'codex') return 'Codex';
if (provider === 'claudecode') return 'ClaudeCode';
return 'Hermes';
}
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
var provider = button.getAttribute('data-page-ai-provider') || 'hermes';
var active = provider === pageUiState.pageAiProvider;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
var subtitle = drawer.querySelector('.wolai-page-ai-subtitle');
if (subtitle instanceof HTMLElement) {
subtitle.textContent = '当前页面上下文优先 · ' + pageAiProviderLabel(pageUiState.pageAiProvider);
}
}
function humanizePageAiResponse(rawText, promptText) {
var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
var text = String(rawText || '').trim();
if (!text) {
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”,但这次没有返回可读内容。';
}
if (text.startsWith('{')) {
try {
var payload = JSON.parse(text);
var operation = payload && payload.operation ? payload.operation : {};
var normalized = operation && operation.normalized_input ? operation.normalized_input : {};
var args = normalized && normalized.args ? normalized.args : {};
var documentId = searchText(args.documentId || args.pageId || currentDocumentId());
var toolName = searchText(operation.tool_name || normalized.toolName || 'doc_get');
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 mnote-cli 建立页面级工具调用,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
} catch (_) {
return providerLabel + ' 已返回结果,但当前结果是结构化文本,已为你保留原始内容。';
}
}
return text;
}
function ensurePageAiDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (existing instanceof HTMLElement) return existing;
@@ -1212,7 +1744,7 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">智能问答</h2>' +
'<div class="wolai-page-ai-subtitle">当前页面上下文优先 · mnote-cli</div>' +
'<div class="wolai-page-ai-subtitle">当前页面上下文优先 · Hermes</div>' +
'</div>' +
'<button type="button" class="wolai-surface-close" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
@@ -1230,7 +1762,11 @@ const SIDEBAR_TREE_JS: &str = r##"
'<div class="wolai-page-ai-toolbar">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-action="new-session" aria-label="当前已是新会话">新会话</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-action="history">历史会话</button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-action="model">mnote-cli</button>' +
'<div class="wolai-page-ai-provider-group">' +
'<button type="button" class="wolai-page-ai-model-chip is-active" data-page-ai-provider="hermes" aria-pressed="true">Hermes</button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-provider="codex" aria-pressed="false">Codex</button>' +
'<button type="button" class="wolai-page-ai-model-chip" data-page-ai-provider="claudecode" aria-pressed="false">ClaudeCode</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-input-row">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问我你想知道的"></textarea>' +
@@ -1258,6 +1794,24 @@ const SIDEBAR_TREE_JS: &str = r##"
var drawer = ensurePageAiDrawer();
var conversation = drawer.querySelector('[data-page-ai-conversation]');
if (!(conversation instanceof HTMLElement)) return;
if (pageUiState.pageAiPage === 'history') {
if (!pageUiState.pageAiSessions.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
return;
}
conversation.innerHTML = pageUiState.pageAiSessions.map(function(session) {
var preview = Array.isArray(session.messages) && session.messages.length
? session.messages.slice(-1)[0].content
: '暂无消息';
var active = session.id === pageUiState.pageAiActiveSessionId;
return '' +
'<button type="button" class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session="' + escapeHtml(session.id) + '">' +
'<div class="wolai-page-ai-message-role">会话</div>' +
'<div class="wolai-page-ai-message-text"><strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' + escapeHtml(preview) + '</div>' +
'</button>';
}).join('');
return;
}
if (!pageUiState.pageAiMessages.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
return;
@@ -1273,8 +1827,10 @@ const SIDEBAR_TREE_JS: &str = r##"
}
function openPageAiDrawer() {
pageAiLoadSessions();
renderPageAiSuggestions();
renderPageAiConversation();
renderPageAiProviderButtons();
var drawer = ensurePageAiDrawer();
drawer.hidden = false;
pageUiState.pageAiOpen = true;
@@ -1320,12 +1876,22 @@ const SIDEBAR_TREE_JS: &str = r##"
if (pageUiState.pageAiBusy) return;
var prompt = searchText(text);
if (!prompt) return;
pageAiLoadSessions();
var aggregate = currentPageAggregate();
var body = aggregate.body || {};
var subtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
var outline = subtree && subtree.outline ? subtree.outline : null;
pageUiState.pageAiBusy = true;
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
var currentSession = pageAiCurrentSession();
if (currentSession) {
if (currentSession.title === '新会话') {
currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
}
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
try {
var response = await fetch('/api/ai-agent/run', {
@@ -1354,7 +1920,7 @@ const SIDEBAR_TREE_JS: &str = r##"
},
options: {
searxng: true,
ai: { provider: 'hermes' }
ai: { provider: pageUiState.pageAiProvider }
}
})
});
@@ -1372,13 +1938,25 @@ const SIDEBAR_TREE_JS: &str = r##"
});
pageUiState.pageAiMessages.push({
role: 'assistant',
content: assistantText || '当前页面 AI 已接通 `mnote-cli`,但这次没有返回文本结果。'
content: humanizePageAiResponse(assistantText, prompt)
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '当前页面 AI 抽屉已接到 `mnote-cli` 主链,但本次请求失败:' + (error instanceof Error ? error.message : String(error))
content: pageAiProviderLabel(pageUiState.pageAiProvider) + ' 当前请求失败:' + (error instanceof Error ? error.message : String(error))
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} finally {
pageUiState.pageAiBusy = false;
renderPageAiConversation();
@@ -1404,7 +1982,6 @@ const SIDEBAR_TREE_JS: &str = r##"
createPageOptionRow('wideLayout', 'checkbox') +
createPageOptionRow('smallText', 'checkbox') +
createPageOptionRow('showToc', 'checkbox') +
createPageOptionRow('showHeadingNumbers', 'checkbox') +
createPageOptionRow('protectEditing', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
@@ -1415,7 +1992,7 @@ const SIDEBAR_TREE_JS: &str = r##"
createPageOptionRow('showBlockRefCount', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
'<div class="wolai-page-settings-global-note">当前 Rust Web 壳仅保留最小全局设置入口,真正的全局偏好仍待后续统一收口。</div>' +
createGlobalHeadingNumbersRow() +
'</div>' +
'<div class="wolai-page-settings-actions">' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
@@ -1443,6 +2020,7 @@ const SIDEBAR_TREE_JS: &str = r##"
var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
select.value = value;
});
renderGlobalOptions(popover);
var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
if (statsNode instanceof HTMLElement) {
var stats = computeLivePageStats();
@@ -1479,6 +2057,7 @@ const SIDEBAR_TREE_JS: &str = r##"
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
options: nextOptions,
commandName: 'page.layout.updateOptions'
})
@@ -1611,6 +2190,21 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var pageAiProvider = closestAction(e.target, '[data-page-ai-provider]');
if (pageAiProvider) {
e.preventDefault();
pageUiState.pageAiProvider = pageAiProvider.getAttribute('data-page-ai-provider') || 'hermes';
renderPageAiProviderButtons();
return;
}
var pageAiSession = closestAction(e.target, '[data-page-ai-session]');
if (pageAiSession) {
e.preventDefault();
pageAiSetActiveSession(pageAiSession.getAttribute('data-page-ai-session') || '');
return;
}
var pageAiSuggestion = closestAction(e.target, '[data-page-ai-suggestion]');
if (pageAiSuggestion) {
e.preventDefault();
@@ -1626,7 +2220,16 @@ const SIDEBAR_TREE_JS: &str = r##"
var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
if (pageAiNewSession) {
e.preventDefault();
pageUiState.pageAiMessages = [];
pageUiState.pageAiPage = 'chat';
pageAiStartNewSession();
return;
}
var pageAiHistory = closestAction(e.target, '[data-page-ai-action="history"]');
if (pageAiHistory) {
e.preventDefault();
pageAiLoadSessions();
pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history';
renderPageAiConversation();
return;
}
@@ -1662,6 +2265,13 @@ const SIDEBAR_TREE_JS: &str = r##"
return;
}
var localFolderTrigger = closestAction(e.target, '[data-mnote-action="open-local-folder"]');
if (localFolderTrigger) {
e.preventDefault();
requestOpenLocalFolder();
return;
}
var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
if (createTrigger) {
e.preventDefault();
@@ -1691,18 +2301,19 @@ const SIDEBAR_TREE_JS: &str = r##"
}
if (fileAction === 'menu') {
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
var point = rowCenter(fileBtn || fileRow);
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
return;
}
e.preventDefault();
fileTree.querySelectorAll('.tree-row[data-selected="true"]').forEach(function(row) {
if (row instanceof HTMLElement) row.setAttribute('data-selected', 'false');
});
fileRow.setAttribute('data-selected', 'true');
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
if ((rowKind === 'document' || rowKind === 'index') && documentId) {
if (e.shiftKey || e.ctrlKey || e.metaKey) {
return;
}
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && documentId) {
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
} else if (assetId) {
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId });
@@ -1755,6 +2366,10 @@ const SIDEBAR_TREE_JS: &str = r##"
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
if (fileRow) {
event.preventDefault();
var contextRowId = fileRow.getAttribute('data-row-id') || '';
if (contextRowId && !sidebarFileTreeSelection.selectedRowIds.has(contextRowId)) {
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
}
openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow);
return;
}
@@ -1792,6 +2407,13 @@ const SIDEBAR_TREE_JS: &str = r##"
});
document.addEventListener('change', function(event) {
var globalCheckbox = closestAction(event.target, '[data-global-option-checkbox="showHeadingNumbers"]');
if (globalCheckbox instanceof HTMLInputElement) {
writeGlobalShowHeadingNumbers(globalCheckbox.checked);
applyPageOptionsToShell();
renderPageSettingsPopover();
return;
}
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
if (checkbox instanceof HTMLInputElement) {
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
@@ -1880,8 +2502,7 @@ const SIDEBAR_TREE_JS: &str = r##"
}
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
if (fileRow) {
var rowId = fileRow.getAttribute('data-row-id') || '';
draggingFileTreeRowIds = rowId ? [rowId] : [];
draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
if (event.dataTransfer) {
var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
event.dataTransfer.effectAllowed = 'copyMove';
@@ -2014,6 +2635,16 @@ const SIDEBAR_TREE_JS: &str = r##"
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
}
var initiallySelectedFileRows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-selected="true"]');
initiallySelectedFileRows.forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return;
sidebarFileTreeSelection.selectedRowIds.add(rowId);
if (!sidebarFileTreeSelection.anchorRowId) sidebarFileTreeSelection.anchorRowId = rowId;
sidebarFileTreeSelection.focusedRowId = rowId;
});
syncSidebarFileTreeSelection();
restoreSidebarTreeTab();
})();
"##;
@@ -2078,6 +2709,13 @@ const TREE_LIVE_CONTROLLER_JS: &str = r##"
return;
}
var bootstrap = readBootstrap();
var params = new URLSearchParams(window.location.search);
var sourceKind = (params.get('sourceKind') || '').trim();
if (sourceKind === 'local_folder') {
applyTransport('local-folder-static');
applyStatus('static');
return;
}
applyTransport(bootstrap.transport || 'convex-command-log-sse');
var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
@@ -2220,6 +2858,7 @@ pub fn PageLayout(
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
</nav>
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
@@ -2269,6 +2908,8 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_JS.contains("treeView"));
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
@@ -2281,6 +2922,9 @@ mod tests {
assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("requestOpenLocalFolder"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind', 'local_folder"));
}
#[test]