Files
mnote/rust/crates/mnote-web/browser/sidebar-tree-runtime.js
T

6269 lines
294 KiB
JavaScript
Raw Normal View History

2026-05-26 01:31:28 +08:00
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js';
import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtime.js';
import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-runtime.js';
import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js';
import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-runtime.js';
2026-05-26 01:31:28 +08:00
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
window.__mnoteSidebarTreeRuntimeStarted = true;
var PAGE_AI_SESSION_STORAGE_VERSION = 3;
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_PREFIX = 'mnote.localFolder.recentRoots:';
var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
var mnoteNavigationInFlight = '';
var draggingPageNodeId = '';
var activePageDropRow = null;
var draggingFileTreeRowIds = [];
var activeFileTreeDropRow = null;
var sidebarFileTreeClipboard = null;
var sidebarFileTreeSelection = {
selectedRowIds: new Set(),
anchorRowId: null,
focusedRowId: null
};
var activeTreeContextMenu = null;
var pageUiState = {
pageOptions: null,
historySnapshots: [],
pageSettingsOpen: false,
pageAiOpen: false,
pageAiBusy: false,
pageAiMessages: [],
pageAiSuggestionIndex: 0,
pageAiProvider: 'hermes',
pageAiPage: 'chat',
pageAiRunStatus: 'idle',
pageAiCurrentRunId: '',
pageAiAcpRuntime: 'reasonix',
pageAiAcpRuntimes: [],
pageAiQueueLength: 0,
pageAiQueuedItems: [],
pageAiStoppedRunIds: {},
pageAiAbortController: null,
pageAiContextScope: 'page',
pageAiTools: [],
pageAiToolsError: '',
pageAiGatewayHealth: null,
pageAiGatewayHealthError: '',
pageAiLastToolCall: null,
pageAiProfiles: [],
pageAiActiveProfileName: 'mnoteai',
pageAiProfileError: '',
pageAiProfileMemory: { memory: '', user: '', soul: '' },
pageAiProfileMemoryDrafts: { memory: '', user: '', soul: '' },
pageAiProfileMemoryError: '',
pageAiSkills: { categories: [], archived: [] },
pageAiSkillQuery: '',
pageAiSkillError: '',
pageAiSessions: [],
pageAiActiveSessionId: '',
pageAiSessionSearchQuery: '',
pageAiSessionSearchResults: [],
pageAiSessionSearchTimer: 0,
pageAiSessionError: '',
pageAiPermissionRequests: [],
localIndexSummary: {
scopeKey: '',
loading: false,
error: '',
backlinks: null,
tags: null
}
};
function closestAction(target, selector) {
var node = target && target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
return node && typeof node.closest === 'function' ? node.closest(selector) : null;
}
2026-05-25 22:53:15 +08:00
function sidebarShellRuntimeFunction(name) {
var runtime = window.__mnoteSidebarShellRuntime;
return runtime && typeof runtime[name] === 'function' ? runtime[name] : null;
}
function toggleWorkspaceSidebar(trigger) {
2026-05-25 22:53:15 +08:00
var runtimeFn = sidebarShellRuntimeFunction('toggleWorkspaceSidebar');
if (runtimeFn) return runtimeFn(trigger);
}
function installWorkspaceSidebarResizer() {
2026-05-25 22:53:15 +08:00
var runtimeFn = sidebarShellRuntimeFunction('installWorkspaceSidebarResizer');
if (runtimeFn) return runtimeFn();
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
return String(value).replace(/["\\]/g, '\\$&');
}
function parseJsonScript(id) {
var node = document.getElementById(id);
if (!node) return null;
try {
return JSON.parse(node.textContent || 'null');
} catch (_) {
return null;
}
}
function currentDocumentId() {
var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
if (!match) match = window.location.pathname.match(/^\/mindmap\/([^\/]+)\/([^\/]+)/);
if (match) return decodeURIComponent(match[1]);
var params = new URLSearchParams(window.location.search);
var fromQuery = (params.get('documentId') || params.get('pageId') || '').trim();
if (fromQuery) return fromQuery;
var activePane = document.querySelector('.document-pane[data-pane-role="primary"][data-pane-document-id], .document-pane[data-pane-visible="true"][data-pane-document-id]');
if (activePane instanceof HTMLElement) {
var paneDocumentId = (activePane.getAttribute('data-pane-document-id') || '').trim();
if (paneDocumentId) return paneDocumentId;
}
var shell = document.querySelector('.document-shell[data-document-id]');
if (shell instanceof HTMLElement) return (shell.getAttribute('data-document-id') || '').trim();
return '';
}
function localFolderSelfChangeSuppressions() {
window.__mnoteLocalFolderSelfChangeSuppressions = window.__mnoteLocalFolderSelfChangeSuppressions || new Map();
return window.__mnoteLocalFolderSelfChangeSuppressions;
}
function persistLocalFolderSelfChangeSuppression(documentId, expiresAt) {
var doc = String(documentId || '').trim();
if (!doc) return;
localFolderSelfChangeSuppressions().set(doc, expiresAt);
try {
var key = 'mnote.localFolder.selfChangeSuppressions.v1';
var existing = JSON.parse(window.sessionStorage.getItem(key) || '{}');
existing[doc] = expiresAt;
window.sessionStorage.setItem(key, JSON.stringify(existing));
} catch (_) {}
}
var EDITOR_UPLOAD_ROOT_SELECTOR = '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]';
function editorUploadRootFromElement(target) {
if (!(target instanceof Element)) return null;
var root = target.closest(EDITOR_UPLOAD_ROOT_SELECTOR);
return root instanceof HTMLElement ? root : null;
}
function rememberEditorUploadRootFromTarget(target) {
var root = editorUploadRootFromElement(target);
if (root instanceof HTMLElement) {
window.__mnoteLastEditorUploadRoot = root;
return root;
}
return null;
}
document.addEventListener('pointerdown', function(event) {
rememberEditorUploadRootFromTarget(event.target);
}, true);
document.addEventListener('focusin', function(event) {
rememberEditorUploadRootFromTarget(event.target);
}, true);
function currentFileTreeActiveRowId() {
var explicitRowId = new URL(window.location.href).searchParams.get('restoreFocusRowId') || '';
if (explicitRowId) return explicitRowId;
var mindmapMatch = window.location.pathname.match(/^\/mindmap\/([^\/]+)\/([^\/]+)/);
if (mindmapMatch) return 'asset:' + decodeURIComponent(mindmapMatch[2]);
var documentId = currentDocumentId();
return documentId ? 'doc:' + documentId : '';
}
function currentPageAggregate() {
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
}
function textFromUnknown(value) {
if (value == null) return '';
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
if (typeof value !== 'object') return '';
var parts = [];
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
var text = textFromUnknown(value[key]);
if (text) parts.push(text);
}
});
return parts.join(' ');
}
function readLocalEditorBlocks() {
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return [];
return Array.from(editor.children).filter(function(node) {
return node instanceof HTMLElement;
}).map(function(node, index) {
var tag = String(node.tagName || '').toUpperCase();
var headingMatch = tag.match(/^H([1-6])$/);
var type = headingMatch ? 'heading' : 'paragraph';
var text = searchText(node.textContent || '');
var id = searchText(node.getAttribute('data-id') || node.id || 'local-block-' + index);
var props = headingMatch ? { level: Number(headingMatch[1]) } : {};
return { id: id, type: type, props: props, content: text };
}).filter(function(block) {
return block.content || block.type === 'heading';
});
}
function buildPageAiLocalSubtree(blocks, title) {
var documentId = currentDocumentId() || 'current-page';
var rootNodeId = 'page:' + documentId;
var headingCounters = [0, 0, 0, 0, 0, 0];
var headingStack = [];
var nodes = [{
id: rootNodeId,
nodeId: rootNodeId,
nodeType: 'page',
blockId: null,
blockType: 'page',
title: title || '',
parentNodeId: null,
headingLevel: null
}];
var outline = [];
var evidence = [];
blocks.forEach(function(block, index) {
var level = block.type === 'heading' ? Math.max(1, Math.min(6, Number(block.props && block.props.level || 1))) : null;
if (level != null) {
while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
}
var parentNodeId = headingStack.length ? headingStack[headingStack.length - 1].nodeId : rootNodeId;
var nodeId = 'local-node:' + String(block.id || index);
var titleText = block.content || (block.type === 'heading' ? '未命名标题' : '');
nodes.push({
id: nodeId,
nodeId: nodeId,
nodeType: 'block',
blockId: block.id,
blockType: block.type,
title: titleText,
parentNodeId: parentNodeId,
headingLevel: level
});
if (level != null) {
headingCounters[level - 1] += 1;
for (var i = level; i < headingCounters.length; i += 1) headingCounters[i] = 0;
var numbering = headingCounters.slice(0, level).filter(function(count) { return count > 0; }).join('.');
outline.push({
id: 'local-outline:' + String(block.id || index),
nodeId: nodeId,
anchorBlockId: block.id,
level: level,
title: titleText,
numbering: numbering
});
headingStack.push({ level: level, nodeId: nodeId });
}
if (titleText) {
evidence.push({
id: 'local-evidence:' + String(block.id || index),
nodeId: nodeId,
anchorBlockId: block.id,
text: titleText,
kind: block.type
});
}
});
return {
projectionId: 'local-editor-dom:' + documentId,
rootNode: {
id: rootNodeId,
documentId: documentId,
title: title || '',
nodeType: 'page'
},
subtree: {
rootNodeId: rootNodeId,
nodes: nodes
},
outline: outline,
evidence: evidence,
stats: {
nodeCount: nodes.length,
headingCount: outline.length,
evidenceCount: evidence.length
},
source: 'local'
};
}
function currentPageAiContextSnapshot() {
var aggregate = currentPageAggregate();
var body = aggregate.body || {};
var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
return {
aggregate: aggregate,
body: body,
subtree: serverSubtree,
pageSubtreeSource: serverSubtree ? 'server' : 'none'
};
}
function currentPageAiSelectedText() {
try {
var selection = window.getSelection ? window.getSelection() : null;
return selection ? searchText(selection.toString() || '') : '';
} catch (_) {
return '';
}
}
function pageAiProjectionBlocks(aggregate) {
var blocks = aggregate && aggregate.body && aggregate.body.blockDocument && aggregate.body.blockDocument.blocks;
return Array.isArray(blocks) ? blocks : [];
}
function pageAiBlockText(block) {
return searchText(block && (block.text || block.title || block.content) || '');
}
function pageAiSelectedBlockIdsFromSelection() {
try {
var selection = window.getSelection ? window.getSelection() : null;
if (!selection || selection.rangeCount === 0 || searchText(selection.toString() || '') === '') return [];
var range = selection.getRangeAt(0);
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return [];
return Array.from(editor.children).filter(function(node) {
if (!(node instanceof HTMLElement)) return false;
try {
return range.intersectsNode(node);
} catch (_) {
return false;
}
}).map(function(node) {
return searchText(node.getAttribute('data-id') || node.id || '');
}).filter(Boolean);
} catch (_) {
return [];
}
}
function pageAiBlocksToPageXml(blocks, aggregate) {
var revision = aggregate && aggregate.body ? String(aggregate.body.revision || '') : '';
var pageId = currentDocumentId() || 'current-page';
var lines = ['<page id="' + escapeHtml(pageId) + '" revision="' + escapeHtml(revision) + '">'];
blocks.forEach(function(block) {
var blockId = String(block && (block.blockId || block.id) || '');
var type = String(block && block.type || 'paragraph');
var revisionRef = String(block && block.revisionRef || '');
var level = block && block.attrs && block.attrs.level ? ' level="' + escapeHtml(block.attrs.level) + '"' : '';
lines.push(' <block id="' + escapeHtml(blockId) + '" type="' + escapeHtml(type) + '" revisionRef="' + escapeHtml(revisionRef) + '"' + level + '>' + escapeHtml(pageAiBlockText(block)) + '</block>');
});
lines.push('</page>');
return lines.join('\n');
}
function buildPageAiContext(contextSnapshot, scope, selectedText) {
var aggregate = contextSnapshot.aggregate || {};
var body = aggregate.body || {};
var allBlocks = pageAiProjectionBlocks(aggregate);
var selectedBlockIds = scope === 'selection' ? pageAiSelectedBlockIdsFromSelection() : [];
var selectedSet = {};
selectedBlockIds.forEach(function(id) { selectedSet[id] = true; });
var selectedBlocks = selectedBlockIds.length
? allBlocks.filter(function(block) { return selectedSet[String(block.blockId || block.id || '')]; })
: [];
var contextBlocks = selectedBlocks.length ? selectedBlocks : allBlocks.slice(0, 120);
var truncated = !selectedBlocks.length && allBlocks.length > contextBlocks.length;
return {
schema: 'mnote.page_ai_context.v1',
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
scope: scope,
revision: body.revision || null,
conflictDetectionKey: body.conflictDetectionKey || null,
selectedText: selectedText || '',
selectedBlockIds: selectedBlockIds,
allowedTargetBlockIds: selectedBlockIds,
selectedBlocks: selectedBlocks,
contextBlocks: contextBlocks,
pageText: contextBlocks.map(pageAiBlockText).filter(Boolean).join('\n'),
pageXml: pageAiBlocksToPageXml(contextBlocks, aggregate),
truncated: truncated,
warnings: truncated ? [{ code: 'page_ai_context_truncated', message: '页面 AI context 已按前 120 个块裁剪' }] : []
};
}
function pageAiScopedPageContext(contextSnapshot) {
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
var scope = pageUiState.pageAiContextScope || 'page';
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
var aiContext = buildPageAiContext(contextSnapshot, scope, selectedText);
return {
pageContext: {
contextScope: scope,
documentBlocks: null,
node: {
documentId: currentDocumentId(),
title: title
},
subtree: null,
outline: null,
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
contentAccess: 'mnote.doc.fetch',
aiContext: aiContext
},
selectedText: selectedText,
selectedBlockId: aiContext.selectedBlockIds[0] || null
};
}
function defaultPageOptions() {
return {
wideLayout: false,
smallText: false,
layoutDensity: 'normal',
showHeadingNumbers: false,
showToc: false,
showStructure: false,
protectEditing: false,
showWordCount: true,
collapseBacklinks: false,
pageFont: 'default',
hideChildPages: false,
showBlockRefCount: false,
hideTitleHeader: false,
embedDefaultBlockId: null
};
}
function currentPageOptions() {
if (pageUiState.pageOptions) return pageUiState.pageOptions;
var aggregate = currentPageAggregate();
var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
? aggregate.layout.pageOptions
: null;
if (!current) {
return defaultPageOptions();
}
pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
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'
|| name === 'layoutDensity'
|| name === 'pageFont'
|| name === 'showHeadingNumbers'
|| (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
}
function pageOptionDescription(name) {
if (name === 'wideLayout') return '自适应宽度';
if (name === 'smallText') return '小字体';
if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
if (name === 'showToc') return '标题目录';
if (name === 'showHeadingNumbers') return '标题自动编号';
if (name === 'protectEditing') return '编辑保护';
if (name === 'collapseBacklinks') return '折叠反向引用';
if (name === 'hideChildPages') return '隐藏子页面';
if (name === 'showBlockRefCount') return '显示块引用数字';
return name;
}
function pageOptionHint(name) {
if (name === 'wideLayout') return '已接通:主内容列宽度立即变化';
if (name === 'smallText') return '已接通:正文排版会更紧凑';
if (name === 'showHeadingNumbers') return '已接通:标题前显示顺序编号';
if (name === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
if (name === 'layoutDensity') return '已接通:段落与列表间距会变化';
if (name === 'pageFont') return '已接通:当前页面字体会切换';
if (name === 'showToc') return '待接线:当前 Rust 壳还没有正式目录面板';
if (name === 'protectEditing') return '待接线:当前主编辑器只显示降级说明';
if (name === 'collapseBacklinks') return '待接线:当前 Rust 壳未挂回链面板';
if (name === 'hideChildPages') return '待接线:当前页面壳还没有子页面块显隐';
if (name === 'showBlockRefCount') return '待接线:当前页面壳未显示块引用计数';
return '待接线';
}
function ensureHistorySnapshotsSeeded() {
if (pageUiState.historySnapshots.length > 0) return;
var aggregate = currentPageAggregate();
var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
pageUiState.historySnapshots = [{
id: 'snapshot-initial',
timestamp: Date.now(),
stats: {
wordCount: Number(stats.wordCount || 0),
characterCount: Number(stats.characterCount || 0),
blockCount: Number(stats.blockCount || 0),
todoTotal: Number(stats.todoTotal || 0),
todoDone: Number(stats.todoDone || 0)
}
}];
}
function computeLivePageStats() {
var aggregate = currentPageAggregate();
var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) {
return {
wordCount: Number(fallbackStats.wordCount || 0),
characterCount: Number(fallbackStats.characterCount || 0),
blockCount: Number(fallbackStats.blockCount || 0),
todoTotal: Number(fallbackStats.todoTotal || 0),
todoDone: Number(fallbackStats.todoDone || 0)
};
}
var text = (editor.textContent || '').trim();
var compact = text.replace(/\s+/g, ' ').trim();
var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
var characterCount = text.replace(/\s/g, '').length;
var blockCount = editor.querySelectorAll(':scope > *').length;
var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
return {
wordCount: wordCount || Number(fallbackStats.wordCount || 0),
characterCount: characterCount || Number(fallbackStats.characterCount || 0),
blockCount: blockCount || Number(fallbackStats.blockCount || 0),
todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
};
}
function recordPageHistorySnapshot(reason, stats) {
ensureHistorySnapshotsSeeded();
pageUiState.historySnapshots = [{
id: 'snapshot-' + Date.now(),
timestamp: Date.now(),
reason: reason || 'save',
stats: stats || computeLivePageStats()
}].concat(pageUiState.historySnapshots).slice(0, 15);
renderPageHistoryDrawer();
}
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');
if (shell instanceof HTMLElement) {
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
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(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(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(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-global-show-heading-numbers', String(globalShowHeadingNumbers));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
if (titleHeader instanceof HTMLElement) {
var hidden = Boolean(options.hideTitleHeader);
titleHeader.hidden = hidden;
titleHeader.setAttribute('data-page-title-hidden', String(hidden));
}
}
2026-05-26 01:31:28 +08:00
const sidebarWorkspace = createSidebarWorkspaceRuntime({
cssEscape,
currentDocumentId,
escapeHtml,
parseJsonScript,
setCommandPending: (...args) => setCommandPending(...args),
sidebarShellRuntimeFunction,
});
const normalizeSidebarTreeMode = (...args) => sidebarWorkspace.normalizeSidebarTreeMode(...args);
const readStoredSidebarTreeMode = (...args) => sidebarWorkspace.readStoredSidebarTreeMode(...args);
const persistSidebarTreeMode = (...args) => sidebarWorkspace.persistSidebarTreeMode(...args);
const activeSidebarTreeMode = (...args) => sidebarWorkspace.activeSidebarTreeMode(...args);
const resolveWorkspaceId = (...args) => sidebarWorkspace.resolveWorkspaceId(...args);
const currentWorkspaceId = (...args) => sidebarWorkspace.currentWorkspaceId(...args);
const currentSourceKind = (...args) => sidebarWorkspace.currentSourceKind(...args);
const currentRootUri = (...args) => sidebarWorkspace.currentRootUri(...args);
const copyWorkspaceSourceParams = (...args) => sidebarWorkspace.copyWorkspaceSourceParams(...args);
const currentWorkspaceSourcePayload = (...args) => sidebarWorkspace.currentWorkspaceSourcePayload(...args);
const openTrashModal = (...args) => sidebarWorkspace.openTrashModal(...args);
const closeTrashModal = (...args) => sidebarWorkspace.closeTrashModal(...args);
const closeLocalFolderDialog = (...args) => sidebarWorkspace.closeLocalFolderDialog(...args);
const requestOpenLocalFolder = (...args) => sidebarWorkspace.requestOpenLocalFolder(...args);
const createDefaultLocalWorkspace = (...args) => sidebarWorkspace.createDefaultLocalWorkspace(...args);
const closeWorkspaceSourceMenu = (...args) => sidebarWorkspace.closeWorkspaceSourceMenu(...args);
const openWorkspaceSourceMenu = (...args) => sidebarWorkspace.openWorkspaceSourceMenu(...args);
const closeAccountMenu = (...args) => sidebarWorkspace.closeAccountMenu(...args);
const openAccountMenu = (...args) => sidebarWorkspace.openAccountMenu(...args);
sidebarWorkspace.autoOpenRecentLocalRootOnHome();
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
if ('disabled' in trigger) {
if (pending) trigger.setAttribute('disabled', 'disabled');
else trigger.removeAttribute('disabled');
}
}
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(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: commandBody, result: payload.result } }));
setCommandPending(trigger, false);
return payload.result;
} catch (error) {
setCommandPending(trigger, false);
if (trigger instanceof HTMLElement) {
trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
}
throw error;
}
}
function navigateToDocument(nodeId, workspaceId, options) {
if (!nodeId) return;
var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
persistSidebarTreeMode(treeView);
if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) {
if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === 'function') {
window.__mnoteDocumentPaneRuntime.activatePageTab();
}
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
if (row instanceof HTMLElement) {
row.setAttribute('data-active', 'false');
row.setAttribute('data-selected', 'false');
}
});
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
if (row instanceof HTMLElement) {
if (row.getAttribute('data-shell-mode') === 'filetree') {
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'doc:' + nodeId));
} else {
row.setAttribute('data-active', 'true');
}
}
});
selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true });
return;
}
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;
if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') {
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
documentId: nodeId,
workspaceId: workspaceId || '',
sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
rootUri: targetUrl.searchParams.get('rootUri') || '',
url: targetUrl,
}).then(function(){
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
document.documentElement.removeAttribute('data-mnote-navigation-pending');
selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true });
}).catch(function(error){
console.warn('mnote pane 内导航失败,将回退整页导航', error);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
});
return;
}
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
}
async function createPage(trigger, parentId) {
var workspaceId = resolveWorkspaceId(trigger);
var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
if (!workspaceId) return;
var result = await dispatchTreeCommand(trigger, {
action: 'create',
workspaceId: workspaceId,
parentId: effectiveParentId || null,
title: '新页面'
});
var nextWorkspaceId = result.workspaceId || workspaceId;
var nextDocumentId = commandDocumentId(result, '');
if (nextDocumentId) {
persistLocalFolderSelfChangeSuppression(nextDocumentId, Date.now() + 5000);
}
if (nextDocumentId && currentSourceKind() === 'local_folder') {
await refreshLocalFolderSidebarSnapshot();
selectSidebarFileTreeDocument(nextDocumentId, { scrollIntoView: true });
document.documentElement.setAttribute('data-mnote-create-page-selected-document-id', nextDocumentId);
}
navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
}
async function createFileTreeFolder(trigger, parentId) {
if (currentSourceKind() !== 'local_folder') return false;
var workspaceId = resolveWorkspaceId(trigger || document.body);
var effectiveParentId = String(parentId || '').trim();
var title = window.prompt('新建文件夹', '新建文件夹');
if (!title || !title.trim()) return false;
var result = await dispatchTreeCommand(trigger || document.body, {
action: 'create_folder',
workspaceId: workspaceId,
parentId: effectiveParentId || null,
title: title.trim()
});
document.documentElement.setAttribute('data-mnote-filetree-folder-created', 'true');
document.documentElement.setAttribute('data-mnote-filetree-folder-created-id', commandDocumentId(result, result.id || ''));
void refreshLocalFolderSidebarSnapshot();
return true;
}
function applySidebarTreeTab(mode, shell) {
2026-05-25 22:53:15 +08:00
var runtimeFn = sidebarShellRuntimeFunction('applySidebarTreeTab');
if (runtimeFn) return runtimeFn(mode, shell);
}
function switchSidebarTreeTab(trigger) {
2026-05-25 22:53:15 +08:00
var runtimeFn = sidebarShellRuntimeFunction('switchSidebarTreeTab');
if (runtimeFn) return runtimeFn(trigger);
}
function restoreSidebarTreeTab() {
2026-05-25 22:53:15 +08:00
var runtimeFn = sidebarShellRuntimeFunction('restoreSidebarTreeTab');
if (runtimeFn) return runtimeFn();
}
const sidebarTreeLiveApply = createSidebarTreeLiveApplyRuntime({
activeSidebarTreeMode,
cssEscape,
currentDocumentId: (...args) => currentDocumentId(...args),
currentFileTreeActiveRowId: (...args) => currentFileTreeActiveRowId(...args),
currentRootUri,
currentSourceKind,
currentWorkspaceId,
escapeHtml,
fileTreeRuntimeFunction: (...args) => fileTreeRuntimeFunction(...args),
localFilePathFromAssetId: (...args) => localFilePathFromAssetId(...args),
navigateToDocument,
refreshEditorLocalAttachmentExistence: (...args) => refreshEditorLocalAttachmentExistence(...args),
restoreSidebarTreeTab,
rowTitle: (...args) => rowTitle(...args),
schedulePendingLocalFolderRestoreFocus: (...args) => schedulePendingLocalFolderRestoreFocus(...args),
shortMindmapFileName: (...args) => shortMindmapFileName(...args),
syncSidebarFileTreeSelection: (...args) => syncSidebarFileTreeSelection(...args),
});
const updateTitleEverywhere = (...args) => sidebarTreeLiveApply.updateTitleEverywhere(...args);
const fileTreePageTitle = (...args) => sidebarTreeLiveApply.fileTreePageTitle(...args);
const isFileTreePageRow = (...args) => sidebarTreeLiveApply.isFileTreePageRow(...args);
const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args);
const validateFileTreeRename = (...args) => sidebarTreeLiveApply.validateFileTreeRename(...args);
const removeDocumentRowForMode = (...args) => sidebarTreeLiveApply.removeDocumentRowForMode(...args);
const commandDocumentId = (...args) => sidebarTreeLiveApply.commandDocumentId(...args);
const applyCreatedDocumentLocally = (...args) => sidebarTreeLiveApply.applyCreatedDocumentLocally(...args);
const localCommandNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.localCommandNeedsProjectionRefresh(...args);
const applyMoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyMoveDocumentDelta(...args);
const applyRemoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyRemoveDocumentDelta(...args);
const setTreeLiveApplyError = (...args) => sidebarTreeLiveApply.setTreeLiveApplyError(...args);
const objectIdentityAttr = (...args) => sidebarTreeLiveApply.objectIdentityAttr(...args);
const renderSidebarSnapshot = (...args) => sidebarTreeLiveApply.renderSidebarSnapshot(...args);
const refreshLocalFolderSidebarSnapshot = (...args) => sidebarTreeLiveApply.refreshLocalFolderSidebarSnapshot(...args);
const startLocalFolderSidebarWatch = (...args) => sidebarTreeLiveApply.startLocalFolderSidebarWatch(...args);
const deltaNeedsProjectionRefresh = (...args) => sidebarTreeLiveApply.deltaNeedsProjectionRefresh(...args);
const toggleChildren = (...args) => sidebarTreeLiveApply.toggleChildren(...args);
function dispatchSidebarEvent(name, detail) {
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
}
const sidebarFileTreeOpen = createSidebarFileTreeOpenRuntime({
copyWorkspaceSourceParams,
currentDocumentId,
currentRootUri,
fileTreeIconKindForFileName: (...args) => fileTreeIconKindForFileName(...args),
getNavigationInFlight: () => mnoteNavigationInFlight,
isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args),
isNonOfficeAttachmentName: (...args) => isNonOfficeAttachmentName(...args),
openCodeEditorAttachment: (...args) => openCodeEditorAttachment(...args),
resolveWorkspaceId,
setNavigationInFlight: (value) => { mnoteNavigationInFlight = String(value || ''); },
shouldOpenLocalResourceInNewWindow: (...args) => shouldOpenLocalResourceInNewWindow(...args),
uploadedFileSize: (...args) => uploadedFileSize(...args),
});
const inferOnlyOfficeFileType = (...args) => sidebarFileTreeOpen.inferOnlyOfficeFileType(...args);
const buildOnlyOfficeOpenUrl = (...args) => sidebarFileTreeOpen.buildOnlyOfficeOpenUrl(...args);
const buildOnlyOfficeOpenPath = (...args) => sidebarFileTreeOpen.buildOnlyOfficeOpenPath(...args);
const buildLocalOnlyOfficeOpenUrl = (...args) => sidebarFileTreeOpen.buildLocalOnlyOfficeOpenUrl(...args);
const openLocalOfficeFileInActiveTab = (...args) => sidebarFileTreeOpen.openLocalOfficeFileInActiveTab(...args);
const buildMindmapOpenPath = (...args) => sidebarFileTreeOpen.buildMindmapOpenPath(...args);
const navigateToMindmapObject = (...args) => sidebarFileTreeOpen.navigateToMindmapObject(...args);
const isMindmapAssetDetail = (...args) => sidebarFileTreeOpen.isMindmapAssetDetail(...args);
const localFilePathFromAssetId = (...args) => sidebarFileTreeOpen.localFilePathFromAssetId(...args);
const buildLocalFileOpenUrl = (...args) => sidebarFileTreeOpen.buildLocalFileOpenUrl(...args);
const openLocalResourceInActiveTab = (...args) => sidebarFileTreeOpen.openLocalResourceInActiveTab(...args);
const readFileTreeObjectIdentity = (...args) => sidebarFileTreeOpen.readFileTreeObjectIdentity(...args);
const fetchCurrentOnlyOfficeUserId = (...args) => sidebarFileTreeOpen.fetchCurrentOnlyOfficeUserId(...args);
const openConvexAssetFromFileTree = (...args) => sidebarFileTreeOpen.openConvexAssetFromFileTree(...args);
var sidebarFileTreeUpload = null;
const fileTreeRowsForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeRowsForUploadPreflight(...args);
const fileTreeRowCapabilities = (...args) => sidebarFileTreeUpload.fileTreeRowCapabilities(...args);
const fileTreeRowIsReadonly = (...args) => sidebarFileTreeUpload.fileTreeRowIsReadonly(...args);
const blockReadonlyFileTreeAction = (...args) => sidebarFileTreeUpload.blockReadonlyFileTreeAction(...args);
const fileTreeDocumentParentsForPreflight = (...args) => sidebarFileTreeUpload.fileTreeDocumentParentsForPreflight(...args);
const fileTreeTargetChildrenForPreflight = (...args) => sidebarFileTreeUpload.fileTreeTargetChildrenForPreflight(...args);
const fileTreeDropPreflightRows = (...args) => sidebarFileTreeUpload.fileTreeDropPreflightRows(...args);
const ensureFileTreeWritableTarget = (...args) => sidebarFileTreeUpload.ensureFileTreeWritableTarget(...args);
const fileTreeDocumentWorkspacesForUploadPreflight = (...args) => sidebarFileTreeUpload.fileTreeDocumentWorkspacesForUploadPreflight(...args);
const preflightFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.preflightFileTreeUploadTarget(...args);
const fallbackFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.fallbackFileTreeUploadTarget(...args);
const resolveFileTreeUploadTarget = (...args) => sidebarFileTreeUpload.resolveFileTreeUploadTarget(...args);
function localUploadRuntimeFunction(name) {
var runtime = window.__mnoteLocalUploadRuntime;
var fn = runtime && runtime[name];
return typeof fn === 'function' ? fn : null;
}
function fileTreeRuntimeFunction(name) {
var runtime = window.__mnoteFileTreeRuntime;
var fn = runtime && runtime[name];
return typeof fn === 'function' ? fn : null;
}
function fileTreeRuntimeDeps() {
return {
currentSourceKind: currentSourceKind,
currentDocumentId: currentDocumentId,
localFilePathFromAssetId: localFilePathFromAssetId,
rowTitle: rowTitle,
resolveWorkspaceId: resolveWorkspaceId,
cssEscape: cssEscape,
escapeHtml: escapeHtml,
objectIdentityAttr: objectIdentityAttr,
uploadedAssetTitle: uploadedAssetTitle,
uploadedAssetType: uploadedAssetType,
fileTreeIconKindForFileName: fileTreeIconKindForFileName
};
}
function fileTreeSelectionRuntimeFunction(name) {
var runtime = window.__mnoteFileTreeSelectionRuntime;
var fn = runtime && runtime[name];
return typeof fn === 'function' ? fn : null;
}
function fileTreeSelectionRuntimeDeps() {
return {
cssEscape: cssEscape
};
}
function uploadedAssetTitle(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
if (runtimeFn) return runtimeFn(asset);
return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
}
function uploadedAssetUrl(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedAssetUrl');
if (runtimeFn) return runtimeFn(asset);
return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
}
function localAssetOpenUrl(asset, download) {
var runtimeFn = localUploadRuntimeFunction('localAssetOpenUrl');
if (runtimeFn) return runtimeFn(asset, download, { rootUri: currentRootUri() });
if (!isLocalUploadedAsset(asset)) return '';
var rootUri = String(asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri();
var rootRelativePath = String(asset && (asset.rootRelativePath || asset.root_relative_path) || '').trim();
if (!rootUri || !rootRelativePath) return '';
var url = new URL('/api/local-folder/files/open', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('path', rootRelativePath);
if (download) url.searchParams.set('download', 'true');
return url.toString();
}
function uploadedAssetType(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedAssetType');
if (runtimeFn) return runtimeFn(asset);
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
}
function fileTreeIconKindForFileName(fileName) {
var runtimeFn = localUploadRuntimeFunction('fileTreeIconKindForFileName');
if (runtimeFn) return runtimeFn(fileName);
var name = String(fileName || '').trim().toLowerCase();
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
if (['doc', 'docx', 'odt', 'rtf', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'office';
if (ext === 'md' || ext === 'markdown') return 'markdown';
if (ext === 'pdf') return 'pdf';
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image';
return '';
}
function shouldOpenLocalResourceInNewWindow(fileName) {
return false;
}
function isLocalUploadedAsset(asset) {
var runtimeFn = localUploadRuntimeFunction('isLocalUploadedAsset');
if (runtimeFn) return runtimeFn(asset);
var id = String(asset && asset.id || '').trim();
return String(asset && asset.sourceKind || '').trim() === 'local_folder' || id.indexOf('local:asset:') === 0;
}
function uploadedAssetExtension(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedAssetExtension');
if (runtimeFn) return runtimeFn(asset);
var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
return match ? match[1] : '';
}
function isNonOfficeAttachmentName(name, ext) {
var runtimeFn = localUploadRuntimeFunction('isNonOfficeAttachmentName');
if (runtimeFn) return runtimeFn(name, ext);
var codeFileNames = [
'.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc',
'dockerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'
];
return [
'pdf', 'toml', 'json', 'yaml', 'yml', 'md', 'markdown', 'txt', 'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss',
'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'py', 'rs', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs',
'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lock', 'log', 'vue', 'svelte', 'astro', 'jsonc', 'json5',
'mts', 'cts', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj',
'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd',
'psm1', 'psd1', 'dockerfile', 'containerfile', 'proto', 'graphql', 'gql', 'prisma', 'tf', 'tfvars',
'hcl', 'nix', 'cmake', 'bazel', 'bzl', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop',
'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'
].indexOf(ext) >= 0 || codeFileNames.indexOf(name) >= 0;
}
function attachmentExtensionFromFileName(fileName) {
var runtimeFn = localUploadRuntimeFunction('attachmentExtensionFromFileName');
if (runtimeFn) return runtimeFn(fileName);
var name = String(fileName || '').trim().toLowerCase();
return name.indexOf('.') >= 0 ? name.split('.').pop() : '';
}
function isPdfAttachmentFileName(fileName) {
var runtimeFn = localUploadRuntimeFunction('isPdfAttachmentFileName');
if (runtimeFn) return runtimeFn(fileName);
return attachmentExtensionFromFileName(fileName) === 'pdf';
}
function isCodeAttachmentFileName(fileName) {
var runtimeFn = localUploadRuntimeFunction('isCodeAttachmentFileName');
if (runtimeFn) return runtimeFn(fileName);
var name = String(fileName || '').trim().toLowerCase();
var ext = attachmentExtensionFromFileName(name);
return ext !== 'pdf' && isNonOfficeAttachmentName(name, ext);
}
function inferCodeAttachmentLanguage(fileName) {
var runtimeFn = localUploadRuntimeFunction('inferCodeAttachmentLanguage');
if (runtimeFn) return runtimeFn(fileName);
var name = String(fileName || '').trim().toLowerCase();
var ext = attachmentExtensionFromFileName(name);
var byName = {
'dockerfile': 'dockerfile',
'makefile': 'makefile',
'cmakelists.txt': 'cmake',
'.gitignore': 'gitignore',
'.gitattributes': 'gitattributes',
'.editorconfig': 'ini',
'.env': 'dotenv'
};
if (byName[name]) return byName[name];
var byExt = {
bash: 'bash', bat: 'batch', c: 'c', cjs: 'javascript', cmd: 'batch', conf: 'text', cpp: 'cpp',
cs: 'csharp', css: 'css', cts: 'typescript', dart: 'dart', dockerfile: 'dockerfile', env: 'dotenv',
go: 'go', gql: 'graphql', gradle: 'groovy', graphql: 'graphql', h: 'c', hcl: 'hcl', hpp: 'cpp',
htm: 'html', html: 'html', ini: 'ini', java: 'java', js: 'javascript', json: 'json', json5: 'json',
jsonc: 'jsonc', jsx: 'javascript', kt: 'kotlin', kts: 'kotlin', less: 'less', log: 'text',
lua: 'lua', m: 'objective-c', markdown: 'markdown', md: 'markdown', mjs: 'javascript',
mts: 'typescript', nix: 'nix', php: 'php', pl: 'perl', pm: 'perl', prisma: 'prisma',
proto: 'protobuf', ps1: 'powershell', py: 'python', r: 'r', rb: 'ruby', rs: 'rust',
scss: 'scss', sh: 'bash', sql: 'sql', svelte: 'svelte', swift: 'swift', tf: 'terraform',
tfvars: 'terraform', toml: 'toml', ts: 'typescript', tsx: 'typescript', txt: 'text',
vue: 'vue', xml: 'xml', yaml: 'yaml', yml: 'yaml', zsh: 'bash'
};
return byExt[ext] || 'text';
}
function attachmentClassForFileName(fileName) {
var runtimeFn = localUploadRuntimeFunction('attachmentClassForFileName');
if (runtimeFn) return runtimeFn(fileName);
var name = String(fileName || '').trim().toLowerCase();
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word';
if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt';
if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet';
if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf';
if (isNonOfficeAttachmentName(name, ext)) {
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code';
}
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file';
}
function uploadedAttachmentClass(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedAttachmentClass');
if (runtimeFn) return runtimeFn(asset);
return attachmentClassForFileName(uploadedAssetTitle(asset));
}
function buildOnlyOfficeAssetOpenUrl(asset, userId) {
var title = uploadedAssetTitle(asset);
var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type);
if (!fileType) return '';
var assetId = String(asset && asset.id || '').trim();
if (isLocalUploadedAsset(asset)) {
var localUrl = localAssetOpenUrl(asset, false);
if (!localUrl) return '';
return buildOnlyOfficeOpenUrl({
fileUrl: localUrl,
fileName: title,
fileType: fileType,
assetId: assetId,
documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
userId: userId || '',
mode: 'view'
});
}
return buildOnlyOfficeOpenUrl({
fileUrl: assetId ? '' : uploadedAssetUrl(asset),
fileName: title,
fileType: fileType,
assetId: assetId,
documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
userId: userId || '',
mode: 'view'
});
}
function uploadedFileSize(asset) {
var runtimeFn = localUploadRuntimeFunction('uploadedFileSize');
if (runtimeFn) return runtimeFn(asset);
var size = Number(asset && (asset.file_size || asset.fileSize) || 0);
if (!Number.isFinite(size) || size <= 0) return '';
if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2) + ' MB';
if (size >= 1024) return (size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2) + ' KB';
return String(Math.round(size)) + ' B';
}
var attachmentMetaCache = Object.create(null);
var attachmentMetaPending = Object.create(null);
var legacyOfficeAttachmentIndex = null;
var legacyOfficeAttachmentIndexPending = null;
function parseCurrentWorkspaceId() {
return (new URLSearchParams(window.location.search).get('workspaceId') || '').trim();
}
async function fetchLegacyOfficeAttachmentIndex() {
if (legacyOfficeAttachmentIndex) return legacyOfficeAttachmentIndex;
if (legacyOfficeAttachmentIndexPending) return legacyOfficeAttachmentIndexPending;
var documentId = currentDocumentId();
var workspaceId = parseCurrentWorkspaceId();
if (!documentId || !workspaceId) {
legacyOfficeAttachmentIndex = Object.create(null);
return legacyOfficeAttachmentIndex;
}
var projectionUrl = new URL('/api/tree/projections/file', window.location.origin);
projectionUrl.searchParams.set('documentId', documentId);
projectionUrl.searchParams.set('workspaceId', workspaceId);
var sourcePayload = currentWorkspaceSourcePayload();
if (sourcePayload.sourceKind) projectionUrl.searchParams.set('sourceKind', sourcePayload.sourceKind);
if (sourcePayload.rootUri) projectionUrl.searchParams.set('rootUri', sourcePayload.rootUri);
legacyOfficeAttachmentIndexPending = fetch(projectionUrl.toString(), {
method: 'GET',
credentials: 'include',
cache: 'no-store'
}).then(function(response) {
return response.json().catch(function() { return null; }).then(function(payload) {
var items = payload && payload.ok && payload.result && Array.isArray(payload.result.items)
? payload.result.items
: [];
var index = Object.create(null);
items.forEach(function(item) {
if (!item || item.rowKind !== 'asset') return;
var title = String(item.title || '').trim();
if (!title || index[title]) return;
var fileType = inferOnlyOfficeFileType(title, '');
if (!fileType) return;
var rowId = String(item.rowId || '').trim();
var assetId = String(item.assetId || '').trim();
if (!assetId && rowId.indexOf('asset:') === 0) assetId = rowId.slice('asset:'.length);
if (!assetId) return;
index[title] = {
assetId: assetId,
fileName: title,
fileType: fileType,
documentId: documentId
};
});
legacyOfficeAttachmentIndex = index;
return index;
});
}).catch(function() {
var empty = Object.create(null);
legacyOfficeAttachmentIndex = empty;
return empty;
}).finally(function() {
legacyOfficeAttachmentIndexPending = null;
});
return legacyOfficeAttachmentIndexPending;
}
async function healLegacyOfficeAttachmentParagraphs() {
var editor = document.querySelector('.editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return;
var index = await fetchLegacyOfficeAttachmentIndex();
var paragraphs = Array.from(editor.querySelectorAll('p'));
paragraphs.forEach(function(paragraph) {
if (!(paragraph instanceof HTMLParagraphElement)) return;
if (paragraph.querySelector('a, img, video, audio, table, iframe, canvas')) return;
if (paragraph.childNodes.length !== 1 || paragraph.firstChild?.nodeType !== Node.TEXT_NODE) return;
var fileName = String(paragraph.textContent || '').trim();
if (!fileName) return;
var detail = index[fileName];
if (!detail) return;
var link = document.createElement('a');
link.textContent = fileName;
link.setAttribute('href', buildOnlyOfficeOpenPath({
fileUrl: '',
fileName: detail.fileName,
fileType: detail.fileType,
assetId: detail.assetId,
documentId: detail.documentId || currentDocumentId() || '',
userId: '',
mode: 'view'
}));
link.setAttribute('data-mnote-attachment-link', 'true');
link.setAttribute('data-asset-id', detail.assetId);
attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
if (name) link.classList.add(name);
});
paragraph.replaceChildren(link);
enhanceEditorAttachmentLink(link);
});
}
function applyEditorAttachmentMeta(link, meta) {
if (!(link instanceof HTMLAnchorElement) || !meta) return;
if (meta.assetId) link.setAttribute('data-asset-id', meta.assetId);
if (meta.fileSize) link.setAttribute('data-file-size', meta.fileSize);
}
async function hydrateEditorAttachmentMeta(link) {
if (!(link instanceof HTMLAnchorElement)) return;
var detail = detailFromEditorAttachmentLink(link);
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
var localFilePath = localFilePathFromAssetId(assetId);
if (localFilePath) {
var localMeta = {
assetId: assetId,
fileSize: String(detail && detail.fileSize || '').trim()
};
attachmentMetaCache[assetId] = localMeta;
applyEditorAttachmentMeta(link, localMeta);
return;
}
if (attachmentMetaCache[assetId]) {
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
return;
}
if (attachmentMetaPending[assetId]) {
try { await attachmentMetaPending[assetId]; } catch (_) {}
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
return;
}
attachmentMetaPending[assetId] = fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
method: 'GET',
credentials: 'include',
cache: 'no-store'
}).then(function(response) {
return response.json().catch(function() { return null; }).then(function(payload) {
if (!response.ok || !payload) return null;
var asset = payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
var meta = {
assetId: assetId,
fileSize: uploadedFileSize(asset)
};
attachmentMetaCache[assetId] = meta;
return meta;
});
}).catch(function() {
return null;
}).finally(function() {
delete attachmentMetaPending[assetId];
});
try {
var meta = await attachmentMetaPending[assetId];
applyEditorAttachmentMeta(link, meta);
} catch (_) {}
}
function revealFileTreeRow(row) {
var runtimeFn = fileTreeRuntimeFunction('revealFileTreeRow');
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
if (!(row instanceof HTMLElement)) return;
var node = row.closest('.tree-node');
while (node && node.parentElement) {
if (node.parentElement.classList && node.parentElement.classList.contains('tree-children')) {
node.parentElement.classList.remove('tree-children--collapsed');
var parentNode = node.parentElement.closest('.tree-node');
var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row') : null;
if (parentRow instanceof HTMLElement) {
parentRow.setAttribute('aria-expanded', 'true');
var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
}
}
node = node.parentElement.closest('.tree-node');
}
try { row.scrollIntoView({ block: 'nearest' }); } catch (_) {}
}
function revealFileTreeAssetRow(assetId) {
var runtimeFn = fileTreeRuntimeFunction('revealFileTreeAssetRow');
if (runtimeFn) return runtimeFn(assetId, fileTreeRuntimeDeps());
if (!assetId) return false;
var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]');
if (!(row instanceof HTMLElement)) return false;
revealFileTreeRow(row);
return true;
}
function appendUploadedAssetRow(asset, documentId) {
var runtimeFn = fileTreeRuntimeFunction('appendUploadedAssetRow');
if (runtimeFn) return runtimeFn(asset, documentId, fileTreeRuntimeDeps());
var assetId = String(asset && asset.id || '').trim();
if (!assetId) return false;
if (revealFileTreeAssetRow(assetId)) return true;
var objectKind = String(asset && (asset.objectKind || asset.resourceKind || '') || '').trim();
if (!objectKind && (String(asset && (asset.asset_type || asset.assetType) || '').trim() === 'mindmap' || /\.mindmap\.json$/i.test(assetId))) {
objectKind = 'mindmap';
}
var targetDocumentId = String(documentId || asset.document_id || asset.documentId || currentDocumentId() || '').trim();
var parentRow = targetDocumentId
? document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + targetDocumentId) + '"]')
: null;
if (!parentRow && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false;
if (!parentRow) parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-kind="document"]');
var root = document.querySelector('#sidebar-file-tree-root .tree-root');
if (!root && !parentRow) return false;
var parentLi = parentRow ? parentRow.closest('.tree-node') : null;
var children = parentLi ? parentLi.querySelector(':scope > .tree-children') : null;
if (parentLi && !children) {
children = document.createElement('ul');
children.className = 'tree-children';
parentLi.appendChild(children);
}
if (children) {
children.classList.remove('tree-children--collapsed');
if (parentRow) {
parentRow.setAttribute('aria-expanded', 'true');
var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
}
}
var container = children || root;
var li = document.createElement('li');
li.className = 'tree-node';
var objectIdentity = {
objectKind: objectKind || 'attachment',
documentId: targetDocumentId || null,
blockId: null,
assetId: assetId
};
li.setAttribute('data-node-id', 'asset:' + assetId);
var title = uploadedAssetTitle(asset);
var iconKind = fileTreeIconKindForFileName(title) || uploadedAssetType(asset) || 'file';
if (objectKind === 'mindmap') iconKind = 'mindmap';
li.innerHTML =
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
'<span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span>' +
'<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
'<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
container.appendChild(li);
revealFileTreeRow(li.querySelector('.tree-row'));
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId);
return true;
}
function removeFileTreeAssetRow(assetId) {
var normalized = String(assetId || '').trim();
if (!normalized) return false;
var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(normalized) + '"]');
var node = row ? row.closest('.tree-node') : null;
if (!node || !node.parentElement) return false;
node.parentElement.removeChild(node);
return true;
}
function applyAssetsChangedToFileTree(detail) {
detail = detail || {};
var docId = String(detail.docId || detail.documentId || currentDocumentId() || '').trim();
var changed = false;
var explicitAsset = detail.asset && typeof detail.asset === 'object' ? detail.asset : null;
if (explicitAsset) {
appendUploadedAssetRow(explicitAsset, docId);
changed = true;
}
var mindmapAssetIds = Array.isArray(detail.mindmapAssetIds) ? detail.mindmapAssetIds : [];
if (detail.mindmapDeleted === true) {
mindmapAssetIds.forEach(function(assetId) {
if (removeFileTreeAssetRow(assetId)) changed = true;
});
}
if (changed) {
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
}
}
function mindmapAssetFromTarget(documentId, mindmapId, writeResult) {
var docId = String(documentId || currentDocumentId() || '').trim();
var assetId = String(mindmapId || '').trim();
if (!docId || !assetId) return null;
var result = writeResult && typeof writeResult === 'object' ? writeResult : {};
var resultAssetId = String(result.assetId || result.id || '').trim();
var relativePath = String(result.relativePath || result.rootRelativePath || '').trim();
var fileName = String(result.fileName || '').trim() || shortMindmapFileName(assetId);
return {
id: resultAssetId || (relativePath ? 'local-file:' + relativePath : assetId),
document_id: docId,
asset_type: 'mindmap',
file_name: fileName,
file_url: relativePath || ('/documents/' + encodeURIComponent(docId) + '/' + encodeURIComponent(fileName)),
sourcePath: relativePath,
rootRelativePath: relativePath,
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
objectKind: 'mindmap'
};
}
function shortMindmapFileName(mindmapId) {
var runtimeFn = fileTreeRuntimeFunction('shortMindmapFileName');
if (runtimeFn) return runtimeFn(mindmapId);
var raw = String(mindmapId || '').trim();
var pathName = raw.indexOf('/') >= 0 ? raw.split('/').pop() : raw;
if (/\.json$/i.test(pathName)) return pathName;
var digits = raw.match(/(\d{4,})$/);
var suffix = digits ? digits[1].slice(-6) : '';
return suffix ? '思维导图' + suffix + '.json' : '思维导图.json';
}
function parseMindmapApiTarget(input) {
var runtimeFn = fileTreeRuntimeFunction('parseMindmapApiTarget');
if (runtimeFn) return runtimeFn(input);
try {
var rawUrl = typeof input === 'string'
? input
: input && typeof input.url === 'string'
? input.url
: '';
if (!rawUrl) return null;
var url = new URL(rawUrl, window.location.origin);
var match = /^\/api\/mindmap\/([^/]+)\/([^/]+)$/.exec(url.pathname);
if (!match) return null;
return {
documentId: decodeURIComponent(match[1]),
mindmapId: decodeURIComponent(match[2])
};
} catch (_) {
return null;
}
}
function withLocalMindmapSourceParams(input) {
var target = parseMindmapApiTarget(input);
if (!target || currentSourceKind() !== 'local_folder') return input;
var rootUri = currentRootUri();
if (!rootUri) return input;
var rawUrl = typeof input === 'string'
? input
: input && typeof input.url === 'string'
? input.url
: '';
if (!rawUrl) return input;
var url = new URL(rawUrl, window.location.origin);
url.searchParams.set('sourceKind', 'local_folder');
url.searchParams.set('rootUri', rootUri);
if (typeof input === 'string') return url.pathname + url.search;
if (typeof Request !== 'undefined' && input instanceof Request) {
return new Request(url.toString(), input);
}
return url.toString();
}
function requestMethod(input, init) {
var runtimeFn = fileTreeRuntimeFunction('requestMethod');
if (runtimeFn) return runtimeFn(input, init);
return String(
init && init.method
? init.method
: input && typeof input.method === 'string'
? input.method
: 'GET'
).toUpperCase();
}
function requestBodyHasMindmapCreateOnly(init) {
var runtimeFn = fileTreeRuntimeFunction('requestBodyHasMindmapCreateOnly');
if (runtimeFn) return runtimeFn(init);
var body = init && typeof init.body === 'string' ? init.body : '';
if (!body) return false;
try {
var payload = JSON.parse(body);
return payload && payload.createOnly === true;
} catch (_) {
return false;
}
}
function applyMindmapApiMutationToFileTree(target, payload) {
if (!target || !target.documentId || !target.mindmapId) return;
var asset = mindmapAssetFromTarget(target.documentId, target.mindmapId, payload && payload.writeResult);
if (!asset) return;
var appended = appendUploadedAssetRow(asset, target.documentId);
if (!appended && currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', asset.id || target.mindmapId);
}
function installMindmapAssetFetchObserver() {
if (window.__mnoteMindmapAssetFetchObserverInstalled === true) return;
if (typeof window.fetch !== 'function') return;
window.__mnoteMindmapAssetFetchObserverInstalled = true;
var originalFetch = window.fetch.bind(window);
window.fetch = function(input, init) {
var nextInput = withLocalMindmapSourceParams(input);
var target = parseMindmapApiTarget(nextInput);
var method = requestMethod(input, init);
var createOnly = requestBodyHasMindmapCreateOnly(init || {});
return originalFetch(nextInput, init).then(function(response) {
if (target && method === 'POST' && response && response.ok) {
var cloned = response.clone();
void cloned.json().then(function(payload) {
if (createOnly || target.documentId === currentDocumentId()) {
applyMindmapApiMutationToFileTree(target, payload);
}
}).catch(function() {
if (createOnly || target.documentId === currentDocumentId()) {
applyMindmapApiMutationToFileTree(target, null);
}
});
}
return response;
});
};
}
async function archiveLocalFileTreeAsset(row, assetId) {
if (!assetId) return false;
await dispatchTreeCommand(row || document.body, {
action: 'archive',
workspaceId: resolveWorkspaceId(row || document.body),
documentId: assetId
});
removeFileTreeAssetRow(assetId);
return true;
}
async function deleteSingleFileTreeAsset(detail, trigger) {
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return false;
var row = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : null;
if (!row) row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]');
if (currentSourceKind() === 'local_folder') {
return archiveLocalFileTreeAsset(row, assetId);
}
var documentId = String(detail && detail.documentId || '').trim();
var kind = classifySidebarFileTreeAsset(row);
if (kind === 'mindmap') {
var response = await fetch('/api/mindmap/' + encodeURIComponent(documentId) + '/' + encodeURIComponent(assetId), { method: 'DELETE' });
if (!response.ok) throw new Error('mindmap_delete_failed_' + response.status);
removeFileTreeAssetRow(assetId);
return true;
}
if (kind === 'table') {
var tableResponse = await fetch('/api/tables/' + encodeURIComponent(assetId), { method: 'DELETE' });
if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status);
removeFileTreeAssetRow(assetId);
return true;
}
await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: [assetId] });
removeFileTreeAssetRow(assetId);
return true;
}
function resolveEditorUploadContext(detail) {
var runtimeFn = localUploadRuntimeFunction('resolveEditorUploadContext');
if (runtimeFn) {
return runtimeFn(detail || {}, {
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
editorUploadRootFromElement: editorUploadRootFromElement,
currentDocumentId: currentDocumentId,
resolveWorkspaceId: resolveWorkspaceId
});
}
var root = null;
var selector = detail && detail.editorRootSelector ? String(detail.editorRootSelector) : '';
if (selector) {
try {
var selected = document.querySelector(selector);
if (selected instanceof HTMLElement) root = selected;
} catch (_) {}
}
if (!root && window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
root = window.__mnoteIntendedSlashRoot;
}
if (!root && window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
root = window.__mnoteLastEditorUploadRoot;
}
if (!root && document.activeElement instanceof Element) {
root = editorUploadRootFromElement(document.activeElement);
}
if (!root) {
var focused = document.querySelector(EDITOR_UPLOAD_ROOT_SELECTOR + ' .ProseMirror:focus-within');
root = editorUploadRootFromElement(focused);
}
if (!root) {
root = document.querySelector('[data-editor-host-kind="leptos_tiptap_island"][data-pane-role="primary"]');
}
var pane = root instanceof Element ? root.closest('.document-pane[data-pane-role]') : null;
var shell = root instanceof Element ? root.closest('.document-shell[data-document-id]') : null;
return {
root: root instanceof HTMLElement ? root : null,
documentId: String(
detail && detail.documentId
|| (root instanceof HTMLElement && root.getAttribute('data-document-id'))
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-document-id'))
|| (shell instanceof HTMLElement && shell.getAttribute('data-document-id'))
|| currentDocumentId()
|| ''
).trim(),
workspaceId: String(
detail && detail.workspaceId
|| (root instanceof HTMLElement && root.getAttribute('data-workspace-id'))
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-workspace-id'))
|| (shell instanceof HTMLElement && shell.getAttribute('data-workspace-id'))
|| resolveWorkspaceId(document.body)
|| ''
).trim()
};
}
function editorRootFromUploadOptions(options) {
var runtimeFn = localUploadRuntimeFunction('editorRootFromUploadOptions');
if (runtimeFn) {
return runtimeFn(options || {}, {
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
editorUploadRootFromElement: editorUploadRootFromElement
});
}
if (options && options.editorRoot instanceof HTMLElement) return options.editorRoot;
if (window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
return window.__mnoteIntendedSlashRoot;
}
if (window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
return window.__mnoteLastEditorUploadRoot;
}
var focused = document.querySelector(EDITOR_UPLOAD_ROOT_SELECTOR + ' .ProseMirror:focus-within');
return editorUploadRootFromElement(focused);
}
async function fetchWithTimeout(input, init, timeoutMs, label) {
var runtimeFn = localUploadRuntimeFunction('fetchWithTimeout');
if (runtimeFn) {
return await runtimeFn(input, init || {}, timeoutMs, label);
}
var controller = typeof AbortController === 'function' ? new AbortController() : null;
var timer = 0;
try {
if (controller) {
timer = window.setTimeout(function() {
controller.abort();
}, Math.max(1000, Number(timeoutMs) || 15000));
}
var nextInit = Object.assign({}, init || {});
if (controller) nextInit.signal = controller.signal;
return await fetch(input, nextInit);
} catch (error) {
if (error && error.name === 'AbortError') {
throw new Error((label || '请求') + '超时');
}
throw error;
} finally {
if (timer) window.clearTimeout(timer);
}
}
async function insertUploadedAssetIntoEditor(asset, targetRoot) {
var runtimeFn = localUploadRuntimeFunction('insertUploadedAssetIntoEditor');
if (runtimeFn) {
return await runtimeFn(asset, targetRoot, {
uploadedAssetTitle: uploadedAssetTitle,
localAssetOpenUrl: localAssetOpenUrl,
uploadedAssetUrl: uploadedAssetUrl,
uploadedAssetType: uploadedAssetType,
isLocalUploadedAsset: isLocalUploadedAsset,
buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl,
fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId,
buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath,
inferOnlyOfficeFileType: inferOnlyOfficeFileType,
currentDocumentId: currentDocumentId,
uploadedAttachmentClass: uploadedAttachmentClass,
uploadedFileSize: uploadedFileSize,
cssEscape: cssEscape,
enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks
});
}
var editorRoot = targetRoot instanceof HTMLElement
? targetRoot.querySelector('.editor-surface .ProseMirror')
: document.querySelector('.editor-surface .ProseMirror');
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
var editor = editorRoot && editorRoot.editor;
if (!editor || !editor.chain) {
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'editor_unavailable');
return false;
}
var title = uploadedAssetTitle(asset);
var url = localAssetOpenUrl(asset, false) || uploadedAssetUrl(asset);
var type = uploadedAssetType(asset);
var assetId = String(asset && asset.id || '').trim();
var sizeLabel = uploadedFileSize(asset);
try {
if (type === 'image' && url) {
return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
}
var isLocalAsset = isLocalUploadedAsset(asset);
var userId = '';
var onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
if (onlyOfficeUrl && assetId) {
userId = await fetchCurrentOnlyOfficeUserId();
onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
}
var href = onlyOfficeUrl || url;
if (href) {
var storedHref = onlyOfficeUrl
? (isLocalAsset ? onlyOfficeUrl : buildOnlyOfficeOpenPath({
fileUrl: '',
fileName: title,
fileType: inferOnlyOfficeFileType(title, asset && asset.mime_type) || 'docx',
assetId: assetId,
documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
userId: userId || '',
mode: 'view'
}))
: href;
var inserted = editor.chain().focus().insertContent([
{
type: 'paragraph',
content: [{
type: 'text',
text: title,
marks: [{
type: 'link',
attrs: {
href: storedHref,
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: uploadedAttachmentClass(asset)
}
}]
}]
},
{ type: 'paragraph' }
]).focus('end').run() === true;
if (inserted) {
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'true');
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId || '');
document.documentElement.removeAttribute('data-mnote-last-upload-insert-error');
} else {
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed');
}
window.setTimeout(function() {
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
enhanceEditorAttachmentLinks();
var selector = assetId
? '.editor-surface .ProseMirror a[href*="' + cssEscape(assetId) + '"]'
: '.editor-surface .ProseMirror a';
var link = targetRoot instanceof HTMLElement
? targetRoot.querySelector(selector)
: document.querySelector(selector);
if (link instanceof HTMLElement) {
link.setAttribute('data-mnote-attachment-link', 'true');
if (assetId) link.setAttribute('data-asset-id', assetId);
if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
}
}, 0);
return inserted;
}
} catch (error) {
console.warn('[mnote upload] insert uploaded asset failed', error);
}
return false;
}
async function uploadFileToMediaAsset(file, plan, options) {
var runtimeFn = localUploadRuntimeFunction('uploadFileToMediaAsset');
if (runtimeFn) {
return await runtimeFn(file, plan, options || {}, {
currentSourceKind: currentSourceKind,
currentRootUri: currentRootUri,
currentDocumentId: currentDocumentId,
editorRootFromUploadOptions: editorRootFromUploadOptions,
appendUploadedAssetRow: appendUploadedAssetRow,
refreshLocalFolderSidebarSnapshot: refreshLocalFolderSidebarSnapshot,
dispatchEvent: function(event) { return window.dispatchEvent(event); },
CustomEvent: CustomEvent,
buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl,
fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId,
buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath,
inferOnlyOfficeFileType: inferOnlyOfficeFileType,
enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks,
cssEscape: cssEscape
});
}
if (currentSourceKind() === 'local_folder') {
var rootUri = currentRootUri();
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath');
var uploadIntent = String(plan && plan.uploadIntent || (hasFolderTarget ? 'filetree.folder.drop' : 'editor.markdown.attach')).trim();
if (!rootUri || !uploadIntent) {
throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId');
}
var localUploadRuntime = localUploadRuntimeFunction('uploadLocalFolderAsset');
var localPayload = null;
if (localUploadRuntime) {
var localResult = await localUploadRuntime(file, plan, {
rootUri: rootUri,
documentId: documentId,
timeoutMs: 15000
});
localPayload = localResult && localResult.asset ? { asset: localResult.asset } : null;
} else {
var localForm = new FormData();
localForm.append('file', file);
localForm.append('rootUri', rootUri);
localForm.append('uploadIntent', uploadIntent);
if (documentId) localForm.append('documentId', documentId);
if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || ''));
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment');
var localResponse = await fetchWithTimeout('/api/local-folder/assets/upload', {
method: 'POST',
credentials: 'include',
body: localForm
}, 15000, '本地上传');
localPayload = await localResponse.json().catch(function() { return null; });
if (!localResponse.ok || !localPayload || !localPayload.asset) {
throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败');
}
}
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(localPayload.asset, editorRootFromUploadOptions(options));
}
void refreshLocalFolderSidebarSnapshot();
window.dispatchEvent(new CustomEvent('wolai:local-assets-changed', {
detail: { docId: documentId, asset: localPayload.asset, assetIds: [localPayload.asset.id] }
}));
return localPayload.asset;
}
var mediaUploadRuntime = localUploadRuntimeFunction('uploadMediaAsset');
var payload = null;
if (mediaUploadRuntime) {
var mediaResult = await mediaUploadRuntime(file, plan, { timeoutMs: 15000 });
payload = mediaResult && mediaResult.asset ? { asset: mediaResult.asset } : null;
if (!payload || !payload.asset) {
throw new Error('上传失败');
}
} else {
var form = new FormData();
form.append('file', file);
form.append('workspaceId', plan.workspaceId);
form.append('documentId', plan.targetDocumentId);
if (plan.targetMindmapId) form.append('mindmapId', plan.targetMindmapId);
var response = await fetchWithTimeout('/api/media/upload', {
method: 'POST',
credentials: 'include',
body: form
}, 15000, '上传');
payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || !payload.asset) {
throw new Error(payload && payload.error ? payload.error : '上传失败');
}
}
appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(payload.asset, editorRootFromUploadOptions(options));
}
window.dispatchEvent(new CustomEvent('wolai:assets-changed', {
detail: { docId: plan.targetDocumentId, asset: payload.asset, assetIds: [payload.asset.id] }
}));
return payload.asset;
}
window.addEventListener('wolai:assets-changed', function(event) {
applyAssetsChangedToFileTree(event.detail || {});
});
installMindmapAssetFetchObserver();
async function uploadFilesWithResolvedTarget(files, detail, options) {
var runtimeFn = localUploadRuntimeFunction('uploadFilesWithResolvedTarget');
if (runtimeFn) {
return await runtimeFn(files, detail || {}, options || {}, {
resolveFileTreeUploadTarget: resolveFileTreeUploadTarget,
uploadFileToMediaAsset: uploadFileToMediaAsset,
alert: function(message) { window.alert(message); }
});
}
var list = Array.from(files || []).filter(Boolean);
if (!list.length) return [];
var plan = await resolveFileTreeUploadTarget(detail || {});
var uploaded = [];
var errors = [];
for (var i = 0; i < list.length; i += 1) {
try {
uploaded.push(await uploadFileToMediaAsset(list[i], plan, options || {}));
} catch (error) {
errors.push(list[i].name + ': ' + (error && error.message ? error.message : '上传失败'));
}
}
if (errors.length) {
window.alert('部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : ''));
}
return uploaded;
}
function openEditorUploadFilePicker(detail) {
var runtimeFn = localUploadRuntimeFunction('openEditorUploadFilePicker');
if (runtimeFn) {
return runtimeFn(detail || {}, {
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
editorUploadRootFromElement: editorUploadRootFromElement,
currentDocumentId: currentDocumentId,
resolveWorkspaceId: resolveWorkspaceId,
uploadFilesWithResolvedTarget: uploadFilesWithResolvedTarget
});
}
var uploadContext = resolveEditorUploadContext(detail || {});
var input = document.createElement('input');
input.type = 'file';
input.multiple = detail && detail.multiple !== false;
if (detail && detail.accept) input.accept = String(detail.accept);
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '-9999px';
document.body.appendChild(input);
input.addEventListener('change', function() {
var files = Array.from(input.files || []);
input.remove();
void uploadFilesWithResolvedTarget(files, {
workspaceId: uploadContext.workspaceId || resolveWorkspaceId(document.body),
documentId: uploadContext.documentId || currentDocumentId(),
targetRowId: null,
uploadIntent: 'editor.markdown.attach'
}, {
insertIntoEditor: detail && detail.insertIntoEditor !== false,
editorRoot: uploadContext.root
});
}, { once: true });
input.click();
}
window.addEventListener('mnote:editor-upload-request', function(event) {
openEditorUploadFilePicker(event.detail || {});
});
window.addEventListener('tree.filetree.external-drop', function(event) {
var detail = event.detail || {};
void uploadFilesWithResolvedTarget(detail.files || [], detail, {
insertIntoEditor: String(detail.documentId || '') === currentDocumentId()
});
});
document.addEventListener('dragover', function(event) {
var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
if (!editorTarget || !hasFiles) return;
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
}, true);
document.addEventListener('drop', function(event) {
var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
if (!editorTarget || !files.length) return;
event.preventDefault();
event.stopPropagation();
void uploadFilesWithResolvedTarget(files, {
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
targetRowId: null,
uploadIntent: 'editor.markdown.attach'
}, {
insertIntoEditor: true,
editorRoot: editorUploadRootFromElement(editorTarget)
});
}, true);
const sidebarFileTreeCommandState = {
get activeTreeContextMenu() { return activeTreeContextMenu; },
set activeTreeContextMenu(value) { activeTreeContextMenu = value; },
get sidebarFileTreeClipboard() { return sidebarFileTreeClipboard; },
set sidebarFileTreeClipboard(value) { sidebarFileTreeClipboard = value; },
};
const sidebarFileTreeCommand = createSidebarFileTreeCommandRuntime({
applyRemoveDocumentDelta,
createFileTreeFolder,
createPage,
cssEscape,
currentDocumentId,
currentSourceKind,
deleteSingleFileTreeAsset,
dispatchSidebarEvent,
dispatchTreeCommand,
ensureFileTreeWritableTarget,
fileTreeRuntimeDeps,
fileTreeRuntimeFunction,
fileTreeSelectionRuntimeDeps,
fileTreeSelectionRuntimeFunction,
isFileTreePageRow,
localFilePathFromAssetId,
normalizeFileTreePageRenameTitle,
openConvexAssetFromFileTree,
openEditorAttachmentDetail,
openEditorAttachmentDownload,
openEditorAttachmentEditTab,
openEditorAttachmentNewWindow,
refreshLocalFolderSidebarSnapshot,
resolveWorkspaceId,
runtimeState: sidebarFileTreeCommandState,
selectedSidebarFileTreeSelection: sidebarFileTreeSelection,
updateTitleEverywhere,
validateFileTreeRename,
});
const rowTitle = (...args) => sidebarFileTreeCommand.rowTitle(...args);
const rowCenter = (...args) => sidebarFileTreeCommand.rowCenter(...args);
const renameFileTreeAsset = (...args) => sidebarFileTreeCommand.renameFileTreeAsset(...args);
const beginFileTreeInlineRename = (...args) => sidebarFileTreeCommand.beginFileTreeInlineRename(...args);
const closeTreeContextMenu = (...args) => sidebarFileTreeCommand.closeTreeContextMenu(...args);
const copyTreeContextValue = (...args) => sidebarFileTreeCommand.copyTreeContextValue(...args);
const triggerBrowserDownload = (...args) => sidebarFileTreeCommand.triggerBrowserDownload(...args);
const recordFileTreeAction = (...args) => sidebarFileTreeCommand.recordFileTreeAction(...args);
const recordFileTreeActionStatus = (...args) => sidebarFileTreeCommand.recordFileTreeActionStatus(...args);
const documentHref = (...args) => sidebarFileTreeCommand.documentHref(...args);
const convertToPreviousSiblingChild = (...args) => sidebarFileTreeCommand.convertToPreviousSiblingChild(...args);
const fileTreeCopyPath = (...args) => sidebarFileTreeCommand.fileTreeCopyPath(...args);
const fileTreeMenuTargetParentId = (...args) => sidebarFileTreeCommand.fileTreeMenuTargetParentId(...args);
const withOfficeEditModeGuard = (...args) => sidebarFileTreeCommand.withOfficeEditModeGuard(...args);
const handleTreeContextMenuAction = (...args) => sidebarFileTreeCommand.handleTreeContextMenuAction(...args);
const appendTreeContextMenuButton = (...args) => sidebarFileTreeCommand.appendTreeContextMenuButton(...args);
const buildSidebarFileTreeContext = (...args) => sidebarFileTreeCommand.buildSidebarFileTreeContext(...args);
const evaluateSidebarFileTreeWhen = (...args) => sidebarFileTreeCommand.evaluateSidebarFileTreeWhen(...args);
const openTreeContextMenu = (...args) => sidebarFileTreeCommand.openTreeContextMenu(...args);
const openPageTreeContextMenu = (...args) => sidebarFileTreeCommand.openPageTreeContextMenu(...args);
const openFileTreeContextMenu = (...args) => sidebarFileTreeCommand.openFileTreeContextMenu(...args);
const visibleFileTreeRows = (...args) => sidebarFileTreeCommand.visibleFileTreeRows(...args);
const syncSidebarFileTreeSelection = (...args) => sidebarFileTreeCommand.syncSidebarFileTreeSelection(...args);
const selectSidebarFileTreeRow = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeRow(...args);
const selectSidebarFileTreeDocument = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeDocument(...args);
const selectSidebarFileTreeRowById = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeRowById(...args);
const pendingLocalFolderRestoreRowId = (...args) => sidebarFileTreeCommand.pendingLocalFolderRestoreRowId(...args);
const clearPendingLocalFolderRestoreRowId = (...args) => sidebarFileTreeCommand.clearPendingLocalFolderRestoreRowId(...args);
const applyPendingLocalFolderRestoreFocusOnce = (...args) => sidebarFileTreeCommand.applyPendingLocalFolderRestoreFocusOnce(...args);
const schedulePendingLocalFolderRestoreFocus = (...args) => sidebarFileTreeCommand.schedulePendingLocalFolderRestoreFocus(...args);
const selectedSidebarFileTreeRowIdsForDrag = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRowIdsForDrag(...args);
const selectedSidebarFileTreeRows = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRows(...args);
const fileTreeRowDocumentId = (...args) => sidebarFileTreeCommand.fileTreeRowDocumentId(...args);
const fileTreeRowAssetId = (...args) => sidebarFileTreeCommand.fileTreeRowAssetId(...args);
const decodeLocalEncodedPath = (...args) => sidebarFileTreeCommand.decodeLocalEncodedPath(...args);
const fileTreeRowLocalRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalRelativePath(...args);
const fileTreeRowLocalUploadTargetRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalUploadTargetRelativePath(...args);
const fileTreeRowKind = (...args) => sidebarFileTreeCommand.fileTreeRowKind(...args);
const isFileTreeDownloadableAssetRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableAssetRow(...args);
const isFileTreeDownloadableRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableRow(...args);
const fileTreeAssetDownloadDetail = (...args) => sidebarFileTreeCommand.fileTreeAssetDownloadDetail(...args);
const selectedSidebarFileTreeRowsForDownload = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRowsForDownload(...args);
const selectedSidebarFileTreeAssetRowsForDownload = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeAssetRowsForDownload(...args);
const downloadSelectedFileTreeAssetRows = (...args) => sidebarFileTreeCommand.downloadSelectedFileTreeAssetRows(...args);
const hasSelectedDocumentAncestor = (...args) => sidebarFileTreeCommand.hasSelectedDocumentAncestor(...args);
const classifySidebarFileTreeAsset = (...args) => sidebarFileTreeCommand.classifySidebarFileTreeAsset(...args);
const buildSidebarFileTreeDeletePlan = (...args) => sidebarFileTreeCommand.buildSidebarFileTreeDeletePlan(...args);
const sidebarFileTreeDeleteConfirmText = (...args) => sidebarFileTreeCommand.sidebarFileTreeDeleteConfirmText(...args);
const postSidebarFileTreeJson = (...args) => sidebarFileTreeCommand.postSidebarFileTreeJson(...args);
const fileTreeRowsByRowIds = (...args) => sidebarFileTreeCommand.fileTreeRowsByRowIds(...args);
const fileTreeChildCount = (...args) => sidebarFileTreeCommand.fileTreeChildCount(...args);
const pasteSidebarFileTreeClipboard = (...args) => sidebarFileTreeCommand.pasteSidebarFileTreeClipboard(...args);
const deleteSelectedSidebarFileTreeRows = (...args) => sidebarFileTreeCommand.deleteSelectedSidebarFileTreeRows(...args);
sidebarFileTreeUpload = createSidebarFileTreeUploadRuntime({
cssEscape,
currentDocumentId,
fileTreeRuntimeDeps,
fileTreeRuntimeFunction,
recordFileTreeAction,
recordFileTreeActionStatus,
resolveWorkspaceId,
rowTitle,
selectedSidebarFileTreeSelection: sidebarFileTreeSelection,
});
function ensureSearchModal() {
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
if (existing instanceof HTMLElement) return existing;
var overlay = document.createElement('div');
overlay.className = 'wolai-search-overlay';
overlay.setAttribute('data-testid', 'wolai-search-modal');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.hidden = true;
overlay.innerHTML = '' +
'<div class="wolai-search-dialog">' +
'<div class="wolai-search-input-row">' +
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
'<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
'</div>' +
'<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
'<div class="wolai-search-options-left">' +
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
'</div>' +
'<div class="wolai-search-options-right">' +
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
'</div>' +
'</div>' +
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
'<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
'</div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
if (input) input.addEventListener('input', scheduleSearchResultsRender);
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
button.addEventListener('click', function() {
var isOn = button.getAttribute('aria-checked') !== 'true';
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn);
scheduleSearchResultsRender();
});
});
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
overlay.addEventListener('click', function(event) {
if (event.target === overlay) closeSearchModal();
});
return overlay;
}
var activeSearchRequestId = 0;
var searchRenderTimer = 0;
function searchText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
}
function currentWorkspaceName() {
var name = document.querySelector('.sidebar-workspace-name');
return searchText(name && name.textContent) || '当前工作区';
}
function currentDocumentId() {
var shell = document.querySelector('.document-shell[data-document-id]');
var bodyId = document.body && document.body.getAttribute('data-document-id');
return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
}
function searchSwitchValue(overlay, name) {
var button = overlay.querySelector('[data-search-switch="' + name + '"]');
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
}
function highlightedHtml(value) {
return escapeHtml(value)
.replace(/&lt;mark&gt;/g, '<mark>')
.replace(/&lt;\/mark&gt;/g, '</mark>');
}
function highlightSearchTitle(title, query) {
var cleanTitle = searchText(title);
var cleanQuery = searchText(query);
if (!cleanQuery) return escapeHtml(cleanTitle);
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
if (index < 0) return escapeHtml(cleanTitle);
return escapeHtml(cleanTitle.slice(0, index)) +
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
}
function renderSearchRecentState(overlay) {
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (options instanceof HTMLElement) options.hidden = true;
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (results) {
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
}
}
function scheduleSearchResultsRender() {
window.clearTimeout(searchRenderTimer);
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
}
async function renderSearchResults() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (!(overlay instanceof HTMLElement)) return;
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (!input || !meta || !results) return;
var query = searchText(input.value);
if (!query) {
activeSearchRequestId += 1;
renderSearchRecentState(overlay);
return;
}
if (options instanceof HTMLElement) options.hidden = false;
var requestId = ++activeSearchRequestId;
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
try {
var response = await fetch('/api/search/documents', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
sourceKind: currentSourceKind() || null,
rootUri: currentRootUri() || null,
documentId: currentDocumentId() || null,
query: query,
limit: 30,
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: false,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
}
})
});
var payload = await response.json();
if (requestId !== activeSearchRequestId) return;
var items = Array.isArray(payload.results) ? payload.results : [];
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (!items.length) {
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
return;
}
results.innerHTML = items.map(function(item) {
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
var resourceType = searchText(item.resourceType || item.resourceKind || 'page');
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '" data-resource-type="' + escapeHtml(resourceType) + '">' +
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span>' +
'<span class="wolai-search-result-path"><span>' + escapeHtml(path) + '</span><span class="wolai-search-result-type">' + escapeHtml(resourceType) + '</span></span></span>' +
'</button>';
}).join('');
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
}
}
function isSearchModalOpen() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
return overlay instanceof HTMLElement && !overlay.hidden;
}
function openSearchModal() {
var overlay = ensureSearchModal();
overlay.hidden = false;
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
void renderSearchResults();
if (input) {
setTimeout(function() { input.focus(); input.select(); }, 0);
}
}
function closeSearchModal() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (overlay instanceof HTMLElement) overlay.hidden = true;
document.documentElement.removeAttribute('data-mnote-search-modal-open');
}
function toggleSearchModal() {
if (isSearchModalOpen()) closeSearchModal();
else openSearchModal();
}
function updatePageSettingsTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
}
function updatePageAiTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
}
function createPageOptionRow(key, type) {
var inputType = type || 'checkbox';
var supported = pageOptionIsSupported(key);
if (inputType === 'checkbox') {
return '' +
'<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
'</label>';
}
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
'<option value="compact">紧凑</option>' +
'<option value="normal">默认</option>' +
'<option value="spacious">宽松</option>' +
'</select>' +
'</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">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">页面字体</span>' +
'<span class="wolai-page-setting-hint">已接通:仅对当前页面生效</span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
'<option value="default">默认</option>' +
'<option value="song">宋体</option>' +
'<option value="kai">楷体</option>' +
'</select>' +
'</label>';
}
function ensurePageHistoryDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-history-drawer';
drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
drawer.setAttribute('data-mnote-surface', 'page-history');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-history-panel">' +
'<div class="wolai-page-history-header">' +
'<div><div class="wolai-page-history-title">页面历史</div><div class="wolai-page-history-subtitle">当前会话内最近保存的 15 个快照。</div></div>' +
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
'</div>' +
'<div class="wolai-page-history-list" data-page-history-list></div>' +
'</div>';
drawer.addEventListener('click', function(event) {
if (event.target === drawer) closePageHistoryDrawer();
});
document.body.appendChild(drawer);
return drawer;
}
function renderPageHistoryDrawer() {
var drawer = ensurePageHistoryDrawer();
var list = drawer.querySelector('[data-page-history-list]');
if (!(list instanceof HTMLElement)) return;
ensureHistorySnapshotsSeeded();
list.innerHTML = pageUiState.historySnapshots.length
? pageUiState.historySnapshots.map(function(snapshot) {
var stats = snapshot.stats || {};
var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return '' +
'<div class="wolai-page-history-item">' +
'<div class="wolai-page-history-item-copy">' +
'<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-history-item-meta">字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop">仅查看</button>' +
'</div>';
}).join('')
: '<div class="wolai-page-history-empty">尚未产生历史快照,编辑后会自动生成。</div>';
}
function openPageHistoryDrawer() {
renderPageHistoryDrawer();
var drawer = ensurePageHistoryDrawer();
drawer.hidden = false;
document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
}
function closePageHistoryDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-history-open');
}
function ensurePageShareDialog() {
var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (existing instanceof HTMLElement) return existing;
var dialog = document.createElement('div');
dialog.className = 'wolai-page-share-dialog';
dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
dialog.setAttribute('data-mnote-surface', 'page-share');
dialog.hidden = true;
dialog.innerHTML = '' +
'<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
'<div class="wolai-page-share-header">' +
'<div><div class="wolai-page-share-title">公开分享页面</div><div class="wolai-page-share-subtitle">当前 3000 公开入口由 mnote-web 持有。</div></div>' +
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
'</div>' +
'<div class="wolai-page-share-state">' +
'<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
'<span class="wolai-page-share-copy">任何拥有链接的人都可以访问当前页面。</span>' +
'</div>' +
'<div class="wolai-page-share-url-row">' +
'<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
'<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link">复制链接</button>' +
'</div>' +
'<div class="wolai-page-share-footer">更多共享者、群组公开和权限策略仍待接线。</div>' +
'</div>';
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closePageShareDialog();
});
document.body.appendChild(dialog);
return dialog;
}
function openPageShareDialog() {
var dialog = ensurePageShareDialog();
var input = dialog.querySelector('[data-page-share-url]');
if (input instanceof HTMLInputElement) input.value = window.location.href;
dialog.hidden = false;
document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
}
function closePageShareDialog() {
var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-share-open');
}
function pageAiSuggestions() {
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
return [
'帮我总结《' + title + '》当前内容',
'把当前页面改写得更简洁一些',
'提炼当前页的关键待办和行动项',
'基于当前页内容生成一个三段式摘要'
];
}
function pageAiStorageKey() {
return 'hermes_page_ai_session:' + currentDocumentId();
}
function pageAiTimestamp(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim()) {
var parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
}
return Date.now();
}
function pageAiBackendSessionQuery(extra) {
var params = new URLSearchParams();
params.set('source', 'acp');
params.set('workspaceId', resolveWorkspaceId(document.body));
params.set('documentId', currentDocumentId());
params.set('profile', pageAiRunProfile());
params.set('sourceKind', currentSourceKind());
if (currentRootUri()) params.set('rootUri', currentRootUri());
Object.keys(extra || {}).forEach(function(key) {
var value = extra[key];
if (value !== undefined && value !== null && String(value).trim() !== '') {
params.set(key, String(value));
}
});
return params.toString();
}
function pageAiNewSession(title) {
var now = Date.now();
return {
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
title: title || '新会话',
profile: pageAiCurrentProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
createdAt: now,
updatedAt: now,
source: 'local',
usage: null,
status: 'idle',
messages: []
};
}
function pageAiUsageSummary(usage) {
if (!usage || typeof usage !== 'object') return '';
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
var parts = [];
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
return parts.join(' ') || '';
}
function pageAiPermissionMessage(payload, eventType) {
payload = payload && typeof payload === 'object' ? payload : {};
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
var args = payload.arguments || payload.args || payload.input || payload.params || payload;
var decision = String(payload.decision || payload.result || '').trim();
if (!decision && eventType === 'permission.denied') decision = 'denied';
if (!decision && eventType === 'permission.allowed') decision = 'allowed';
return {
role: 'tool',
kind: 'permission',
permissionId: permissionId,
toolName: toolName,
argsSummary: pageAiPreviewValue(args),
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
resolved: decision === 'denied' || decision === 'allowed',
decision: decision
};
}
function pageAiApplyPermissionEvent(eventName, payloadText) {
var payload = null;
try {
payload = JSON.parse(payloadText || 'null');
} catch (_) {
payload = {};
}
var message = pageAiPermissionMessage(payload, eventName);
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.kind === 'permission' && item.permissionId === message.permissionId;
});
if (existing) {
Object.assign(existing, message);
} else {
pageUiState.pageAiMessages.push(message);
}
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item.permissionId !== message.permissionId;
}).concat([message]).slice(-20);
if (!message.resolved) {
pageAiShowPermissionDialog(message);
} else {
pageAiHidePermissionDialog();
}
}
function pageAiResolvePermission(permissionId, decision) {
permissionId = String(permissionId || '').trim();
if (!permissionId) return;
// 调用后端 resolve-permission 端点,让 ACP agent 得到真实响应
var runId = pageUiState.pageAiCurrentRunId;
if (runId) {
fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ permissionId: permissionId, decision: decision })
}).then(function(response) {
if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status);
}).catch(function(err) {
console.warn('resolve-permission 请求失败', err);
});
} else {
console.warn('resolve-permission: 无活跃 runId,只能本地更新');
}
// 本地乐观更新 UI
pageUiState.pageAiMessages.forEach(function(item) {
if (item.kind === 'permission' && item.permissionId === permissionId) {
item.resolved = true;
item.decision = decision;
item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求';
}
});
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
renderPageAiConversation();
}
function pageAiHidePermissionDialog() {
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
}
function pageAiShowPermissionDialog(message) {
if (!message || message.kind !== 'permission') return;
if (message.resolved) {
pageAiHidePermissionDialog();
return;
}
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (!(dialog instanceof HTMLElement)) {
dialog = document.createElement('div');
dialog.className = 'wolai-page-ai-permission-dialog';
dialog.setAttribute('data-page-ai-permission-dialog', 'true');
dialog.innerHTML = '' +
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
'<div class="wolai-page-ai-tool-meta" data-page-ai-permission-args></div>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-dialog-action>允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-dialog-action>拒绝</button>' +
'</div>' +
'</div>';
document.body.appendChild(dialog);
}
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
var args = dialog.querySelector('[data-page-ai-permission-args]');
if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || '';
dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) {
if (button instanceof HTMLButtonElement) {
button.setAttribute('data-page-ai-permission-id', message.permissionId || '');
button.disabled = Boolean(message.resolved);
}
});
dialog.hidden = false;
}
function pageAiNormalizeSessions(sessions) {
return (Array.isArray(sessions) ? sessions : [])
.slice(0, 20)
.map(function(session) {
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
return {
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '新会话',
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
createdAt: pageAiTimestamp(session && session.createdAt),
updatedAt: pageAiTimestamp(session && session.updatedAt),
source: String(session && session.source || 'local').trim() || 'local',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
runId: String(session && (session.runId || session.run_id) || '').trim(),
status: String(session && session.status || '').trim(),
usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null,
preview: String(session && session.preview || '').trim(),
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : []
};
})
.sort(function(a, b) {
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
});
}
function pageAiNormalizeBackendSessionRow(row) {
if (!row || typeof row !== 'object') return null;
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
var sessionId = String(row.sessionId || row.session_id || '').trim();
if (!sessionId) return null;
var title = String(row.title || payload.title || payload.message || '').trim();
if (title.length > 28) title = title.slice(0, 28) + '…';
var persistence = String(row.persistence || payload.persistence || '').trim();
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
return {
id: sessionId,
title: title || '当前页问答',
profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default',
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
runId: String(row.runId || row.run_id || '').trim(),
status: String(row.status || (row.runtime && row.runtime.status) || '').trim(),
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
preview: String(payload.message || row.snippet || '').trim(),
messages: []
};
}
function pageAiMergeSessions(localSessions, backendSessions) {
var byId = {};
pageAiNormalizeSessions(localSessions).forEach(function(session) {
byId[session.id] = session;
});
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
var existing = byId[session.id];
byId[session.id] = Object.assign({}, existing || {}, session, {
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
});
});
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
}
function pageAiSessionStorageLabel(session) {
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
if (storage === 'local_shared') return '共享会话';
if (storage === 'local_private') return '本地私有';
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '云端会话';
if (persistence === 'local_ai_session_jsonl') return '本地私有';
return String(session && session.source || '').trim() === 'acp' ? '云端会话' : '本地私有';
}
function pageAiLoadSessions() {
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
try {
var raw = window.localStorage.getItem(pageAiStorageKey());
var parsed = raw ? JSON.parse(raw) : null;
var activeId = String(parsed && parsed.activeSessionId || '').trim();
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions);
var storageVersion = Number(parsed && parsed.version || 0);
if (storageVersion >= PAGE_AI_SESSION_STORAGE_VERSION && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
if (activeProfile) pageAiSetActiveProfile(activeProfile);
if (sessions.length) {
pageUiState.pageAiSessions = sessions;
var activeSession = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = activeSession.id;
if (activeSession.profile) pageAiSetActiveProfile(activeSession.profile);
if (!pageUiState.pageAiAcpRuntime && activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(activeSession.messages) ? activeSession.messages.slice() : [];
return;
}
if (activeId) {
pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
pageUiState.pageAiSessions[0].id = activeId;
pageUiState.pageAiActiveSessionId = activeId;
pageUiState.pageAiMessages = [];
return;
}
} catch (_) {}
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
pageUiState.pageAiActiveSessionId = fresh.id;
pageUiState.pageAiMessages = [];
}
async function pageAiLoadBackendSessions() {
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
}
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
if (!backendSessions.length) return [];
pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions);
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
}
var active = pageAiCurrentSession();
if (active) {
if (active.profile) pageAiSetActiveProfile(active.profile);
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
}
pageUiState.pageAiSessionError = '';
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return backendSessions;
}
function pageAiMessageFromRuntimeEvent(event) {
if (!event || typeof event !== 'object') return null;
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
if (eventType === 'message.delta') {
var delta = String(payload.delta || payload.text || payload.output_text || '').trim();
return delta ? { role: 'assistant', content: delta } : null;
}
if (eventType === 'thought.delta') {
var thought = String(payload.delta || payload.text || '').trim();
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
}
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
var rawLocations = payload.locations;
return {
role: 'tool',
content: toolName,
toolName: toolName,
toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''),
toolKind: String(payload.kind || ''),
status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'),
argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input),
resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error),
locations: Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : [],
traceId: String(payload.traceId || payload.trace_id || ''),
auditId: String(payload.auditId || payload.audit_id || '')
};
}
if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') {
return pageAiPermissionMessage(payload, eventType);
}
return null;
}
function pageAiApplyBackendSessionDetail(payload) {
var sessionPayload = payload && payload.session ? payload.session : {};
var runs = pageAiNormalizeArray(sessionPayload.runs);
var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null;
var events = pageAiNormalizeArray(payload && payload.events);
var messages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) {
return {
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
content: String(message.content || '')
};
}).filter(function(message) { return message.content; });
events.forEach(function(event) {
var message = pageAiMessageFromRuntimeEvent(event);
if (message) messages.push(message);
});
var current = pageAiCurrentSession();
if (latest && current) {
Object.assign(current, latest);
}
if (current) {
current.messages = messages.slice(-300);
current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now());
if (latest && latest.usage) current.usage = latest.usage;
}
pageAiApplyRuntimeState(payload && payload.runtime);
pageUiState.pageAiMessages = messages.slice(-300);
pageAiPersistSessions();
}
async function pageAiLoadBackendSessionDetail(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return null;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status));
}
pageAiApplyBackendSessionDetail(payload);
renderPageAiConversation();
renderPageAiControls();
return payload;
}
async function pageAiSearchBackendSessions(query) {
var q = String(query || '').trim();
pageUiState.pageAiSessionSearchQuery = q;
if (!q) {
pageUiState.pageAiSessionSearchResults = [];
renderPageAiConversation();
return [];
}
var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status));
}
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) {
var normalized = pageAiNormalizeBackendSessionRow(row) || {};
normalized.snippet = String(row && row.snippet || normalized.preview || '').trim();
return normalized;
}).filter(function(row) { return row.id; });
renderPageAiConversation();
return pageUiState.pageAiSessionSearchResults;
}
function pageAiPersistSessions() {
document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
try {
pageAiSyncCurrentSessionMessages();
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
version: PAGE_AI_SESSION_STORAGE_VERSION,
activeSessionId: pageUiState.pageAiActiveSessionId,
activeProfileName: pageAiCurrentProfile(),
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
}));
} catch (_) {}
}
async function pageAiEnsureHermesSession(forceCreate) {
pageAiLoadSessions();
var current = pageAiCurrentSession();
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === pageAiCurrentProfile()) return current;
var response = await fetch('/api/hermes/client/sessions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
traceId: 'page-ai-' + Date.now().toString(36),
profile: pageAiCurrentProfile(),
title: current && current.title ? current.title : '当前页问答'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'hermes_session_failed_' + response.status));
}
var session = {
id: String(payload.sessionId || '').trim(),
title: String(payload.title || '当前页问答'),
profile: String(payload.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
persistence: String(payload.persistence || '').trim(),
sessionStorage: String(payload.sessionStorage || '').trim(),
permissionLevel: String(payload.permissionLevel || '').trim(),
shareId: String(payload.shareId || '').trim(),
createdAt: Date.now(),
updatedAt: Date.now(),
messages: pageUiState.pageAiMessages.slice()
};
pageUiState.pageAiSessions = pageAiNormalizeSessions([session]);
pageUiState.pageAiActiveSessionId = session.id;
pageAiPersistSessions();
renderPageAiConversation();
return session;
}
async function pageAiRestoreHermesSession() {
var current = pageAiCurrentSession();
if (!current || !String(current.id || '').startsWith('mnote_')) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
if (!response.ok) return;
var payload = await response.json().catch(function(){ return null; });
if (payload && payload.persistence === 'convex_acp_runtime_store') {
pageAiApplyBackendSessionDetail(payload);
renderPageAiConversation();
renderPageAiControls();
return;
}
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
var messages = session && Array.isArray(session.messages) ? session.messages : [];
pageAiApplyRuntimeState(payload && payload.runtime);
if (session && (session.profile || session.profileName)) {
current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
}
if (!messages.length) {
renderPageAiControls();
return;
}
pageUiState.pageAiMessages = messages.slice(-300).map(function(message) {
return {
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
content: String(message.content || '')
};
});
current.messages = pageUiState.pageAiMessages.slice();
current.updatedAt = Date.now();
renderPageAiConversation();
renderPageAiControls();
}
function pageAiCurrentSession() {
return pageUiState.pageAiSessions.find(function(session) {
return session.id === pageUiState.pageAiActiveSessionId;
}) || null;
}
function pageAiSyncCurrentSessionMessages() {
var session = pageAiCurrentSession();
if (!session) return;
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
session.profile = pageAiCurrentProfile();
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
session.updatedAt = Date.now();
}
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();
renderPageAiControls();
renderPageAiConversation();
if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) {
void pageAiLoadBackendSessionDetail(session.id).catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}
}
function pageAiStartNewSession() {
var session = pageAiNewSession();
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
pageUiState.pageAiActiveSessionId = session.id;
pageUiState.pageAiMessages = [];
pageUiState.pageAiPage = 'chat';
pageAiPersistSessions();
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiRenameBackendSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
var title = window.prompt('重命名 AI 会话', session.title || '当前页问答');
if (title === null) return;
title = String(title || '').trim();
if (!title) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
body: JSON.stringify({ title: title })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status));
}
session.title = String((payload.result && payload.result.title) || title);
session.updatedAt = Date.now();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
async function pageAiDeleteBackendSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
if (!window.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({}), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
}
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; });
if (pageUiState.pageAiActiveSessionId === sessionId) {
var next = pageUiState.pageAiSessions[0] || pageAiNewSession();
if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next];
pageUiState.pageAiActiveSessionId = next.id;
pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : [];
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
async function pageAiResumeBackendSession(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
pageAiSetActiveSession(sessionId);
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status));
}
pageAiApplyBackendSessionDetail(payload);
pageUiState.pageAiPage = 'chat';
renderPageAiConversation();
renderPageAiControls();
}
function pageAiProviderLabel(provider) {
if (provider === 'codex') return 'Codex';
if (provider === 'claudecode') return 'ClaudeCode';
return 'Hermes';
}
function pageAiNormalizeArray(value) {
return Array.isArray(value) ? value : [];
}
function pageAiDefaultAcpRuntimes() {
return [
{
name: 'reasonix',
title: 'ACP · Reasonix',
description: '通过 ACP 协议直连 ReasonixDeepSeek 缓存优先)',
model: 'deepseek-chat',
preset: 'auto'
},
{
name: 'hermes',
title: 'ACP · Hermes',
description: '通过 ACP 协议直连 Hermes agent runtime'
}
];
}
function pageAiNormalizeAcpRuntimes(runtimes) {
var byName = {};
pageAiDefaultAcpRuntimes().forEach(function(runtime) {
byName[runtime.name] = Object.assign({}, runtime);
});
pageAiNormalizeArray(runtimes).forEach(function(runtime) {
var name = String(runtime && runtime.name || '').trim();
if (name !== 'reasonix' && name !== 'hermes') return;
byName[name] = Object.assign({}, byName[name] || {}, runtime, { name: name });
});
return ['reasonix', 'hermes'].map(function(name) { return byName[name]; }).filter(Boolean);
}
function pageAiUnwrapUpstream(payload) {
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
return payload || null;
}
function pageAiProfileValue(profile) {
if (profile && typeof profile === 'object') {
return String(profile.name || profile.profile || profile.id || '').trim();
}
return String(profile || '').trim();
}
function pageAiCurrentProfile() {
var active = String(pageUiState.pageAiActiveProfileName || '').trim();
if (active) return active;
var selected = pageUiState.pageAiProfiles.find(function(profile) {
return profile && profile.active;
});
return pageAiProfileValue(selected) || 'mnoteai';
}
function pageAiRunProfile() {
return String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
}
function pageAiMnoteToolModel() {
return String(document.documentElement.getAttribute('data-mnote-page-ai-tool-model') || 'deepseek-v4-flash').trim() || 'deepseek-v4-flash';
}
function pageAiCurrentProfileRecord() {
var active = pageAiCurrentProfile();
return pageUiState.pageAiProfiles.find(function(profile) {
return pageAiProfileValue(profile) === active;
}) || null;
}
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
var toolModel = pageAiMnoteToolModel();
if (!profile) return 'tool: ' + toolModel;
var model = String(profile.model || '').trim();
var gateway = String(profile.gateway || '').trim();
var profileLabel = [model, gateway].filter(Boolean).join(' · ') || '由 Hermes 决定';
return 'tool: ' + toolModel + ' · profile: ' + profileLabel;
}
function pageAiNormalizeProfiles(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
return profiles.map(function(profile) {
return {
name: pageAiProfileValue(profile) || 'default',
active: Boolean(profile && profile.active),
model: String(profile && profile.model || '').trim(),
gateway: String(profile && profile.gateway || '').trim(),
alias: String(profile && profile.alias || '').trim()
};
});
}
function pageAiNormalizeSkills(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var categories = pageAiNormalizeArray(upstream && upstream.categories ? upstream.categories : []);
var archived = pageAiNormalizeArray(upstream && upstream.archived ? upstream.archived : []);
return {
categories: categories.map(function(category) {
return {
name: String(category && category.name || '').trim() || 'misc',
description: String(category && category.description || '').trim(),
skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}),
archived: archived.map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}
function pageAiSkillListEntries() {
var result = [];
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
result.push({
category: category.name,
name: skill.name,
description: skill.description,
enabled: skill.enabled !== false,
source: skill.source || 'local',
origin: skill.origin || '',
createdBy: skill.createdBy || '',
patchCount: Number(skill.patchCount || 0),
modified: Boolean(skill.modified)
});
});
});
return result.concat(pageAiNormalizeArray(pageUiState.pageAiSkills.archived));
}
function pageAiSetActiveProfile(profileName) {
var next = String(profileName || '').trim() || 'mnoteai';
pageUiState.pageAiActiveProfileName = next;
document.documentElement.setAttribute('data-mnote-page-ai-profile', next);
document.documentElement.setAttribute('data-mnote-page-ai-tool-model', pageAiMnoteToolModel());
}
function pageAiSetRunStatus(status, runId) {
pageUiState.pageAiRunStatus = status || 'idle';
pageUiState.pageAiCurrentRunId = runId || pageUiState.pageAiCurrentRunId || '';
document.documentElement.setAttribute('data-mnote-page-ai-run-status', pageUiState.pageAiRunStatus);
if (pageUiState.pageAiCurrentRunId) {
document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
}
}
function pageAiApplyRuntimeState(runtime) {
if (!runtime || typeof runtime !== 'object') return;
var status = String(runtime.status || '').trim();
var runId = String(runtime.runId || runtime.run_id || '').trim();
if (status) pageAiSetRunStatus(status, runId);
var queueLength = Number(runtime.queueLength || runtime.queue_length || 0);
if (Number.isFinite(queueLength)) pageUiState.pageAiQueueLength = Math.max(0, queueLength);
var toolName = String(runtime.lastToolName || runtime.last_tool_name || '').trim();
if (toolName) {
pageUiState.pageAiLastToolCall = {
event: String(runtime.lastEvent || runtime.last_event || ''),
name: toolName,
runId: runId,
traceId: String(runtime.traceId || runtime.trace_id || ''),
auditId: String(runtime.lastAuditId || runtime.last_audit_id || '')
};
}
}
function pageAiApplyQueuedRun(payload) {
if (!payload || payload.queued !== true) return false;
var queueId = String(payload.queueId || payload.queue_id || '').trim();
var queueLength = Number(payload.queueLength || payload.queue_length || 0);
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : 1;
if (queueId) {
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(item) {
return item.queueId !== queueId;
}).concat([{
queueId: queueId,
sessionId: String(payload.sessionId || payload.session_id || pageUiState.pageAiActiveSessionId || ''),
traceId: String(payload.traceId || payload.trace_id || ''),
queuedAt: Number(payload.queuedAt || payload.queued_at || Date.now())
}]);
}
pageAiSetRunStatus('queued', pageUiState.pageAiCurrentRunId);
return true;
}
function pageAiPreviewValue(value) {
if (value == null || value === '') return '';
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
try {
var text = JSON.stringify(value);
return text.length > 120 ? text.slice(0, 120) + '…' : text;
} catch (_) {
return String(value);
}
}
function pageAiNormalizeToolName(name) {
return String(name || '').trim().replace(/_/g, '.');
}
function pageAiFormatChangedFiles(files) {
return pageAiNormalizeArray(files).map(function(file) {
var path = String(file && file.path || '').trim();
var changeType = String(file && file.changeType || file.change_type || 'modified').trim();
var summary = String(file && file.summary || '').trim();
return [changeType, path, summary].filter(Boolean).join(' · ');
}).filter(Boolean).join('\n');
}
function pageAiToolEventDeepFindString(value, keys, depth) {
if (!value || typeof value !== 'object' || depth > 5) return '';
for (var index = 0; index < keys.length; index += 1) {
var key = keys[index];
if (Object.prototype.hasOwnProperty.call(value, key) && typeof value[key] === 'string' && value[key].trim()) {
return value[key].trim();
}
}
if (Array.isArray(value)) {
for (var arrayIndex = 0; arrayIndex < value.length; arrayIndex += 1) {
var fromArray = pageAiToolEventDeepFindString(value[arrayIndex], keys, depth + 1);
if (fromArray) return fromArray;
}
return '';
}
var preferred = ['audit', 'args', 'arguments', 'input', 'result', 'summary', 'output', 'upstream'];
for (var prefIndex = 0; prefIndex < preferred.length; prefIndex += 1) {
var child = value[preferred[prefIndex]];
var fromPreferred = pageAiToolEventDeepFindString(child, keys, depth + 1);
if (fromPreferred) return fromPreferred;
}
var objectKeys = Object.keys(value);
for (var objectIndex = 0; objectIndex < objectKeys.length; objectIndex += 1) {
var fromObject = pageAiToolEventDeepFindString(value[objectKeys[objectIndex]], keys, depth + 1);
if (fromObject) return fromObject;
}
return '';
}
function pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId) {
var normalizedTool = pageAiNormalizeToolName(toolName);
var writesCurrentPage = [
'mnote.page.save',
'mnote.page.update.title',
'mnote.page.update.options',
'mnote.doc.apply.block.ops',
'mnote.block.replace',
'mnote.block.insert.after',
'mnote.block.delete',
'mnote.block.move.after'
].indexOf(normalizedTool) >= 0 || [
'mnote.page.update_title',
'mnote.page.update_options',
'mnote.doc.apply_block_ops',
'mnote.block.replace',
'mnote.block.insert_after',
'mnote.block.delete',
'mnote.block.move_after'
].indexOf(String(toolName || '').trim()) >= 0;
if (!writesCurrentPage) return;
var documentId = pageAiToolEventDeepFindString(toolEvent, ['documentId', 'document_id'], 0) || currentDocumentId();
var workspaceId = pageAiToolEventDeepFindString(toolEvent, ['workspaceId', 'workspace_id'], 0) || resolveWorkspaceId(document.body);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: toolName,
normalizedToolName: normalizedTool,
documentId: documentId,
workspaceId: workspaceId,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId || ''),
traceId: String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || ''),
toolCallId: String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || '')
}
}));
} catch (error) {
console.warn('mnote 页面 AI 写入刷新事件派发失败', error);
}
}
function pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId) {
var toolEvent = null;
try {
toolEvent = JSON.parse(payloadText || 'null');
} catch (_) {
toolEvent = {};
}
var rawToolName = toolEvent && (toolEvent.name || toolEvent.tool || toolEvent.toolName || '');
var toolName = String(rawToolName || eventName);
var toolCallId = String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || (runId + ':' + toolName));
var eventStatus = String(toolEvent && toolEvent.status || '').trim();
var status = eventName === 'tool.completed'
? (toolEvent && toolEvent.error ? 'failed' : 'completed')
: (eventName === 'tool.failed' ? 'failed' : (eventStatus || 'running'));
if (status === 'in_progress' || status === 'pending') status = 'running';
var traceId = String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || '');
var auditId = String(toolEvent && (toolEvent.audit_id || toolEvent.auditId) || '');
var argsSummary = pageAiPreviewValue(toolEvent && (toolEvent.arguments || toolEvent.args || toolEvent.input));
var resultSource = toolEvent && (toolEvent.summary || toolEvent.result || toolEvent.output);
if (!resultSource && status === 'failed') {
resultSource = [toolEvent && toolEvent.code, toolEvent && toolEvent.error].filter(Boolean).join(' ');
}
var resultSummary = pageAiPreviewValue(resultSource);
pageUiState.pageAiLastToolCall = {
event: eventName,
name: toolName,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId),
traceId: traceId,
auditId: auditId
};
var rawLocations = toolEvent && toolEvent.locations;
var locations = Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : [];
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'tool' && item.toolCallId === toolCallId;
});
if (!rawToolName && existing && existing.toolName) toolName = existing.toolName;
if (!existing) {
existing = {
role: 'tool',
content: toolName,
toolCallId: toolCallId,
toolName: toolName,
toolKind: String(toolEvent && toolEvent.kind || ''),
status: status,
argsSummary: '',
resultSummary: '',
locations: locations,
traceId: traceId,
auditId: auditId
};
pageUiState.pageAiMessages.push(existing);
}
existing.content = toolName;
existing.toolName = toolName;
existing.toolKind = String(toolEvent && toolEvent.kind || existing.toolKind || '');
existing.status = status;
existing.traceId = traceId || existing.traceId || '';
existing.auditId = auditId || existing.auditId || '';
if (argsSummary) existing.argsSummary = argsSummary;
if (resultSummary) existing.resultSummary = resultSummary;
if (locations.length) existing.locations = locations;
if (status === 'completed') {
pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId);
}
}
async function pageAiCancelQueuedRun(queueId) {
queueId = String(queueId || '').trim();
if (!queueId) return;
var item = pageUiState.pageAiQueuedItems.find(function(entry) {
return entry.queueId === queueId;
});
var sessionId = String(item && item.sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
try {
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/queue/' + encodeURIComponent(queueId), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'queue_cancel_failed_' + response.status));
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(entry) {
return entry.queueId !== queueId;
});
var queueLength = Number(payload && (payload.queueLength || payload.queue_length || 0));
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : pageUiState.pageAiQueuedItems.length;
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已取消一条 Hermes 队列项。' });
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '取消 Hermes 队列项失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiConversation();
renderPageAiControls();
}
function pageAiSetContextScope(scope) {
var next = String(scope || '').trim() || 'page';
pageUiState.pageAiContextScope = next;
document.documentElement.setAttribute('data-mnote-page-ai-context-scope', next);
}
function pageAiRunStatusLabel(status) {
if (status === 'queued') return '排队中';
if (status === 'running') return '运行中';
if (status === 'tool_calling') return '调用工具';
if (status === 'completed') return '已完成';
if (status === 'failed') return '失败';
if (status === 'aborted') return '已停止';
return '空闲';
}
function pageAiMemoryFileLabel(section) {
if (section === 'soul') return 'SOUL.md';
if (section === 'user') return 'USER.md';
return 'MEMORY.md';
}
function pageAiContextScopeLabel(scope) {
if (scope === 'selection') return '当前选区';
if (scope === 'block') return '当前块';
if (scope === 'options') return '页面设置';
return '当前页';
}
function pageAiHermesSettingsUrl() {
var configured = String(window.__mnoteHermesSettingsUrl || '').trim();
return configured || '';
}
function pageAiOpenHermesSettings() {
var url = pageAiHermesSettingsUrl();
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
pageUiState.pageAiProfileError = '未配置 Hermes 设置入口:请设置 MNOTE_WEB_HERMES_UPSTREAM_URL。';
renderPageAiControls();
}
function pageAiNormalizeTools(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
var tools = pageAiNormalizeArray(upstream.tools || upstream);
return tools.map(function(tool) {
var name = String(tool && (tool.name || tool.toolName || tool.tool) || '').trim();
if (!name) return null;
return {
name: name,
description: String(tool && tool.description || '').trim(),
scope: String(tool && (tool.scope || tool.requiredScope || '') || '').trim(),
kind: String(tool && (tool.kind || tool.mode || '') || '').trim(),
status: String(tool && (tool.status || tool.permission || 'available') || '').trim(),
enabled: tool && tool.enabled !== false,
unavailableReason: String(tool && (tool.unavailableReason || tool.reason || '') || '').trim()
};
}).filter(Boolean);
}
function pageAiErrorMessage(payload, fallback) {
if (!payload || typeof payload !== 'object') return fallback;
return String(payload.message || payload.error || payload.code || fallback || '').trim() || fallback;
}
function pageAiNormalizeGatewayHealth(payload) {
var upstream = pageAiUnwrapUpstream(payload) || payload || {};
return {
ok: Boolean(upstream.ok),
profile: upstream.profile || null,
gateway: upstream.gateway || null,
suggestions: pageAiNormalizeArray(upstream.suggestions).map(function(item) {
return String(item || '').trim();
}).filter(Boolean)
};
}
function pageAiSetDraftForSection(section, value) {
pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
}
function pageAiApplyProfileMemory(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
pageUiState.pageAiProfileMemory = {
memory: String(upstream.memory || ''),
user: String(upstream.user || ''),
soul: String(upstream.soul || '')
};
pageUiState.pageAiProfileMemoryDrafts = {
memory: pageUiState.pageAiProfileMemory.memory,
user: pageUiState.pageAiProfileMemory.user,
soul: pageUiState.pageAiProfileMemory.soul
};
}
async function pageAiLoadTools() {
try {
var response = await fetch('/api/hermes/client/tools?scope=mnote&profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tools_failed_' + response.status));
pageUiState.pageAiTools = pageAiNormalizeTools(payload);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiLoadGatewayHealth() {
try {
var response = await fetch('/api/hermes/client/gateway/health?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'gateway_health_failed_' + response.status));
pageUiState.pageAiGatewayHealth = pageAiNormalizeGatewayHealth(payload);
pageUiState.pageAiGatewayHealthError = '';
} catch (error) {
pageUiState.pageAiGatewayHealthError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiStopRun() {
var runId = String(pageUiState.pageAiCurrentRunId || '').trim();
if (!runId || pageUiState.pageAiRunStatus === 'idle' || pageUiState.pageAiRunStatus === 'completed') return;
pageUiState.pageAiStoppedRunIds[runId] = true;
try {
var response = await fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/abort', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
reason: 'page_ai_user_stop'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'run_abort_failed_' + response.status));
pageAiApplyRuntimeState(payload && payload.runtime);
pageAiSetRunStatus('aborted', runId);
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已请求 Hermes 停止当前 run。' });
} catch (error) {
pageAiSetRunStatus('failed', runId);
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '停止 Hermes run 失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiControls();
renderPageAiConversation();
}
function pageAiFilteredSkillEntries() {
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
return pageAiSkillListEntries().filter(function(skill) {
if (!query) return true;
return String(skill.name || '').toLowerCase().indexOf(query) >= 0
|| String(skill.description || '').toLowerCase().indexOf(query) >= 0
|| String(skill.category || '').toLowerCase().indexOf(query) >= 0
|| pageAiSkillOriginLabel(skill).toLowerCase().indexOf(query) >= 0;
});
}
function pageAiSkillOriginLabel(skill) {
var origin = String(skill && skill.origin || '').trim();
if (origin === 'generated' || String(skill && skill.createdBy || '') === 'agent') return '生成';
if (origin === 'installed') return '安装';
if (origin === 'builtin') return '内置';
if (origin === 'copied') return '本地';
var source = String(skill && skill.source || '').trim();
if (source === 'hub') return '安装';
if (source === 'builtin') return '内置';
if (source === 'reasonix') {
if (origin === 'project') return 'Reasonix 项目';
if (origin === 'global') return 'Reasonix 全局';
return 'Reasonix';
}
return '本地';
}
async function pageAiLoadProfiles() {
try {
var response = await fetch('/api/hermes/client/profiles', {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profiles_failed_' + response.status));
var profiles = pageAiNormalizeProfiles(payload);
pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'mnoteai', active: true }];
var acpRuntimes = Array.isArray(payload.acpRuntimes) ? payload.acpRuntimes : [];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(acpRuntimes);
var current = pageAiCurrentProfile();
var hasCurrent = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === current; });
var hasMnoteAi = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === 'mnoteai'; });
var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
pageAiSetActiveProfile(hasCurrent && current !== 'default' ? current : (hasMnoteAi ? 'mnoteai' : (active || current)));
pageUiState.pageAiProfileError = '';
void pageAiLoadGatewayHealth();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'mnoteai', active: true }];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(pageUiState.pageAiAcpRuntimes);
pageAiSetActiveProfile(pageAiCurrentProfile());
}
renderPageAiProviderButtons();
renderPageAiControls();
}
async function pageAiSwitchProfile(profileName) {
var next = String(profileName || '').trim();
if (!next) return;
pageAiSetActiveProfile(next);
renderPageAiProviderButtons();
renderPageAiControls();
try {
var response = await fetch('/api/hermes/client/profiles/active', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: next })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_switch_failed_' + response.status));
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(session) {
return session && session.profile === next;
});
pageUiState.pageAiActiveSessionId = '';
pageUiState.pageAiMessages = [];
pageAiPersistSessions();
await pageAiEnsureHermesSession(true);
await pageAiLoadProfileMemory();
await pageAiLoadSkills();
await pageAiLoadTools();
await pageAiLoadGatewayHealth();
renderPageAiControls();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
}
renderPageAiConversation();
}
async function pageAiLoadProfileMemory() {
try {
var response = await fetch('/api/hermes/client/profile-memory?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_failed_' + response.status));
pageAiApplyProfileMemory(payload);
pageUiState.pageAiProfileMemoryError = '';
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiSaveProfileMemory(section) {
var normalized = String(section || '').trim();
if (['memory', 'user', 'soul'].indexOf(normalized) < 0) return;
var content = String(pageUiState.pageAiProfileMemoryDrafts[normalized] || '');
try {
var response = await fetch('/api/hermes/client/profile-memory', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
section: normalized,
content: content
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_save_failed_' + response.status));
pageUiState.pageAiProfileMemory[normalized] = content;
pageUiState.pageAiProfileMemoryError = '';
document.documentElement.setAttribute('data-mnote-page-ai-memory-saved', normalized);
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiLoadSkills() {
try {
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
var params = runtime === 'reasonix'
? 'runtime=reasonix'
: 'profile=' + encodeURIComponent(pageAiCurrentProfile());
var response = await fetch('/api/hermes/client/skills?' + params, {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skills_failed_' + response.status));
pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
pageUiState.pageAiSkills = { categories: [], archived: [] };
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleSkill(skillName, enabled) {
var name = String(skillName || '').trim();
if (!name) return;
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return;
var previous = null;
pageAiSkillListEntries().forEach(function(skill) {
if (skill.name === name && previous == null) previous = skill.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/skills/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skill_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = Boolean(enabled);
});
});
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = previous;
});
});
}
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleTool(toolName, enabled) {
var name = String(toolName || '').trim();
if (!name) return;
var previous = null;
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name && previous == null) previous = tool.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/tools/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tool_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) {
tool.enabled = Boolean(enabled);
tool.status = Boolean(enabled) ? 'available' : 'disabled';
tool.unavailableReason = Boolean(enabled) ? '' : '当前 Hermes profile 已关闭该 mnote tool';
}
});
document.documentElement.setAttribute('data-mnote-page-ai-tool-toggled', name);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) tool.enabled = previous;
});
}
}
renderPageAiControls();
}
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
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 providerNode = drawer.querySelector('.wolai-page-ai-subtitle span:first-child');
if (providerNode instanceof HTMLElement) {
providerNode.textContent = isAcp ? 'ACP · ' + (pageUiState.pageAiAcpRuntime === 'reasonix' ? 'Reasonix' : 'Hermes') : pageAiProviderLabel(pageUiState.pageAiProvider);
}
}
function renderPageAiControls() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeProfile = pageAiRunProfile();
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
// Populate ACP runtime dropdown
var acpSelect = drawer.querySelector('[data-page-ai-acp-runtime]');
if (acpSelect instanceof HTMLSelectElement) {
var runtimes = pageUiState.pageAiAcpRuntimes.length ? pageUiState.pageAiAcpRuntimes : pageAiNormalizeAcpRuntimes([]);
acpSelect.innerHTML = runtimes.map(function(rt) {
return '<option value="' + escapeHtml(rt.name || '') + '"' + ((rt.name || '') === pageUiState.pageAiAcpRuntime ? ' selected' : '') + '>' + escapeHtml(rt.title || rt.name) + '</option>';
}).join('');
acpSelect.value = pageUiState.pageAiAcpRuntime || 'reasonix';
}
// Show/hide Hermes-specific profile select
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
if (profileLabel instanceof HTMLElement) {
profileLabel.style.display = pageUiState.pageAiAcpRuntime === 'reasonix' ? 'none' : '';
}
// When ACP is selected, populate agent panel with ACP runtime info
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement) {
if (pageUiState.pageAiAcpRuntime === 'reasonix') {
var rt = pageUiState.pageAiAcpRuntimes.find(function(r) { return r.name === pageUiState.pageAiAcpRuntime; }) || {};
var keyStatus = rt.apiKeyConfigured ? '已配置' : '未检测到 DEEPSEEK_API_KEY';
agentPanel.innerHTML = '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">' + escapeHtml(rt.title || 'ACP Runtime') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">模型</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.model || 'deepseek-chat') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Preset</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.preset || 'auto') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">API Key</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(keyStatus) + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">描述</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.description || '') + '</div></div>' +
'</section>';
}
}
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
if (profileSelect instanceof HTMLSelectElement) {
var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
profileSelect.innerHTML = profiles.map(function(profile) {
var name = pageAiProfileValue(profile) || 'default';
var model = [profile.model, profile.gateway].filter(Boolean).join(' / ');
var label = [name, profile.alias, model].filter(Boolean).join(' · ');
return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('');
profileSelect.value = activeProfile;
}
var runStatus = drawer.querySelector('[data-page-ai-run-status]');
if (runStatus instanceof HTMLElement) {
var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : '';
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
}
var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]');
if (stopButton instanceof HTMLButtonElement) {
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
stopButton.disabled = !canStop;
stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true');
}
var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]');
if (settingsLink instanceof HTMLButtonElement) {
settingsLink.disabled = !pageAiHermesSettingsUrl();
settingsLink.title = pageAiHermesSettingsUrl() ? '打开 Hermes 设置' : '未配置 MNOTE_WEB_HERMES_UPSTREAM_URL';
}
var queueList = drawer.querySelector('[data-page-ai-queue-list]');
if (queueList instanceof HTMLElement) {
if (!pageUiState.pageAiQueuedItems.length) {
queueList.innerHTML = '<div class="wolai-page-ai-empty">暂无排队项。</div>';
} else {
queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) {
var label = '队列 ' + (index + 1);
return '' +
'<div class="wolai-page-ai-tool-row" data-page-ai-queue-item="' + escapeHtml(item.queueId) + '">' +
'<div><strong>' + escapeHtml(label) + '</strong><br /><span>' + escapeHtml(item.queueId) + '</span></div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="cancel-queued-run" data-page-ai-queue-id="' + escapeHtml(item.queueId) + '">取消</button>' +
'</div>';
}).join('');
}
}
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
if (sessionNode instanceof HTMLElement) {
var session = pageAiCurrentSession();
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
sessionNode.textContent = session && session.id
? [String(session.title || '当前页问答'), pageAiSessionStorageLabel(session), usageText].filter(Boolean).join(' · ')
: '等待 Hermes session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
var target = button.getAttribute('data-page-ai-tab') || 'chat';
var active = target === pageUiState.pageAiPage;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
if (panel instanceof HTMLElement) {
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
}
});
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
if (profileError instanceof HTMLElement) {
profileError.textContent = pageUiState.pageAiProfileError || '';
profileError.hidden = !pageUiState.pageAiProfileError;
}
var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
if (memoryError instanceof HTMLElement) {
var memoryErrorText = pageUiState.pageAiProfileMemoryError || '';
if (memoryErrorText && memoryErrorText === pageUiState.pageAiProfileError) memoryErrorText = '';
memoryError.textContent = memoryErrorText;
memoryError.hidden = !memoryErrorText;
}
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement && pageUiState.pageAiAcpRuntime !== 'reasonix') {
agentPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
var label = pageAiMemoryFileLabel(section);
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
return '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head">' +
'<div>' +
'<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
'</div>' +
'<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
'</section>';
}).join('');
}
var skillError = drawer.querySelector('[data-page-ai-skill-error]');
if (skillError instanceof HTMLElement) {
skillError.textContent = pageUiState.pageAiSkillError || '';
skillError.hidden = !pageUiState.pageAiSkillError;
}
var sessionError = drawer.querySelector('[data-page-ai-session-error]');
if (sessionError instanceof HTMLElement) {
sessionError.textContent = pageUiState.pageAiSessionError || '';
sessionError.hidden = !pageUiState.pageAiSessionError;
}
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) {
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
}
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
skillSearch.value = pageUiState.pageAiSkillQuery;
}
var skillList = drawer.querySelector('[data-page-ai-skill-list]');
if (skillList instanceof HTMLElement) {
var skills = pageAiFilteredSkillEntries();
if (!skills.length) {
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
? '没有匹配的 Reasonix skill。'
: '没有匹配的 Hermes skill。';
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
} else {
skillList.innerHTML = skills.map(function(skill) {
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
var description = String(skill.description || '').trim();
var hasDescription = description && description !== '---' && description !== '无描述';
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
return '' +
'<div class="wolai-page-ai-skill-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-skill-main">' +
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
'</div>' +
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '"' + (canToggle ? '' : ' disabled title="Reasonix skills 当前为只读展示"') + '>' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var toolsList = drawer.querySelector('[data-page-ai-tool-list]');
if (toolsList instanceof HTMLElement) {
var tools = pageAiNormalizeArray(pageUiState.pageAiTools);
if (!tools.length) {
toolsList.innerHTML = '<div class="wolai-page-ai-empty">尚未读取到 mnote tool manifest。</div>';
} else {
toolsList.innerHTML = tools.map(function(tool) {
return '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(tool.name) + '</div>' +
'<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '</div>' +
(tool.unavailableReason ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.unavailableReason) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (tool.enabled !== false ? ' is-on' : '') + '" data-page-ai-tool-toggle="' + escapeHtml(tool.name) + '" aria-pressed="' + (tool.enabled !== false ? 'true' : 'false') + '">' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var gatewayError = drawer.querySelector('[data-page-ai-gateway-error]');
if (gatewayError instanceof HTMLElement) {
gatewayError.textContent = pageUiState.pageAiGatewayHealthError || '';
gatewayError.hidden = !pageUiState.pageAiGatewayHealthError;
}
var gatewayStatusNode = drawer.querySelector('[data-page-ai-gateway-status]');
var gatewayDetail = drawer.querySelector('[data-page-ai-gateway-detail]');
var gatewayHealth = pageUiState.pageAiGatewayHealth;
if (gatewayStatusNode instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayStatusNode.textContent = '未检查';
} else {
var gateway = gatewayHealth.gateway || {};
var profile = gatewayHealth.profile || {};
gatewayStatusNode.textContent = gatewayHealth.ok ? '可用' : '需要设置';
if (gateway.status) gatewayStatusNode.textContent += ' · ' + gateway.status;
if (profile.name) gatewayStatusNode.textContent += ' · ' + profile.name;
}
}
if (gatewayDetail instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开 Runtime 后会检查 Hermes gateway 与当前 profile。</div>';
} else {
var gatewayInfo = gatewayHealth.gateway || {};
var profileInfo = gatewayHealth.profile || {};
var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join('');
gatewayDetail.innerHTML = '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(profileInfo.name || activeProfile) + '</div>' +
'<div class="wolai-page-ai-tool-meta">model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '</div>' +
'<div class="wolai-page-ai-tool-meta">provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '</div>' +
'</div>' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '</div>' +
'<div class="wolai-page-ai-tool-meta">gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '</div>' +
(suggestionText ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(suggestionText) + '</div>' : '') +
'</div>';
}
}
var toolError = drawer.querySelector('[data-page-ai-tool-error]');
if (toolError instanceof HTMLElement) {
toolError.textContent = pageUiState.pageAiToolsError || '';
toolError.hidden = !pageUiState.pageAiToolsError;
}
var lastTool = drawer.querySelector('[data-page-ai-last-tool]');
if (lastTool instanceof HTMLElement) {
var call = pageUiState.pageAiLastToolCall;
lastTool.textContent = call
? [call.event, call.name, call.runId, call.traceId || call.auditId].filter(Boolean).join(' · ')
: '暂无 tool call';
}
}
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) + '”。当前已通过 Hermes 调用 mnote plugin 工具,目标页面是 ' + (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;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-ai-drawer';
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
drawer.setAttribute('data-mnote-surface', 'page-ai');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
'<div class="wolai-page-ai-subtitle">' +
'<span>Hermes</span>' +
'<span data-page-ai-profile-summary>default</span>' +
'<span data-page-ai-model-status>由 Hermes 决定</span>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-header-actions">' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置">⚙</button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-body">' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
'<div class="wolai-page-ai-chat-meta">' +
'<button type="button" class="wolai-page-ai-session-button" data-page-ai-action="history">' +
'<span data-page-ai-session-status>等待 Hermes session</span>' +
'</button>' +
'<span data-page-ai-context-scope-label>当前页</span>' +
'<span data-page-ai-run-status>空闲</span>' +
'</div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'<div class="wolai-page-ai-suggestions">' +
'<div class="wolai-page-ai-suggestions-header">' +
'<span>推荐问题</span>' +
'<div class="wolai-page-ai-intents">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="agent" role="tab" aria-selected="true">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
'<label class="wolai-page-ai-profile-select">' +
'<span>ACP</span>' +
'<select data-page-ai-acp-runtime></select>' +
'</label>' +
'<label class="wolai-page-ai-profile-select" data-page-ai-hermes-profile>' +
'<span>agent / profile</span>' +
'<select data-page-ai-profile-select></select>' +
'</label>' +
'<label class="wolai-page-ai-context-select">' +
'<span>上下文</span>' +
'<select data-page-ai-context-scope>' +
'<option value="page">当前页</option>' +
'<option value="selection">当前选区</option>' +
'<option value="block">当前块</option>' +
'<option value="options">页面设置</option>' +
'</select>' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="skills" role="tab" aria-selected="true">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-skills-toolbar">' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索技能</span>' +
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="runtime" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="runtime" role="tab" aria-selected="true">Runtime</button>' +
'</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">打开 Hermes 设置</button>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-gateway-error hidden></div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>gateway</span>' +
'<span data-page-ai-gateway-status>未检查</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-gateway-detail></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>queue</span>' +
'<span data-page-ai-queue-status>由 mnote-web 管理</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-queue-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>mnote tools</span>' +
'<span data-page-ai-last-tool>暂无 tool call</span>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-tool-error hidden></div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-tool-list></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'</div>' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索会话</span>' +
'<input type="search" data-page-ai-session-search placeholder="搜索后端 AI 会话…" />' +
'</label>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-session-error hidden></div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'</section>' +
'</div>' +
'<div class="wolai-page-ai-footer">' +
'<div class="wolai-page-ai-composer">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问 Hermes…"></textarea>' +
'<div class="wolai-page-ai-composer-bar">' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话"></button>' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="history" title="历史会话">⌕</button>' +
'<span class="wolai-page-ai-composer-spacer"></span>' +
'<button type="button" class="wolai-page-ai-stop" data-page-ai-action="stop-run" disabled>停止</button>' +
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">发送</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(drawer);
return drawer;
}
function renderPageAiSuggestions() {
var drawer = ensurePageAiDrawer();
var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
if (!(list instanceof HTMLElement)) return;
var container = list.closest('.wolai-page-ai-suggestions');
if (container instanceof HTMLElement) {
container.hidden = pageUiState.pageAiPage !== 'chat' || pageUiState.pageAiMessages.length > 0;
}
var suggestions = pageAiSuggestions();
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
list.innerHTML = ordered.map(function(text) {
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
}).join('');
}
function renderPageAiConversation() {
var drawer = ensurePageAiDrawer();
renderPageAiSuggestions();
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
if (!(conversation instanceof HTMLElement)) return;
if (pageUiState.pageAiPage === 'history') {
var historyRows = pageUiState.pageAiSessionSearchResults.length
? pageUiState.pageAiSessionSearchResults
: pageUiState.pageAiSessions;
if (!historyRows.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
return;
}
conversation.innerHTML = historyRows.map(function(session) {
var preview = Array.isArray(session.messages) && session.messages.length
? session.messages.slice(-1)[0].content
: (session.snippet || session.preview || '暂无消息');
var active = session.id === pageUiState.pageAiActiveSessionId;
var usage = pageAiUsageSummary(session.usage);
var meta = [pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '">' +
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
'<span>' + escapeHtml(preview) + '</span>' +
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
'</button>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
'</div>' +
'</div>';
}).join('');
return;
}
if (!pageUiState.pageAiMessages.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
return;
}
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
if (item.role === 'tool') {
var statusLabel = item.status === 'completed' ? '完成' : (item.status === 'failed' ? '失败' : '运行中');
var locationRows = Array.isArray(item.locations) && item.locations.length
? '<div class="wolai-page-ai-tool-meta">位置:' + item.locations.map(function(loc, idx) {
return '<span style="display:inline-flex;align-items:center;gap:4px;margin-right:8px">' +
'<span>' + escapeHtml(loc) + '</span>' +
'<button type="button" class="wolai-page-ai-ghost" style="font-size:0.85em;padding:0 4px;min-width:unset" data-page-ai-open-location="' + escapeHtml(loc) + '" data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" title="打开文件">打开</button>' +
'</span>';
}).join('') + '</div>'
: '';
var detailRows = [
locationRows,
item.argsSummary ? '<div class="wolai-page-ai-tool-meta">参数 ' + escapeHtml(item.argsSummary) + '</div>' : '',
item.resultSummary ? '<div class="wolai-page-ai-tool-meta">结果 ' + escapeHtml(item.resultSummary) + '</div>' : '',
item.changedFiles && item.changedFiles.length ? '<pre class="wolai-page-ai-tool-meta wolai-page-ai-changed-files">' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '</pre>' : '',
(item.traceId || item.auditId) ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '</div>' : ''
].filter(Boolean).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-tool-card data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" data-page-ai-tool-status="' + escapeHtml(item.status || '') + '">' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' +
'<details class="wolai-page-ai-tool-details">' +
'<summary>' +
'<strong>' + escapeHtml(item.toolName || item.content || 'tool') + '</strong>' +
'<span>' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '</span>' +
'</summary>' +
(detailRows || '<div class="wolai-page-ai-tool-meta">暂无参数或结果详情。</div>') +
'</details>' +
'</div>' +
'</div>';
}
if (item.kind === 'thought') {
return '' +
'<details class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-thought-delta="true">' +
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.7;font-size:0.85em">思考过程</summary>' +
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.6;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' + escapeHtml(item.content || '') + '</div>' +
'</details>';
}
if (item.kind === 'permission') {
var permissionActions = item.resolved ? '' : (
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">拒绝</button>' +
'</div>'
);
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
'<div class="wolai-page-ai-message-role">权限</div>' +
'<div class="wolai-page-ai-message-text">' +
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
permissionActions +
'</div>' +
'</div>';
}
if (item.kind === 'plan') {
var planEntries = Array.isArray(item.entries) ? item.entries : [];
var listHtml = planEntries.map(function(entry, idx) {
return '<li style="margin:2px 0">' + escapeHtml(String(entry || '')) + '</li>';
}).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-plan="true">' +
'<details class="wolai-page-ai-plan-details" open>' +
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.8;font-size:0.85em">执行计划 · ' + planEntries.length + ' 步</summary>' +
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.75;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' +
'<ol style="margin:4px 0;padding-left:20px">' + listHtml + '</ol>' +
'</div>' +
'</details>' +
'</div>';
}
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '"' + streamingAttr + '>' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}).join('');
conversation.scrollTop = conversation.scrollHeight;
}
function openPageAiDrawer() {
pageAiLoadSessions();
renderPageAiSuggestions();
renderPageAiConversation();
renderPageAiProviderButtons();
renderPageAiControls();
var drawer = ensurePageAiDrawer();
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
Promise.all([
pageAiLoadProfiles(),
pageAiLoadProfileMemory(),
pageAiLoadSkills(),
pageAiLoadTools(),
pageAiLoadGatewayHealth(),
pageAiLoadBackendSessions()
]).then(function() {
renderPageAiControls();
}).catch(function() {}).then(function() {
return pageAiEnsureHermesSession();
}).then(function() {
return pageAiRestoreHermesSession();
}).then(function() {
renderPageAiControls();
}).catch(function(error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'Hermes 当前不可用:' + (error instanceof Error ? error.message : String(error))
});
renderPageAiConversation();
renderPageAiControls();
});
}
function closePageAiDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
pageUiState.pageAiOpen = false;
updatePageAiTriggerState();
}
function isPageAiDrawerOpen() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
return drawer instanceof HTMLElement && !drawer.hidden;
}
async function streamPageAiResponse(response, onEvent) {
if (!response.body || typeof response.body.getReader !== 'function') return;
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var frames = buffer.split('\n\n');
buffer = frames.pop() || '';
frames.forEach(function(frame) {
var eventName = '';
var dataLines = [];
frame.split('\n').forEach(function(line) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
});
var payloadText = dataLines.join('\n');
if (!eventName && payloadText) {
try {
var parsed = JSON.parse(payloadText);
eventName = parsed && parsed.event ? String(parsed.event) : '';
} catch (_) {}
}
if (eventName) onEvent(eventName, payloadText);
});
}
}
function pageAiDecodeDeltaText(payloadText) {
try {
var payload = JSON.parse(payloadText || 'null');
return String((payload && (payload.text || payload.delta)) || '');
} catch (_) {
return String(payloadText || '');
}
}
function pageAiEnsureStreamingAssistantMessage(runId) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
if (existing) return existing;
existing = {
role: 'assistant',
content: '',
runId: id,
streaming: true
};
pageUiState.pageAiMessages.push(existing);
return existing;
}
function pageAiAppendStreamingAssistantDelta(runId, deltaText) {
var delta = String(deltaText || '');
if (!delta) return '';
var message = pageAiEnsureStreamingAssistantMessage(runId);
message.content = String(message.content || '') + delta;
pageAiSyncCurrentSessionMessages();
renderPageAiConversation();
return message.content;
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
var text = String(finalText || (message && message.content) || '');
var content = humanizePageAiResponse(text, promptText);
if (message) {
message.content = content;
message.streaming = false;
} else if (content) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: content,
runId: id
});
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
}
function pageAiLooksLikeBlockEdit(prompt) {
var text = searchText(prompt);
return ['新增', '添加', '插入', '删除', '删掉', '修改', '替换', '改成', '移动', '移到', 'move', 'replace', 'delete', 'insert'].some(function(word) {
return text.indexOf(word) >= 0;
});
}
async function pageAiTryBlockEditWorkflow(prompt, scopedContext) {
if (currentSourceKind() === 'local_folder') return false;
if (!pageAiLooksLikeBlockEdit(prompt)) return false;
var runId = 'page-ai-fast-' + Date.now().toString(36);
var traceId = 'page-ai-fast-' + Date.now().toString(36);
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var started = Date.now();
var response = await fetch('/api/page-ai/block-edit-workflow', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
runId: runId,
profile: pageAiCurrentProfile(),
model: pageAiMnoteToolModel(),
message: prompt,
pageContext: scopedContext.pageContext,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: traceId
})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || payload.ok !== true) {
var code = payload && payload.code ? String(payload.code) : '';
if (code === 'page_ai_workflow_not_block_edit') return false;
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'failed',
toolCallId: runId,
resultSummary: pageAiErrorMessage(payload, 'fast_path_failed_' + response.status)
});
renderPageAiConversation();
pageAiSetRunStatus('failed', runId);
renderPageAiControls();
return true;
}
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'completed',
toolCallId: runId,
resultSummary: 'operations=' + String(Array.isArray(payload.operations) ? payload.operations.length : 0) + ' · ' + String(Date.now() - started) + 'ms'
});
pageUiState.pageAiMessages.push({
role: 'assistant',
content: payload.message || '已通过页面块编辑快路径完成写入。'
});
pageAiSetRunStatus('completed', runId);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'mnote.doc.apply_block_ops',
normalizedToolName: 'mnote.doc.apply.block.ops',
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: traceId,
toolCallId: runId
}
}));
} catch (_) {}
var currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return true;
}
async function sendPageAiMessage(text) {
var allowQueue = ['running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0;
if (pageUiState.pageAiBusy && !allowQueue) return;
var prompt = searchText(text);
if (!prompt) return;
if (!allowQueue) pageUiState.pageAiBusy = true;
var currentSession = null;
try {
await pageAiEnsureHermesSession();
if (!allowQueue) pageAiSetRunStatus('queued');
renderPageAiControls();
var contextSnapshot = currentPageAiContextSnapshot();
var scopedContext = pageAiScopedPageContext(contextSnapshot);
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
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();
if (!allowQueue && await pageAiTryBlockEditWorkflow(prompt, scopedContext)) {
return;
}
var response = await fetch('/api/hermes/client/runs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
contextScope: pageUiState.pageAiContextScope,
message: prompt,
model: pageAiMnoteToolModel(),
pageContext: scopedContext.pageContext,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: 'page-ai-run-' + Date.now().toString(36)
})
});
if (!response.ok) {
var errorPayload = await response.json().catch(function(){ return null; });
throw new Error(pageAiErrorMessage(errorPayload, 'page_ai_failed_' + response.status));
}
var runPayload = await response.json().catch(function(){ return null; });
if (pageAiApplyQueuedRun(runPayload)) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '已加入 Hermes 队列,前一条 run 完成后继续处理。'
});
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return;
}
if (allowQueue) {
throw new Error('hermes_queue_expected_queued_response');
}
var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
var runId = upstream && (upstream.run_id || upstream.runId);
if (!runId) throw new Error('hermes_run_missing_run_id');
var runTraceId = String((upstream && (upstream.trace_id || upstream.traceId)) || (runPayload && (runPayload.trace_id || runPayload.traceId)) || '');
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
headers: { 'accept': 'text/event-stream' }
});
if (!eventResponse.ok) {
var eventError = await eventResponse.json().catch(function(){ return null; });
throw new Error(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
}
var assistantText = '';
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
}
if (eventName === 'thought.delta') {
try {
var thoughtPayload = JSON.parse(payloadText || 'null');
var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || '');
if (thoughtText) {
var msgs = pageUiState.pageAiMessages;
var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null;
if (lastThought) {
lastThought.content += thoughtText;
} else {
msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText });
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
}
} catch (_) {}
}
if (eventName === 'usage.updated') {
try {
var usagePayload = JSON.parse(payloadText || 'null') || {};
var sessionForUsage = pageAiCurrentSession();
if (sessionForUsage) {
sessionForUsage.usage = {
source: 'usage_update',
used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0),
size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0)
};
pageAiPersistSessions();
}
} catch (_) {}
renderPageAiControls();
}
if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') {
pageAiApplyPermissionEvent(eventName, payloadText);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
if (eventName === 'run.completed') {
try {
var completed = JSON.parse(payloadText || 'null');
if (completed && completed.output) assistantText = String(completed.output || '');
if (completed && completed.usage) {
var completedSession = pageAiCurrentSession();
if (completedSession) completedSession.usage = completed.usage;
}
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles);
if (changedFiles.length) {
pageUiState.pageAiMessages.push({
role: 'tool',
content: 'agent.changed_files',
toolCallId: runId + ':agent.changed_files',
toolName: 'agent.changed_files',
toolKind: 'audit',
status: 'completed',
argsSummary: String(agentAudit.rootUri || ''),
resultSummary: String(agentAudit.diffSummary || changedFiles.length + ' changed file(s)'),
changedFiles: changedFiles,
traceId: runTraceId,
auditId: String(agentAudit.eventId || '')
});
try {
var currentId = currentDocumentId();
var currentPath = String(currentId || '').replace(/^local-md:/, '').replace(/~2F/g, '/');
var touchesCurrent = changedFiles.some(function(file) {
var path = String(file && (file.documentId || file.path || file.filePath || '') || '');
return path === currentId || (currentPath && path.indexOf(currentPath) >= 0);
});
if (touchesCurrent) {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'agent.changed_files',
normalizedToolName: 'agent.changed_files',
documentId: currentId,
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: runTraceId,
toolCallId: runId + ':agent.changed_files'
}
}));
}
} catch (_) {}
}
} catch (_) {}
pageAiSetRunStatus('completed', runId);
}
if (eventName === 'run.failed') {
try {
var failed = JSON.parse(payloadText || 'null');
assistantText = String((failed && (failed.message || failed.code || failed.error)) || 'Hermes run failed');
} catch (_) {}
pageAiSetRunStatus('failed', runId);
}
if (eventName === 'run.aborted' || eventName === 'run.cancelled' || eventName === 'run.canceled') {
pageUiState.pageAiStoppedRunIds[runId] = true;
pageAiSetRunStatus('aborted', runId);
}
if (eventName === 'session.info.updated') {
try {
var infoPayload = JSON.parse(payloadText || 'null') || {};
var newTitle = String(infoPayload.title || '').trim();
if (newTitle) {
var sessionForTitle = pageAiCurrentSession();
if (sessionForTitle) {
sessionForTitle.title = newTitle;
sessionForTitle.updatedAt = Date.now();
pageAiPersistSessions();
renderPageAiControls();
}
}
} catch (_) {}
}
if (eventName === 'plan.updated') {
try {
var planPayload = JSON.parse(payloadText || 'null') || {};
var planEntries = Array.isArray(planPayload.entries) ? planPayload.entries : [];
if (planEntries.length) {
var planMsgs = pageUiState.pageAiMessages;
var existingPlan = planMsgs.length > 0 && planMsgs[planMsgs.length - 1].kind === 'plan' ? planMsgs[planMsgs.length - 1] : null;
if (existingPlan) {
existingPlan.entries = planEntries;
existingPlan.updatedAt = Date.now();
} else {
planMsgs.push({ role: 'system', kind: 'plan', entries: planEntries, createdAt: Date.now(), updatedAt: Date.now() });
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
}
} catch (_) {}
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
pageAiSetRunStatus('tool_calling', runId);
renderPageAiControls();
}
});
if (!pageUiState.pageAiStoppedRunIds[runId]) {
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt);
}
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} catch (error) {
pageAiSetRunStatus('failed');
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'Hermes 当前请求失败:' + (error instanceof Error ? error.message : String(error))
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} finally {
if (!allowQueue) pageUiState.pageAiBusy = false;
renderPageAiConversation();
renderPageAiControls();
}
}
function ensurePageSettingsPopover() {
var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (existing instanceof HTMLElement) return existing;
var popover = document.createElement('div');
popover.className = 'wolai-page-settings-popover';
popover.setAttribute('data-testid', 'wolai-page-settings-popover');
popover.setAttribute('data-mnote-surface', 'page-settings');
popover.hidden = true;
popover.innerHTML = '' +
'<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
'<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page">页面选项</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom">自定义页面</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="index">索引</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global">全局选项</button>' +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
createPageOptionRow('wideLayout', 'checkbox') +
createPageOptionRow('smallText', 'checkbox') +
createPageOptionRow('showToc', 'checkbox') +
createPageOptionRow('protectEditing', 'checkbox') +
createPageOptionRow('hideTitleHeader', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
createPageFontRow() +
createPageOptionRow('layoutDensity', 'select') +
createPageOptionRow('collapseBacklinks', 'checkbox') +
createPageOptionRow('hideChildPages', 'checkbox') +
createPageOptionRow('showBlockRefCount', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="index" hidden>' +
'<div class="wolai-page-settings-index-panel" data-testid="wolai-page-settings-local-index-panel">' +
'<div class="wolai-page-settings-index-status" data-testid="wolai-page-settings-local-index-status"></div>' +
'<section class="wolai-page-settings-index-group">' +
'<div class="wolai-page-settings-index-title">反链</div>' +
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-backlinks"></div>' +
'</section>' +
'<section class="wolai-page-settings-index-group">' +
'<div class="wolai-page-settings-index-title">标签</div>' +
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-tags"></div>' +
'</section>' +
'</div>' +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
createGlobalHeadingNumbersRow() +
'</div>' +
'<div class="wolai-page-settings-actions">' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">公开分享页面...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>移动到...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>嵌入到...</button>' +
'<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled>删除页面</button>' +
'</div>' +
'<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
'</div>';
document.body.appendChild(popover);
return popover;
}
function pageSettingsLocalIndexScopeKey() {
return [
currentSourceKind(),
currentRootUri(),
resolveWorkspaceId(document.body),
currentDocumentId()
].join('|');
}
function pageSettingsLocalIndexIsAvailable() {
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
}
function pageSettingsLocalIndexEmpty(message) {
return '<div class="wolai-page-settings-index-empty">' + escapeHtml(message) + '</div>';
}
function renderPageSettingsLocalIndexList(items, kind) {
var rows = Array.isArray(items) ? items : [];
if (!rows.length) {
return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
}
if (kind === 'backlinks') {
return rows.slice(0, 12).map(function(item) {
var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
var path = searchText(item && item.path);
var snippet = searchText(item && item.snippet);
return '' +
'<div class="wolai-page-settings-index-row">' +
'<div class="wolai-page-settings-index-row-title">' + escapeHtml(title) + '</div>' +
(path ? '<div class="wolai-page-settings-index-row-meta">' + escapeHtml(path) + '</div>' : '') +
(snippet ? '<div class="wolai-page-settings-index-row-snippet">' + escapeHtml(snippet) + '</div>' : '') +
'</div>';
}).join('');
}
return rows.slice(0, 16).map(function(item) {
var tag = searchText(item && item.tag) || 'untagged';
var count = Number(item && item.count || 0);
return '' +
'<div class="wolai-page-settings-index-row is-tag">' +
'<div class="wolai-page-settings-index-row-title">#' + escapeHtml(tag) + '</div>' +
'<div class="wolai-page-settings-index-row-meta">' + count + ' 个页面</div>' +
'</div>';
}).join('');
}
function renderPageSettingsLocalIndex(popover) {
popover = popover || ensurePageSettingsPopover();
var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
if (!pageSettingsLocalIndexIsAvailable()) {
statusNode.textContent = '本地索引仅在本地工作区页面可用';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
return;
}
var summary = pageUiState.localIndexSummary || {};
if (summary.loading) {
statusNode.textContent = '正在读取本地索引...';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
return;
}
if (summary.error) {
statusNode.textContent = '本地索引读取失败';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
return;
}
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
statusNode.textContent = '切换到索引页签后读取本地索引';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
return;
}
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
}
async function loadPageSettingsLocalIndex(force) {
if (!pageSettingsLocalIndexIsAvailable()) {
renderPageSettingsLocalIndex();
return;
}
var scopeKey = pageSettingsLocalIndexScopeKey();
var current = pageUiState.localIndexSummary || {};
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
renderPageSettingsLocalIndex();
return;
}
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: true,
error: '',
backlinks: null,
tags: null
};
renderPageSettingsLocalIndex();
try {
var baseParams = new URLSearchParams();
baseParams.set('workspaceId', resolveWorkspaceId(document.body));
baseParams.set('rootUri', currentRootUri());
var backlinksParams = new URLSearchParams(baseParams);
backlinksParams.set('documentId', currentDocumentId());
var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
var responses = await Promise.all([
fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
fetch(tagsUrl, { headers: { accept: 'application/json' } })
]);
var backlinksPayload = await responses[0].json().catch(function(){ return null; });
var tagsPayload = await responses[1].json().catch(function(){ return null; });
if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
throw new Error('backlinks_' + responses[0].status);
}
if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
throw new Error('tags_' + responses[1].status);
}
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: false,
error: '',
backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
};
} catch (error) {
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: false,
error: error instanceof Error ? error.message : String(error),
backlinks: [],
tags: []
};
}
renderPageSettingsLocalIndex();
}
function renderPageSettingsPopover() {
var popover = ensurePageSettingsPopover();
var options = currentPageOptions();
popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
var key = input.getAttribute('data-page-option-checkbox');
input.checked = Boolean(options[key]);
input.disabled = !pageOptionIsSupported(key);
});
popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
var key = select.getAttribute('data-page-option-select');
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();
statsNode.innerHTML = '' +
'<span>字数 ' + Number(stats.wordCount || 0) + '</span>' +
'<span>字符 ' + Number(stats.characterCount || 0) + '</span>' +
'<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
'<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
}
renderPageSettingsLocalIndex(popover);
}
function setActivePageSettingsTab(tabName) {
var popover = ensurePageSettingsPopover();
popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
var active = tab.getAttribute('data-page-settings-tab') === tabName;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
});
if (tabName === 'index') void loadPageSettingsLocalIndex(false);
}
async function persistPageOptionsPatch(patch) {
var previous = Object.assign({}, currentPageOptions());
pageUiState.pageOptions = Object.assign({}, previous, patch);
var nextOptions = Object.assign({}, pageUiState.pageOptions);
applyPageOptionsToShell();
renderPageSettingsPopover();
try {
var response = await fetch('/api/documents/options', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
options: nextOptions,
commandName: 'page.layout.updateOptions'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
}
document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
} catch (error) {
pageUiState.pageOptions = previous;
applyPageOptionsToShell();
renderPageSettingsPopover();
document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
}
}
function isPageSettingsOpen() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
}
function openPageSettingsPopover() {
if (!currentDocumentId()) return;
var popover = ensurePageSettingsPopover();
renderPageSettingsPopover();
setActivePageSettingsTab('page');
popover.hidden = false;
pageUiState.pageSettingsOpen = true;
updatePageSettingsTriggerState();
}
function closePageSettingsPopover() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
pageUiState.pageSettingsOpen = false;
updatePageSettingsTriggerState();
}
function togglePageSettingsPopover() {
if (isPageSettingsOpen()) closePageSettingsPopover();
else openPageSettingsPopover();
}
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
attachmentClassForFileName: (...args) => attachmentClassForFileName(...args),
buildLocalFileOpenUrl,
buildLocalOnlyOfficeOpenUrl,
buildOnlyOfficeOpenPath,
buildOnlyOfficeOpenUrl,
currentDocumentId,
currentRootUri,
currentWorkspaceSourcePayload,
fileTreeIconKindForFileName,
inferCodeAttachmentLanguage: (...args) => inferCodeAttachmentLanguage(...args),
inferOnlyOfficeFileType,
isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args),
isPdfAttachmentFileName: (...args) => isPdfAttachmentFileName(...args),
localFilePathFromAssetId,
openLocalOfficeFileInActiveTab,
openLocalResourceInActiveTab,
openTreeContextMenu: (...args) => openTreeContextMenu(...args),
resolveWorkspaceId,
triggerBrowserDownload: (...args) => triggerBrowserDownload(...args),
uploadedFileSize,
});
const refreshEditorLocalAttachmentExistence = (...args) => sidebarAttachmentOpen.refreshEditorLocalAttachmentExistence(...args);
const detailFromEditorAttachmentLink = (...args) => sidebarAttachmentOpen.detailFromEditorAttachmentLink(...args);
const enhanceEditorAttachmentLink = (...args) => sidebarAttachmentOpen.enhanceEditorAttachmentLink(...args);
const enhanceEditorAttachmentLinks = (...args) => sidebarAttachmentOpen.enhanceEditorAttachmentLinks(...args);
const openEditorAttachmentDetail = (...args) => sidebarAttachmentOpen.openEditorAttachmentDetail(...args);
const openEditorAttachmentNewWindow = (...args) => sidebarAttachmentOpen.openEditorAttachmentNewWindow(...args);
const openEditorAttachmentEditTab = (...args) => sidebarAttachmentOpen.openEditorAttachmentEditTab(...args);
const openCodeEditorAttachment = (...args) => sidebarAttachmentOpen.openCodeEditorAttachment(...args);
const openEditorAttachmentDownload = (...args) => sidebarAttachmentOpen.openEditorAttachmentDownload(...args);
document.addEventListener('click', function(e) {
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
if (activeTreeContextMenu) closeTreeContextMenu();
var sidebarToggle = closestAction(e.target, '[data-mnote-action="toggle-sidebar"]');
if (sidebarToggle) {
e.preventDefault();
toggleWorkspaceSidebar(sidebarToggle);
return;
}
var attachmentAction = closestAction(e.target, '[data-attachment-action]');
if (attachmentAction) {
e.preventDefault();
e.stopPropagation();
var link = activeEditorAttachmentLink;
if (!(link instanceof HTMLAnchorElement)) return;
var attachmentDetail = detailFromEditorAttachmentLink(link);
var attachmentActionName = attachmentAction.getAttribute('data-attachment-action') || '';
if (attachmentActionName === 'download') {
openEditorAttachmentDownload(attachmentDetail);
return;
}
if (attachmentActionName === 'menu') {
openEditorAttachmentMenu(link, attachmentAction);
return;
}
return;
}
var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
e.preventDefault();
openEditorAttachmentLink(editorAttachmentLink);
return;
}
var historyClose = closestAction(e.target, '[data-page-history-action="close"]');
if (historyClose) {
e.preventDefault();
closePageHistoryDrawer();
return;
}
var shareClose = closestAction(e.target, '[data-page-share-action="close"]');
if (shareClose) {
e.preventDefault();
closePageShareDialog();
return;
}
var shareCopy = closestAction(e.target, '[data-page-share-action="copy-link"]');
if (shareCopy) {
e.preventDefault();
void copyTreeContextValue(window.location.href, 'page-share-link');
return;
}
var pageHistoryTrigger = closestAction(e.target, '[data-mnote-action="open-page-history"]');
if (pageHistoryTrigger) {
e.preventDefault();
openPageHistoryDrawer();
return;
}
var pageSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-page-settings"]');
if (pageSettingsTrigger) {
e.preventDefault();
togglePageSettingsPopover();
return;
}
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
if (isPageSettingsOpen() && !pageSettingsPanel) {
closePageSettingsPopover();
}
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
if (pageSettingsTab) {
e.preventDefault();
setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page');
return;
}
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
if (pageSettingsAction) {
e.preventDefault();
var actionName = pageSettingsAction.getAttribute('data-page-settings-action') || '';
if (actionName === 'history') {
openPageHistoryDrawer();
return;
}
if (actionName === 'share') {
openPageShareDialog();
return;
}
return;
}
var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
if (pageAiTrigger) {
e.preventDefault();
openPageAiDrawer();
return;
}
var pageAiClose = closestAction(e.target, '[data-page-ai-action="close"]');
if (pageAiClose) {
e.preventDefault();
closePageAiDrawer();
return;
}
var pageAiSettings = closestAction(e.target, '[data-page-ai-action="open-hermes-settings"]');
if (pageAiSettings) {
e.preventDefault();
pageAiOpenHermesSettings();
return;
}
var pageAiStop = closestAction(e.target, '[data-page-ai-action="stop-run"]');
if (pageAiStop) {
e.preventDefault();
void pageAiStopRun();
return;
}
var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
if (pageAiRotate) {
e.preventDefault();
pageUiState.pageAiSuggestionIndex += 1;
renderPageAiSuggestions();
return;
}
var pageAiIntent = closestAction(e.target, '[data-page-ai-intent]');
if (pageAiIntent) {
e.preventDefault();
var intentName = pageAiIntent.getAttribute('data-page-ai-intent') || '';
if (intentName === 'create-summary') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_summary,为当前页面创建或更新 AI Summary。');
return;
}
if (intentName === 'create-ai-note') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_ai_note,基于当前页面创建一篇新的 AI Note。');
return;
}
}
var pageAiTab = closestAction(e.target, '[data-page-ai-tab]');
if (pageAiTab) {
e.preventDefault();
pageUiState.pageAiPage = pageAiTab.getAttribute('data-page-ai-tab') || 'chat';
if (pageUiState.pageAiPage === 'runtime') void pageAiLoadGatewayHealth();
renderPageAiControls();
renderPageAiConversation();
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 pageAiMemorySave = closestAction(e.target, '[data-page-ai-memory-save]');
if (pageAiMemorySave) {
e.preventDefault();
void pageAiSaveProfileMemory(pageAiMemorySave.getAttribute('data-page-ai-memory-save') || '');
return;
}
var pageAiSkillToggle = closestAction(e.target, '[data-page-ai-skill-toggle]');
if (pageAiSkillToggle) {
e.preventDefault();
var skillName = pageAiSkillToggle.getAttribute('data-page-ai-skill-toggle') || '';
var nextEnabled = pageAiSkillToggle.getAttribute('aria-pressed') !== 'true';
void pageAiToggleSkill(skillName, nextEnabled);
return;
}
var pageAiToolToggle = closestAction(e.target, '[data-page-ai-tool-toggle]');
if (pageAiToolToggle) {
e.preventDefault();
var toolName = pageAiToolToggle.getAttribute('data-page-ai-tool-toggle') || '';
var nextToolEnabled = pageAiToolToggle.getAttribute('aria-pressed') !== 'true';
void pageAiToggleTool(toolName, nextToolEnabled);
return;
}
var pageAiSessionResume = closestAction(e.target, '[data-page-ai-session-resume]');
if (pageAiSessionResume) {
e.preventDefault();
void pageAiResumeBackendSession(pageAiSessionResume.getAttribute('data-page-ai-session-resume') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiSessionRename = closestAction(e.target, '[data-page-ai-session-rename]');
if (pageAiSessionRename) {
e.preventDefault();
void pageAiRenameBackendSession(pageAiSessionRename.getAttribute('data-page-ai-session-rename') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiSessionDelete = closestAction(e.target, '[data-page-ai-session-delete]');
if (pageAiSessionDelete) {
e.preventDefault();
void pageAiDeleteBackendSession(pageAiSessionDelete.getAttribute('data-page-ai-session-delete') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiPermissionAction = closestAction(e.target, '[data-page-ai-permission-action]');
if (pageAiPermissionAction) {
e.preventDefault();
pageAiResolvePermission(
pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '',
pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny'
);
return;
}
var pageAiOpenLocation = closestAction(e.target, '[data-page-ai-open-location]');
if (pageAiOpenLocation) {
e.preventDefault();
var loc = String(pageAiOpenLocation.getAttribute('data-page-ai-open-location') || '').trim();
if (loc) {
openLocalResourceInActiveTab({ path: loc }).then(function(opened) {
if (!opened) {
var href = buildLocalFileOpenUrl(loc, false);
if (href) window.open(href, '_blank', 'noopener,noreferrer');
}
});
}
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();
var text = pageAiSuggestion.getAttribute('data-page-ai-suggestion') || '';
var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (inputNode instanceof HTMLTextAreaElement) {
inputNode.value = text;
inputNode.focus();
}
return;
}
var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
if (pageAiNewSession) {
e.preventDefault();
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';
renderPageAiControls();
renderPageAiConversation();
if (pageUiState.pageAiPage === 'history') {
void pageAiLoadBackendSessions().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}
return;
}
var pageAiSend = closestAction(e.target, '[data-page-ai-action="send"]');
if (pageAiSend) {
e.preventDefault();
var input = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (input instanceof HTMLTextAreaElement) {
var message = input.value;
input.value = '';
void sendPageAiMessage(message);
}
return;
}
var cancelQueuedRun = closestAction(e.target, '[data-page-ai-action="cancel-queued-run"]');
if (cancelQueuedRun) {
e.preventDefault();
void pageAiCancelQueuedRun(cancelQueuedRun.getAttribute('data-page-ai-queue-id'));
return;
}
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
toggleSearchModal();
return;
}
var searchDialog = closestAction(e.target, '.wolai-search-dialog');
if (isSearchModalOpen() && !searchDialog) {
closeSearchModal();
return;
}
var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
if (sourceMenuTrigger) {
e.preventDefault();
var existingSourceMenu = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existingSourceMenu) closeWorkspaceSourceMenu();
else openWorkspaceSourceMenu(sourceMenuTrigger);
return;
}
var sourceMenu = closestAction(e.target, '[data-testid="mnote-workspace-source-menu"]');
if (!sourceMenu) closeWorkspaceSourceMenu();
var accountMenuTrigger = closestAction(e.target, '[data-mnote-action="open-account-menu"]');
if (accountMenuTrigger) {
e.preventDefault();
var existingAccountMenu = document.querySelector('[data-testid="mnote-account-menu"]');
if (existingAccountMenu) closeAccountMenu();
else openAccountMenu(accountMenuTrigger);
return;
}
var accountMenu = closestAction(e.target, '[data-testid="mnote-account-menu"]');
if (!accountMenu) closeAccountMenu();
var trashTrigger = closestAction(e.target, '[data-mnote-action="open-trash-modal"]');
if (trashTrigger) {
e.preventDefault();
openTrashModal(trashTrigger);
return;
}
var trashClose = closestAction(e.target, '[data-mnote-trash-modal-close]');
if (trashClose) {
e.preventDefault();
closeTrashModal();
return;
}
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
if (tabTrigger) {
e.preventDefault();
switchSidebarTreeTab(tabTrigger);
return;
}
var localFolderTrigger = closestAction(e.target, '[data-mnote-action="open-local-folder"]');
if (localFolderTrigger) {
e.preventDefault();
requestOpenLocalFolder();
return;
}
var createLocalWorkspaceTrigger = closestAction(e.target, '[data-mnote-action="create-local-workspace"]');
if (createLocalWorkspaceTrigger) {
e.preventDefault();
void createDefaultLocalWorkspace(createLocalWorkspaceTrigger);
return;
}
var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
if (createTrigger) {
e.preventDefault();
void createPage(createTrigger, null);
return;
}
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(e.target)) {
var fileBtn = closestAction(e.target, '[data-rust-action]');
var fileRow = closestAction(e.target, '.tree-row[data-shell-mode="filetree"]');
if (!fileRow) return;
var fileAction = fileBtn ? fileBtn.getAttribute('data-rust-action') : 'open';
var rowId = fileRow.getAttribute('data-row-id') || '';
var rowKind = fileRow.getAttribute('data-row-kind') || '';
var documentId = fileRow.getAttribute('data-document-id') || fileRow.getAttribute('data-doc-id') || '';
var ownerDocumentId = fileRow.getAttribute('data-owner-document-id') || documentId;
var assetId = fileRow.getAttribute('data-asset-id') || '';
var assetType = '';
var kindBadge = fileRow.querySelector('.tree-kind-badge');
if (kindBadge instanceof HTMLElement) {
assetType = kindBadge.getAttribute('data-kind') || '';
}
if (fileAction === 'toggle') {
e.preventDefault();
toggleChildren(fileRow, fileBtn);
return;
}
if (fileAction === 'create') {
e.preventDefault();
void createPage(fileBtn || fileRow, documentId || fileRow.getAttribute('data-node-id'));
return;
}
if (fileAction === 'menu') {
e.preventDefault();
if (rowId && !sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
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;
}
if (fileAction === 'open' && rowKind === 'folder') {
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
toggleChildren(fileRow, fileRow.querySelector('[data-rust-action="toggle"]'));
return;
}
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
var objectIdentity = readFileTreeObjectIdentity(fileRow);
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
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: ownerDocumentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow), openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab' });
}
return;
}
var tree = document.getElementById('sidebar-tree-root');
if (!tree || !tree.contains(e.target)) return;
var pageRow = closestAction(e.target, '.tree-row[data-shell-mode="page"]');
var btn = closestAction(e.target, '[data-rust-action]');
if (!btn && !pageRow) return;
var nodeId = (btn && btn.getAttribute('data-node-id')) || (pageRow && pageRow.getAttribute('data-node-id')) || '';
var action = btn ? btn.getAttribute('data-rust-action') : 'open';
if (action === 'toggle') {
var row = btn.closest('.tree-row');
if (!row) return;
toggleChildren(row, btn);
e.preventDefault();
} else if (action === 'open') {
var openTrigger = btn || pageRow;
if (openTrigger && openTrigger.getAttribute('data-page-openable') === 'false') {
e.preventDefault();
return;
}
var workspaceId = resolveWorkspaceId(openTrigger);
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
e.preventDefault();
} else if (action === 'create') {
e.preventDefault();
void createPage(btn, nodeId);
} else if (action === 'rename') {
e.preventDefault();
var title = window.prompt('重命名页面');
if (title && title.trim()) {
void dispatchTreeCommand(btn, {
action: 'rename',
workspaceId: resolveWorkspaceId(btn),
documentId: nodeId,
title: title.trim()
}).then(function(){
updateTitleEverywhere(nodeId, title.trim());
});
}
} else if (action === 'menu') {
e.preventDefault();
var menuPoint = rowCenter(btn);
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
}
});
document.addEventListener('contextmenu', function(event) {
var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
event.preventDefault();
event.stopPropagation();
enhanceEditorAttachmentLink(editorAttachmentLink);
openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });
return;
}
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;
}
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
if (pageRow) {
event.preventDefault();
openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow);
}
});
document.addEventListener('keydown', function(event) {
var keyTarget = event.target;
var fileTreeRootForKey = document.getElementById('sidebar-file-tree-root');
var fileTreeRowForKey = closestAction(keyTarget, '.tree-row[data-shell-mode="filetree"]');
if (!fileTreeRowForKey && fileTreeRootForKey && fileTreeRootForKey.contains(document.activeElement)) {
fileTreeRowForKey = closestAction(document.activeElement, '.tree-row[data-shell-mode="filetree"]');
}
if (fileTreeRowForKey) {
var _kbRT_ = window.__mnoteFileTreeKeyboardRuntime;
if (_kbRT_ && typeof _kbRT_.handleFileTreeKeyDown === 'function') {
var handled = _kbRT_.handleFileTreeKeyDown(event, {
closestAction: closestAction,
beginFileTreeInlineRename: beginFileTreeInlineRename,
selectedSidebarFileTreeRows: selectedSidebarFileTreeRows,
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
deleteSelectedSidebarFileTreeRows: deleteSelectedSidebarFileTreeRows,
pasteSidebarFileTreeClipboard: pasteSidebarFileTreeClipboard,
});
if (handled) return;
} else {
// inline fallback
if (event.key === 'F2') {
event.preventDefault();
beginFileTreeInlineRename(fileTreeRowForKey);
return;
}
if ((event.ctrlKey || event.metaKey) && !event.altKey && event.key) {
var shortcutKey = event.key.toLowerCase();
if (shortcutKey === 'c' || shortcutKey === 'x') {
var selectedRows = selectedSidebarFileTreeRows();
var selectedRowIds = selectedRows.map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean);
if (selectedRowIds.length > 0) {
event.preventDefault();
sidebarFileTreeClipboard = { action: shortcutKey === 'x' ? 'cut' : 'copy', rowIds: selectedRowIds };
document.documentElement.setAttribute('data-mnote-filetree-clipboard-action', sidebarFileTreeClipboard.action);
}
return;
}
if (shortcutKey === 'v') {
event.preventDefault();
void pasteSidebarFileTreeClipboard(fileTreeRowForKey);
return;
}
}
if (event.key === 'Delete' || event.key === 'Backspace') {
event.preventDefault();
var delCtx = buildSidebarFileTreeContext('filetree');
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')) {
return;
}
void deleteSelectedSidebarFileTreeRows(fileTreeRowForKey);
return;
}
}
}
if (event.key === 'Escape' && isPageSettingsOpen()) {
event.preventDefault();
closePageSettingsPopover();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
event.preventDefault();
toggleSearchModal();
return;
}
if (event.key === 'Escape') {
closeTrashModal();
closeSearchModal();
closeTreeContextMenu();
}
if (event.key === 'Enter' && !event.shiftKey) {
var aiInput = closestAction(event.target, '[data-page-ai-input]');
if (aiInput instanceof HTMLTextAreaElement) {
event.preventDefault();
var text = aiInput.value;
aiInput.value = '';
void sendPageAiMessage(text);
}
}
});
document.addEventListener('input', function(event) {
var skillSearch = closestAction(event.target, '[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement) {
pageUiState.pageAiSkillQuery = skillSearch.value;
renderPageAiControls();
return;
}
var sessionSearch = closestAction(event.target, '[data-page-ai-session-search]');
if (sessionSearch instanceof HTMLInputElement) {
var sessionQuery = sessionSearch.value;
window.clearTimeout(pageUiState.pageAiSessionSearchTimer || 0);
pageUiState.pageAiSessionSearchTimer = window.setTimeout(function() {
void pageAiSearchBackendSessions(sessionQuery).catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}, 200);
return;
}
var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]');
if (memoryEditor instanceof HTMLTextAreaElement) {
var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || '';
if (['memory', 'user', 'soul'].indexOf(section) >= 0) {
pageUiState.pageAiProfileMemoryDrafts[section] = memoryEditor.value;
}
}
});
document.addEventListener('change', function(event) {
var pageAiAcpRuntimeSelect = closestAction(event.target, '[data-page-ai-acp-runtime]');
if (pageAiAcpRuntimeSelect instanceof HTMLSelectElement) {
var next = String(pageAiAcpRuntimeSelect.value || 'reasonix').trim() || 'reasonix';
pageUiState.pageAiAcpRuntime = next;
pageUiState.pageAiSkills = { categories: [], archived: [] };
pageUiState.pageAiSkillError = '';
pageAiPersistSessions();
void pageAiLoadProfiles();
if (next !== 'reasonix') void pageAiLoadProfileMemory();
void pageAiLoadSkills();
void pageAiLoadBackendSessions().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
});
renderPageAiControls();
renderPageAiProviderButtons();
return;
}
var pageAiProfileSelect = closestAction(event.target, '[data-page-ai-profile-select]');
if (pageAiProfileSelect instanceof HTMLSelectElement) {
void pageAiSwitchProfile(pageAiProfileSelect.value);
return;
}
var pageAiContextSelect = closestAction(event.target, '[data-page-ai-context-scope]');
if (pageAiContextSelect instanceof HTMLSelectElement) {
pageAiSetContextScope(pageAiContextSelect.value);
renderPageAiControls();
return;
}
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') || '';
if (!pageOptionIsSupported(key)) return;
var patch = {};
patch[key] = checkbox.checked;
void persistPageOptionsPatch(patch);
return;
}
var select = closestAction(event.target, '[data-page-option-select]');
if (select instanceof HTMLSelectElement) {
var selectKey = select.getAttribute('data-page-option-select') || '';
var next = {};
if (selectKey === 'layoutDensity') next.layoutDensity = select.value;
if (selectKey === 'pageFont') next.pageFont = select.value;
if (Object.keys(next).length) {
void persistPageOptionsPatch(next);
}
}
});
function initializePageUiSurfaces() {
pageUiState.pageOptions = null;
applyPageOptionsToShell();
updatePageSettingsTriggerState();
updatePageAiTriggerState();
ensureHistorySnapshotsSeeded();
installWorkspaceSidebarResizer();
}
window.__mnoteRecordPageHistorySnapshot = recordPageHistorySnapshot;
window.__mnoteApplyPageOptionsToShell = applyPageOptionsToShell;
function scheduleInitializePageUiSurfaces() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initializePageUiSurfaces, 0);
}, { once: true });
return;
}
setTimeout(initializePageUiSurfaces, 0);
}
scheduleInitializePageUiSurfaces();
function installMnoteDevHotReload() {
var bootId = '';
var failedOnce = false;
var timer = 0;
function tick() {
fetch('/api/dev/hot-reload', { cache: 'no-store', headers: { accept: 'application/json' } })
.then(function(response) { return response.ok ? response.json() : null; })
.then(function(payload) {
if (!payload || payload.enabled !== true || !payload.bootId) {
if (!bootId && timer) window.clearInterval(timer);
return;
}
document.documentElement.setAttribute('data-mnote-dev-hot-reload', 'enabled');
if (!bootId) {
bootId = String(payload.bootId);
return;
}
if (failedOnce || String(payload.bootId) !== bootId) {
window.location.assign(window.location.href);
}
})
.catch(function() {
if (bootId) failedOnce = true;
});
}
tick();
timer = window.setInterval(tick, 1000);
}
installMnoteDevHotReload();
function readPageDragNodeId(event) {
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
return (fromTransfer || draggingPageNodeId || '').trim();
}
function clearPageDropFeedback() {
if (activePageDropRow instanceof HTMLElement) {
activePageDropRow.setAttribute('data-drop-feedback', 'false');
}
activePageDropRow = null;
}
function canDropPage(sourceNodeId, targetRow) {
if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false;
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
if (!targetNodeId || targetNodeId === sourceNodeId) return false;
var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]');
return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow));
}
function pageDropPosition(event, row) {
var rect = row.getBoundingClientRect();
var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
if (ratio < 0.25) return 'before';
if (ratio > 0.75) return 'after';
return 'inside';
}
function resolvePageMoveTarget(targetRow, position) {
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
var parentId = targetRow.getAttribute('data-parent-id') || null;
if (position === 'inside') {
var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
return { parentId: targetNodeId, sortOrder: children.length };
}
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) {
return (row.getAttribute('data-parent-id') || '') === (parentId || '');
});
var index = siblings.indexOf(targetRow);
return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) };
}
document.addEventListener('dragstart', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]');
if (pageRow) {
draggingPageNodeId = pageRow.getAttribute('data-node-id') || '';
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId);
event.dataTransfer.setData('text/plain', draggingPageNodeId);
}
return;
}
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
if (fileRow) {
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
if (_dndRT_ && typeof _dndRT_.startFileTreeDrag === 'function') {
var dragInfo = _dndRT_.startFileTreeDrag(fileRow, {
selectedSidebarFileTreeRowIdsForDrag: selectedSidebarFileTreeRowIdsForDrag,
});
draggingFileTreeRowIds = _dndRT_.draggingFileTreeRowIds;
if (event.dataTransfer && dragInfo) {
event.dataTransfer.effectAllowed = dragInfo.effectAllowed || 'copyMove';
event.dataTransfer.setData(_dndRT_.FILETREE_DRAG_MIME, dragInfo.payload);
event.dataTransfer.setData('application/x-mnote-file-tree', dragInfo.payload);
event.dataTransfer.setData('text/plain', dragInfo.payload);
}
} else {
// inline fallback
draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
if (event.dataTransfer) {
var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
event.dataTransfer.effectAllowed = 'copyMove';
event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
event.dataTransfer.setData('application/x-mnote-file-tree', payload);
event.dataTransfer.setData('text/plain', payload);
}
}
}
});
document.addEventListener('dragover', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
var sourceNodeId = readPageDragNodeId(event);
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
event.preventDefault();
clearPageDropFeedback();
pageRow.setAttribute('data-drop-feedback', 'true');
pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow));
activePageDropRow = pageRow;
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
return;
}
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
if (_dndRT_ && typeof _dndRT_.handleFileTreeDragOver === 'function') {
if (_dndRT_.handleFileTreeDragOver(event, { closestAction: closestAction })) {
return;
}
} else {
// inline fallback
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(event.target)) {
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
var hasInternal = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return;
event.preventDefault();
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
var copyModifier = event.altKey || event.ctrlKey || event.metaKey;
if (event.dataTransfer) event.dataTransfer.dropEffect = hasFiles || copyModifier ? 'copy' : 'move';
}
}
});
document.addEventListener('drop', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
var sourceNodeId = readPageDragNodeId(event);
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
event.preventDefault();
var position = pageDropPosition(event, pageRow);
var target = resolvePageMoveTarget(pageRow, position);
clearPageDropFeedback();
draggingPageNodeId = '';
void dispatchTreeCommand(pageRow, {
action: 'move',
workspaceId: resolveWorkspaceId(pageRow),
documentId: sourceNodeId,
parentId: target.parentId,
sortOrder: target.sortOrder
});
return;
}
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
if (_dndRT_ && typeof _dndRT_.handleFileTreeDrop === 'function') {
_dndRT_.handleFileTreeDrop(event, {
closestAction: closestAction,
resolveWorkspaceId: resolveWorkspaceId,
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
ensureFileTreeWritableTarget: ensureFileTreeWritableTarget,
dispatchSidebarEvent: dispatchSidebarEvent,
});
} else {
// inline fallback
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(event.target)) {
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
var raw = event.dataTransfer ? event.dataTransfer.getData(FILETREE_DRAG_MIME) || event.dataTransfer.getData('application/x-mnote-file-tree') || '' : '';
var rowIds = draggingFileTreeRowIds.slice();
if (raw) {
try {
var parsed = JSON.parse(raw);
if (Array.isArray(parsed.rowIds)) rowIds = parsed.rowIds;
} catch (_) {}
}
if (!files.length && !rowIds.length) return;
event.preventDefault();
var detail = {
workspaceId: resolveWorkspaceId(targetRow || fileTree),
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null,
targetRelativePath: targetRow ? fileTreeRowLocalUploadTargetRelativePath(targetRow) : '',
uploadIntent: 'filetree.folder.drop'
};
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
if (files.length) {
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
} else {
void Promise.resolve(ensureFileTreeWritableTarget('drop', targetRow, rowIds, event.altKey === true || event.ctrlKey === true || event.metaKey === true)).then(function(writable) {
if (!writable) return;
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true || event.ctrlKey === true || event.metaKey === true }));
});
}
draggingFileTreeRowIds = [];
}
}
});
document.addEventListener('dragend', function() {
draggingPageNodeId = '';
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
if (_dndRT_ && typeof _dndRT_.resetFileTreeDragState === 'function') {
_dndRT_.resetFileTreeDragState();
} else {
draggingFileTreeRowIds = [];
clearPageDropFeedback();
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
}
});
sidebarTreeLiveApply.installTreeLiveApplyEventListeners();
var tree = document.getElementById('sidebar-tree-root');
var activeId = currentDocumentId();
if (tree && activeId) {
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();
schedulePendingLocalFolderRestoreFocus();
window.addEventListener('mnote:primary-document-activated', function(event) {
var detail = event && event.detail ? event.detail : {};
selectSidebarFileTreeDocument(detail.documentId, { scrollIntoView: false });
});
restoreSidebarTreeTab();
startLocalFolderSidebarWatch();
})();