重构基线

This commit is contained in:
Agent Board
2026-06-25 21:08:17 +08:00
parent cc4f975466
commit dabaf03bd7
42 changed files with 7720 additions and 138 deletions
@@ -0,0 +1,69 @@
// MNote UI 基础运行时:全局 portal 与 toast API。
(function initMnoteUiRuntime() {
if (window.__mnoteUiRuntimeStarted) return;
window.__mnoteUiRuntimeStarted = true;
function ensurePortalRoot() {
var root = document.getElementById('mnote-portal-root');
if (root instanceof HTMLElement) return root;
root = document.createElement('div');
root.id = 'mnote-portal-root';
root.setAttribute('data-mnote-portal-root', 'true');
document.body.appendChild(root);
return root;
}
function ensureToastRegion() {
var root = ensurePortalRoot();
var region = root.querySelector('[data-mnote-toast-region="true"]');
if (region instanceof HTMLElement) return region;
region = document.createElement('div');
region.className = 'mnote-toast-region';
region.setAttribute('data-mnote-toast-region', 'true');
region.setAttribute('role', 'status');
region.setAttribute('aria-live', 'polite');
root.appendChild(region);
return region;
}
function toast(message, options) {
var text = String(message || '').trim();
if (!text) return null;
var config = options || {};
var kind = String(config.kind || config.type || 'info').trim() || 'info';
var timeoutMs = Number(config.timeoutMs || config.duration || 3200);
if (!Number.isFinite(timeoutMs) || timeoutMs < 800) timeoutMs = 3200;
var region = ensureToastRegion();
var item = document.createElement('div');
item.className = 'mnote-toast mnote-toast--' + kind;
item.setAttribute('data-mnote-toast', 'true');
item.setAttribute('data-kind', kind);
item.textContent = text;
region.appendChild(item);
requestAnimationFrame(function showToast() {
item.classList.add('mnote-toast--visible');
});
window.setTimeout(function hideToast() {
item.classList.remove('mnote-toast--visible');
window.setTimeout(function removeToast() {
if (item.parentNode) item.parentNode.removeChild(item);
}, 180);
}, timeoutMs);
return item;
}
window.mnote = window.mnote || {};
window.mnote.ensurePortalRoot = ensurePortalRoot;
window.mnote.toast = toast;
window.addEventListener('mnote:toast', function onMnoteToast(event) {
var detail = event && event.detail ? event.detail : {};
toast(detail.message, detail);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensurePortalRoot, { once: true });
} else {
ensurePortalRoot();
}
})();
@@ -123,6 +123,7 @@ export function createSidebarPageAiProfileRuntime(context) {
}
function pageAiSessionAgentFilterValue(session) {
if (String(session && session.source || '') === 'board' || session && session.workerPresetId) return 'board:mnote-page-ai';
var agentId = pageAiNormalizeAgentId(session && session.agentId);
if (agentId === 'reasonix') return 'reasonix';
var profileId = String(session && (session.profileId || session.profile) || '').trim();
@@ -131,6 +132,11 @@ export function createSidebarPageAiProfileRuntime(context) {
}
function pageAiSessionAgentLabel(session) {
if (String(session && session.source || '') === 'board' || session && session.workerPresetId) {
var worker = String(session && session.workerPresetId || 'mnote-page-ai-zcode').trim();
var model = String(session && session.modelOverride || '').trim();
return 'Agent Board / ' + worker + (model ? ' / ' + model : '');
}
var agentId = pageAiNormalizeAgentId(session && session.agentId);
if (agentId === 'reasonix') return pageAiAgentRecord(agentId).label || 'Reasonix';
var profileId = String(session && (session.profileId || session.profile) || '').trim();
@@ -54,6 +54,16 @@ export function createSidebarPageAiRenderRuntime(context) {
pageAiSkillSourceParts,
pageAiToggleableSkillEntries,
pageAiUsageSummary,
pageAiBoardFirstEnabled,
pageAiBoardWorkerOptions,
pageAiBoardWorkflowOptions,
pageAiBoardSelectedWorkerId,
pageAiBoardSelectedWorkflowId,
pageAiBoardWorkerLabel,
pageAiBoardWorkflowLabel,
pageAiBoardModelOptions,
pageAiBoardSelectedModelOverride,
pageAiBoardModelLabel,
pageUiState,
renderPageAiMarkdown,
searchText,
@@ -62,6 +72,18 @@ export function createSidebarPageAiRenderRuntime(context) {
} = context;
const PAGE_AI_CONTEXT_REF_REGISTRY = Array.isArray(contextRefRegistry) ? contextRefRegistry : [];
function pageAiLocalBoardFirstEnabled() {
try {
return window.localStorage ? window.localStorage.getItem('mnote.page_ai.legacy_provider_mode') !== '1' : true;
} catch (_error) {
return true;
}
}
function isPageAiBoardFirstEnabled() {
return typeof pageAiBoardFirstEnabled === 'function' ? pageAiBoardFirstEnabled() : pageAiLocalBoardFirstEnabled();
}
function pageAiTargetLabel(target) {
var workspacePath = target && target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
var kind = String(target && (target.resourceKind || target.editorKind) || workspacePath.resourceKind || '').trim();
@@ -430,6 +452,8 @@ export function createSidebarPageAiRenderRuntime(context) {
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
if (drawer.getAttribute('data-page-ai-opencode-host') === 'true') return;
var boardFirst = isPageAiBoardFirstEnabled();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
var provider = button.getAttribute('data-page-ai-provider') || 'hermes';
@@ -439,12 +463,14 @@ export function createSidebarPageAiRenderRuntime(context) {
});
var providerNode = drawer.querySelector('.wolai-page-ai-subtitle span:first-child');
if (providerNode instanceof HTMLElement) {
providerNode.textContent = pageAiAgentRecord(pageAiCurrentAgentId()).label;
providerNode.textContent = boardFirst ? 'Agent Board' : pageAiAgentRecord(pageAiCurrentAgentId()).label;
}
}
function renderPageAiControls() {
var drawer = ensurePageAiDrawer();
if (drawer.getAttribute('data-page-ai-opencode-host') === 'true') return;
var boardFirst = isPageAiBoardFirstEnabled();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeAgentId = pageAiCurrentAgentId();
var activeProfile = pageAiCurrentProfile();
@@ -456,19 +482,52 @@ export function createSidebarPageAiRenderRuntime(context) {
documentRef.documentElement.setAttribute('data-mnote-page-ai-agent-id', activeAgentId);
var agentButton = drawer.querySelector('[data-page-ai-agent-button]');
var agentPopoverId = 'mnote-page-ai-agent-popover';
var activeAgentLabel = pageAiCurrentAgentSelectionLabel();
var activeAgentLabel = boardFirst && typeof pageAiBoardWorkerLabel === 'function'
? pageAiBoardWorkerLabel() + ' / ' + (typeof pageAiBoardModelLabel === 'function' ? pageAiBoardModelLabel() : '默认')
: pageAiCurrentAgentSelectionLabel();
if (agentButton instanceof HTMLElement) {
agentButton.textContent = 'AI';
agentButton.textContent = boardFirst ? 'Board' : 'AI';
agentButton.setAttribute('aria-expanded', pageUiState.pageAiAgentPopoverOpen ? 'true' : 'false');
agentButton.setAttribute('aria-controls', agentPopoverId);
agentButton.setAttribute('data-page-ai-agent-summary', activeAgentLabel);
agentButton.setAttribute('aria-label', 'Agent' + activeAgentLabel);
agentButton.setAttribute('title', 'Agent' + activeAgentLabel);
agentButton.setAttribute('aria-label', (boardFirst ? '运行控制:' : 'Agent') + activeAgentLabel);
agentButton.setAttribute('title', (boardFirst ? '运行控制:' : 'Agent') + activeAgentLabel);
}
var agentPopover = drawer.querySelector('[data-page-ai-agent-popover]');
if (agentPopover instanceof HTMLElement) {
agentPopover.id = agentPopoverId;
agentPopover.hidden = !pageUiState.pageAiAgentPopoverOpen;
if (boardFirst) {
var selectedWorkerId = typeof pageAiBoardSelectedWorkerId === 'function' ? pageAiBoardSelectedWorkerId() : 'mnote-page-ai-zcode';
var selectedWorkflowId = typeof pageAiBoardSelectedWorkflowId === 'function' ? pageAiBoardSelectedWorkflowId() : 'builtin-mnote-page-ai-chat';
var workerOptions = typeof pageAiBoardWorkerOptions === 'function' ? pageAiBoardWorkerOptions() : [];
var workflowOptions = typeof pageAiBoardWorkflowOptions === 'function' ? pageAiBoardWorkflowOptions() : [];
var modelOptions = typeof pageAiBoardModelOptions === 'function' ? pageAiBoardModelOptions(selectedWorkerId) : [];
var selectedModelOverride = typeof pageAiBoardSelectedModelOverride === 'function' ? pageAiBoardSelectedModelOverride() : 'zcode-default';
var workerSelect = workerOptions.length > 1 ? '<label class="wolai-page-ai-profile-select"><span>Worker</span><select data-page-ai-board-worker>' + workerOptions.map(function(worker) {
var id = String(worker.id || '').trim();
var label = String(worker.name || worker.id || '').trim();
var detail = [worker.agentType, worker.agentModel].filter(Boolean).join(' · ');
return '<option value="' + escapeHtml(id) + '"' + (id === selectedWorkerId ? ' selected' : '') + '>' + escapeHtml([label, detail].filter(Boolean).join(' · ')) + '</option>';
}).join('') + '</select></label>' : '<div class="wolai-page-ai-profile-select"><span>Worker</span><strong>' + escapeHtml(typeof pageAiBoardWorkerLabel === 'function' ? pageAiBoardWorkerLabel() : 'MNote 页面 AI · ZCode') + '</strong></div>';
var workflowSelect = workflowOptions.length > 1 ? '<label class="wolai-page-ai-profile-select"><span>Workflow</span><select data-page-ai-board-workflow>' + workflowOptions.map(function(workflow) {
var id = String(workflow.id || '').trim();
var label = String(workflow.name || workflow.id || '').trim();
return '<option value="' + escapeHtml(id) + '"' + (id === selectedWorkflowId ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('') + '</select></label>' : '<div class="wolai-page-ai-profile-select"><span>Workflow</span><strong>' + escapeHtml(typeof pageAiBoardWorkflowLabel === 'function' ? pageAiBoardWorkflowLabel() : 'MNote 页面 AI') + '</strong></div>';
var modelSelect = '<label class="wolai-page-ai-profile-select"><span>模型</span><select data-page-ai-board-model>' + modelOptions.map(function(option) {
var id = String(option.id || '').trim();
var label = String(option.label || option.id || '').trim();
return '<option value="' + escapeHtml(id) + '"' + (id === selectedModelOverride ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('') + '</select></label>';
agentPopover.innerHTML = '' +
'<div class="wolai-page-ai-context-popover-head">' +
'<strong>Agent Board</strong>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="close-agent-popover">完成</button>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' + workerSelect + modelSelect + workflowSelect + '</div>' +
'<div class="wolai-page-ai-empty">MNote 只发送 capability/envelopeprovider、执行和 workflow 由 Agent Board 负责。</div>';
} else {
var chatOnlyOptions = pageAiChatOnlyProfileEntries().map(function(profile) {
var spec = pageAiChatOnlyProfileSpec(profile);
var label = profile.menuLabel || (spec && spec.label) || pageAiProfileDisplayLabel(profile, '');
@@ -509,11 +568,12 @@ export function createSidebarPageAiRenderRuntime(context) {
'</button>' +
'</div>' +
'</div>';
}
}
var agentChip = drawer.querySelector('[data-page-ai-agent-chip]');
if (agentChip instanceof HTMLElement) {
agentChip.textContent = activeAgentLabel;
agentChip.setAttribute('title', '当前 Agent' + activeAgentLabel);
agentChip.textContent = boardFirst && typeof pageAiBoardWorkerLabel === 'function' ? pageAiBoardWorkerLabel() + ' / ' + (typeof pageAiBoardModelLabel === 'function' ? pageAiBoardModelLabel() : '默认') : activeAgentLabel;
agentChip.setAttribute('title', boardFirst ? '当前 worker/model' + agentChip.textContent : '当前 Agent' + activeAgentLabel);
}
var contextButton = drawer.querySelector('[data-page-ai-context-button]');
var contextPopoverId = 'mnote-page-ai-context-popover';
@@ -676,7 +736,10 @@ export function createSidebarPageAiRenderRuntime(context) {
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
}
drawer.querySelectorAll('[data-page-ai-runtime-badge]').forEach(function(runtimeBadge) {
if (runtimeBadge instanceof HTMLElement) runtimeBadge.textContent = pageAiRuntimeStatusLabel();
if (runtimeBadge instanceof HTMLElement) {
var workflowLabel = boardFirst && typeof pageAiBoardWorkflowLabel === 'function' ? pageAiBoardWorkflowLabel() : '';
runtimeBadge.textContent = boardFirst ? 'Agent Board · ' + workflowLabel : pageAiRuntimeStatusLabel();
}
});
var runtimeActive = drawer.querySelector('[data-page-ai-runtime-active-run]');
if (runtimeActive instanceof HTMLElement) {
@@ -684,7 +747,7 @@ export function createSidebarPageAiRenderRuntime(context) {
runtimeActive.textContent = pageAiRunStatusLabel(activeRunStatus);
}
var runtimeAcp = drawer.querySelector('[data-page-ai-runtime-acp]');
if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = pageAiRuntimeStatusValue('acp.acpSessionId', '无 ACP session');
if (runtimeAcp instanceof HTMLElement) runtimeAcp.textContent = boardFirst ? 'provider 由 Board worker 决定' : pageAiRuntimeStatusValue('acp.acpSessionId', '无 ACP session');
var runtimeDetail = drawer.querySelector('[data-page-ai-runtime-detail]');
if (runtimeDetail instanceof HTMLElement) runtimeDetail.innerHTML = pageAiRuntimeDetailHtml();
var jobsList = drawer.querySelector('[data-page-ai-jobs-list]');
@@ -709,12 +772,12 @@ export function createSidebarPageAiRenderRuntime(context) {
}
var usageDetail = drawer.querySelector('[data-page-ai-usage-detail]');
if (usageDetail instanceof HTMLElement) usageDetail.innerHTML = pageAiUsageDetailHtml();
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;
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
drawer.querySelectorAll('[data-page-ai-action="stop-run"]').forEach(function(stopButton) {
if (!(stopButton instanceof HTMLButtonElement)) return;
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();
@@ -771,6 +834,19 @@ export function createSidebarPageAiRenderRuntime(context) {
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
}
});
if (boardFirst) {
var subtitle = drawer.querySelector('.wolai-page-ai-subtitle');
if (subtitle instanceof HTMLElement) {
var workerLabel = typeof pageAiBoardWorkerLabel === 'function' ? pageAiBoardWorkerLabel() : 'ZCode 默认 developer';
subtitle.innerHTML = '<span>Agent Board</span><span>' + escapeHtml(workerLabel) + '</span><span>local-first 文件编辑</span>';
}
drawer.querySelectorAll('[data-page-ai-tab="agent"], [data-page-ai-tab="reasonix-settings"], [data-page-ai-tab="hermes-settings"]').forEach(function(node) {
if (node instanceof HTMLElement) node.hidden = true;
});
drawer.querySelectorAll('[data-page-ai-panel="agent"], [data-page-ai-panel="reasonix-settings"], [data-page-ai-panel="hermes-settings"]').forEach(function(node) {
if (node instanceof HTMLElement) node.setAttribute('data-page-ai-legacy-provider-panel', 'true');
});
}
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
if (profileError instanceof HTMLElement) {
profileError.textContent = pageUiState.pageAiProfileError || '';
@@ -1068,8 +1144,8 @@ export function createSidebarPageAiRenderRuntime(context) {
'</div>' +
'</div>' +
'<div class="wolai-page-ai-header-actions">' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="new-session" aria-label="新建 AI 会话" title="新建 AI 会话"></button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="history" aria-label="历史会话" title="历史会话"></button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="new-session" aria-label="新建 AI 会话" title="新建 AI 会话"><span class="material-symbols-outlined" data-icon="add" aria-hidden="true"></span></button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="history" aria-label="历史会话" title="历史会话"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="skills" aria-label="能力" title="能力">' +
'<svg class="wolai-page-ai-icon-svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false">' +
'<path d="M12 3l1.35 4.15L17.5 8.5l-4.15 1.35L12 14l-1.35-4.15L6.5 8.5l4.15-1.35L12 3z" />' +
@@ -1077,8 +1153,8 @@ export function createSidebarPageAiRenderRuntime(context) {
'<path d="M5.5 14l.65 1.85L8 16.5l-1.85.65L5.5 19l-.65-1.85L3 16.5l1.85-.65L5.5 14z" />' +
'</svg>' +
'</button>' +
'<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>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置"><span class="material-symbols-outlined" data-icon="admin_panel_settings" aria-hidden="true"></span></button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-runtime-strip" data-page-ai-runtime-strip data-page-ai-tab="status" aria-label="打开 Page AI 状态">' +
@@ -1540,6 +1616,13 @@ export function createSidebarPageAiRenderRuntime(context) {
function pageAiRenderNonToolMessage(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
if (item.kind === 'status') {
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-status-message="true"' + (item.streaming ? ' data-page-ai-streaming="true"' : '') + '>' +
'<div class="wolai-page-ai-message-role">AI</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}
if (item.kind === 'thought') {
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-collapse-card data-page-ai-collapse-id="thought-single">' +
@@ -1602,10 +1685,14 @@ export function createSidebarPageAiRenderRuntime(context) {
}
if (pageAiLooksLikeReasonixPlanMessage(item)) return pageAiRenderReasonixPlanCard(item);
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
var detailButton = item.boardRunId
? '<div class="wolai-page-ai-message-actions"><button type="button" class="wolai-page-ai-ghost" data-page-ai-board-run-detail="' + escapeHtml(item.boardRunId) + '">Run detail</button>' + (item.retryPrompt ? '<button type="button" class="wolai-page-ai-ghost" data-page-ai-retry-message="' + escapeHtml(item.boardRunId) + '">重试</button>' : '') + '<button type="button" class="wolai-page-ai-ghost" data-page-ai-copy-message="' + escapeHtml(item.boardRunId) + '">复制</button></div>'
: '';
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">' + (item.role === 'assistant' ? renderPageAiMarkdown(item.content || '') : escapeHtml(item.content || '')) + '</div>' +
detailButton +
'</div>';
}
@@ -1669,6 +1756,38 @@ export function createSidebarPageAiRenderRuntime(context) {
return titleKey === previewKey || previewKey.indexOf(titleKey) === 0 || titleKey.indexOf(previewKey) === 0;
}
function pageAiBoardRunDetailHtml(runId) {
var current = typeof pageAiCurrentSession === 'function' ? pageAiCurrentSession() : null;
var detail = pageUiState.pageAiBoardRunDetails && pageUiState.pageAiBoardRunDetails[runId]
|| current && current.boardRuns && current.boardRuns[runId]
|| null;
if (!detail) return '<div class="wolai-page-ai-empty">暂无 run detail。</div>';
var changedFiles = pageAiNormalizeArray(detail.changedFiles).map(function(file) {
return '<li>' + escapeHtml(String(file.path || file || '')) + (file.status ? ' · ' + escapeHtml(file.status) : '') + '</li>';
}).join('');
var verification = pageAiNormalizeArray(detail.verification).map(function(item) {
return '<li>' + escapeHtml([item.command, item.status, item.output].filter(Boolean).join(' · ')) + '</li>';
}).join('');
var timeline = pageAiNormalizeArray(detail.timeline).map(function(event) {
return '<li>' + escapeHtml([event.type, event.message, event.timestamp].filter(Boolean).join(' · ')) + '</li>';
}).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-board-run-detail-card="true">' +
'<div class="wolai-page-ai-permission-card-header">' +
'<div class="wolai-page-ai-plan-card-title-wrap"><span class="wolai-page-ai-plan-card-kicker">Run detail</span><strong class="wolai-page-ai-plan-card-title">' + escapeHtml(detail.runId || runId) + '</strong></div>' +
'<span class="wolai-page-ai-permission-badge">' + escapeHtml(detail.status || '') + '</span>' +
'</div>' +
'<div class="wolai-page-ai-permission-summary">' +
'<div class="wolai-page-ai-permission-row"><span>Worker</span><strong>' + escapeHtml(detail.workerName || detail.workerPresetId || '') + '</strong></div>' +
'<div class="wolai-page-ai-permission-row"><span>Model</span><strong>' + escapeHtml(detail.modelLabel || detail.modelOverride || '') + '</strong></div>' +
'<div class="wolai-page-ai-permission-row"><span>Workflow</span><strong>' + escapeHtml(detail.workflowName || detail.workflowId || '') + '</strong></div>' +
'</div>' +
(changedFiles ? '<details class="wolai-page-ai-tool-details" open><summary><strong>changed files</strong><span>' + String(pageAiNormalizeArray(detail.changedFiles).length) + '</span></summary><ul>' + changedFiles + '</ul></details>' : '') +
(verification ? '<details class="wolai-page-ai-tool-details"><summary><strong>verification</strong><span>' + String(pageAiNormalizeArray(detail.verification).length) + '</span></summary><ul>' + verification + '</ul></details>' : '') +
(timeline ? '<details class="wolai-page-ai-tool-details"><summary><strong>timeline</strong><span>' + String(pageAiNormalizeArray(detail.timeline).length) + '</span></summary><ul>' + timeline + '</ul></details>' : '') +
'</div>';
}
function renderPageAiConversation() {
var drawer = ensurePageAiDrawer();
renderPageAiSuggestions();
@@ -1721,6 +1840,9 @@ export function createSidebarPageAiRenderRuntime(context) {
var previousScrollTop = conversation.scrollTop;
var openToolDetails = pageAiCaptureOpenToolDetails(conversation);
conversation.innerHTML = pageAiConversationRenderHtml(pageUiState.pageAiMessages);
if (pageUiState.pageAiBoardActiveDetailRunId) {
conversation.insertAdjacentHTML('beforeend', pageAiBoardRunDetailHtml(pageUiState.pageAiBoardActiveDetailRunId));
}
pageAiRestoreOpenToolDetails(conversation, openToolDetails);
if (shouldStickToBottom) {
conversation.scrollTop = conversation.scrollHeight;
File diff suppressed because it is too large Load Diff
@@ -82,6 +82,7 @@ export function createSidebarPageAiSessionRuntime(context) {
profile = profileId;
}
return {
schema: String(session && session.schema || '').trim(),
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '新会话',
agentId: agentId,
@@ -97,12 +98,18 @@ export function createSidebarPageAiSessionRuntime(context) {
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
acpSessionId: String(session && (session.acpSessionId || session.acp_session_id) || '').trim(),
runId: String(session && (session.runId || session.run_id) || '').trim(),
boardRunId: String(session && (session.boardRunId || session.board_run_id || session.runId || session.run_id) || '').trim(),
workflowId: String(session && (session.workflowId || session.workflow_id) || '').trim(),
workerPresetId: String(session && (session.workerPresetId || session.worker_preset_id) || '').trim(),
modelOverride: String(session && (session.modelOverride || session.model_override) || '').trim(),
receiptId: String(session && (session.receiptId || session.receipt_id) || '').trim(),
boardRuns: session && session.boardRuns && typeof session.boardRuns === 'object' ? session.boardRuns : {},
status: String(session && session.status || '').trim(),
runtimeMode: String(session && (session.runtimeMode || session.runtime_mode) || '').trim(),
replaySeen: Boolean(session && session.replaySeen),
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) : []
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300).map(function(message) { return Object.assign({}, message); }) : []
};
})
.sort(function(a, b) {
@@ -208,6 +215,15 @@ export function createSidebarPageAiSessionRuntime(context) {
var storageVersion = Number(parsed && parsed.version || 0);
if (storageVersion >= sessionStorageVersion && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
if (activeProfile) pageAiSetActiveProfile(activeProfile);
var storedSessions = pageAiNormalizeSessions(parsed && parsed.sessions);
if (storageVersion >= sessionStorageVersion && storedSessions.length) {
pageUiState.pageAiSessions = storedSessions;
var activeSessionId = String(parsed && parsed.activeSessionId || '').trim();
pageUiState.pageAiActiveSessionId = storedSessions.some(function(session) { return session.id === activeSessionId; }) ? activeSessionId : storedSessions[0].id;
var active = pageAiCurrentSession();
pageUiState.pageAiMessages = active && Array.isArray(active.messages) ? active.messages.slice() : [];
return;
}
} catch (_) {}
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
@@ -443,15 +459,16 @@ export function createSidebarPageAiSessionRuntime(context) {
}
function pageAiPersistSessions() {
pageAiSyncCurrentSessionMessages();
doc.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
doc.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
try {
pageAiSyncCurrentSessionMessages();
win.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
version: sessionStorageVersion,
activeSessionId: pageUiState.pageAiActiveSessionId,
activeProfileName: pageAiCurrentProfile(),
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
sessions: pageAiNormalizeArray(pageUiState.pageAiSessions).slice(0, 20)
}));
} catch (_) {}
}
@@ -603,6 +620,12 @@ export function createSidebarPageAiSessionRuntime(context) {
session.profileId = pageUiState.pageAiAcpRuntime === 'hermes' ? runProfile : '';
session.profile = runProfile;
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
if (String(session.source || '') === 'board') {
session.schema = session.schema || 'mnote.page_ai.session.v2';
session.workerPresetId = pageUiState.pageAiBoardWorkerId || session.workerPresetId || '';
session.workflowId = pageUiState.pageAiBoardWorkflowId || session.workflowId || '';
session.modelOverride = pageUiState.pageAiBoardModelOverride || session.modelOverride || '';
}
session.updatedAt = Date.now();
}
@@ -611,6 +634,14 @@ export function createSidebarPageAiSessionRuntime(context) {
if (!session) return;
pageUiState.pageAiActiveSessionId = session.id;
if (session.agentId) pageUiState.pageAiAgentId = pageAiNormalizeAgentId(session.agentId);
if (session.source === 'board') {
if (session.workerPresetId) pageUiState.pageAiBoardWorkerId = session.workerPresetId;
if (session.workflowId) pageUiState.pageAiBoardWorkflowId = session.workflowId;
if (session.modelOverride) pageUiState.pageAiBoardModelOverride = session.modelOverride;
if (session.boardRuns && typeof session.boardRuns === 'object') {
pageUiState.pageAiBoardRunDetails = Object.assign({}, pageUiState.pageAiBoardRunDetails || {}, session.boardRuns);
}
}
if (session.acpRuntime) pageUiState.pageAiAcpRuntime = session.acpRuntime;
if (session.profileId || session.profile) pageAiSetActiveProfile(session.profileId || session.profile);
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
@@ -464,7 +464,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<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>' +
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'<div class="wolai-page-history-list" data-page-history-list></div>' +
'</div>';
@@ -527,7 +527,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<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>' +
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'<div class="wolai-page-share-state">' +
'<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
@@ -658,7 +658,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-local-index-settings-panel" role="dialog" aria-modal="false" aria-label="索引设置">' +
'<div class="mnote-settings-panel-head">' +
'<strong>索引设置</strong>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭索引设置">×</button>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭索引设置"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'<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>' +
@@ -703,7 +703,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel" role="dialog" aria-modal="false" aria-label="资料库问答设置">' +
'<div class="mnote-settings-panel-head">' +
'<strong>资料库问答</strong>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭资料库问答设置">×</button>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭资料库问答设置"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="资料库设置子面板">' +
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources">资料源管理</button>' +
@@ -919,7 +919,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(localIndexStatusLabel(kind)) + '"></span>' +
'<input type="text" class="wolai-page-settings-index-path" data-local-index-range-input="true" data-local-index-value="' + escapeHtml(normalizedPath) + '" value="' + escapeHtml(displayPath) + '" spellcheck="false"' + (inputDisabled ? ' disabled' : '') + (isPersisted ? ' data-local-index-persisted="true" title="已保存的索引目录不能直接修改;删除后重新新增范围"' : '') + ' />' +
rootScopeHint +
'<button type="button" class="wolai-page-settings-index-remove" data-local-index-action="remove-path" data-local-index-path-index="' + String(index) + '"' + (disabled ? ' disabled' : '') + ' aria-label="删除索引范围">×</button>' +
'<button type="button" class="wolai-page-settings-index-remove" data-local-index-action="remove-path" data-local-index-path-index="' + String(index) + '"' + (disabled ? ' disabled' : '') + ' aria-label="删除索引范围"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>';
}).join('');
}
@@ -1132,7 +1132,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="mnote-knowledge-rag-source-input-row" data-knowledge-rag-source-input-row="true">' +
'<input type="text" class="wolai-page-settings-index-path" data-knowledge-rag-source-input="true" value="' + escapeHtml(value || '') + '" placeholder="文件或目录,例如 docs/book.pdf / docs" spellcheck="false" />' +
'<span class="mnote-knowledge-rag-input-status" data-knowledge-rag-input-status-label="true" hidden></span>' +
'<button type="button" class="wolai-page-settings-index-remove" data-knowledge-rag-action="remove-source" aria-label="删除资料来源">×</button>' +
'<button type="button" class="wolai-page-settings-index-remove" data-knowledge-rag-action="remove-source" aria-label="删除资料来源"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>';
}
@@ -99,10 +99,7 @@ fn mindmap_tools() -> Vec<Value> {
}
fn office_tools() -> Vec<Value> {
vec![
office_fetch_summary_tool(),
office_propose_changes_tool(),
]
vec![office_fetch_summary_tool(), office_propose_changes_tool()]
}
fn onlyoffice_tools() -> Vec<Value> {
@@ -160,8 +157,6 @@ fn artifact_tools() -> Vec<Value> {
]
}
fn annotate_tools_with_capabilities(tools: Vec<Value>) -> Vec<Value> {
tools
.into_iter()
+113 -1
View File
@@ -8,7 +8,6 @@ pub(crate) mod evidence;
mod gateway;
mod health;
mod hermes;
mod ui_debug;
mod hermes_client;
mod hermes_tools;
mod kernel;
@@ -25,6 +24,8 @@ mod mindmap_shell;
pub(crate) mod navigation_recent;
mod onlyoffice;
pub(crate) mod onlyoffice_bridge;
mod page_ai_board;
mod page_ai_opencode;
mod page_ai_workflow;
mod query_support;
mod resource_trash;
@@ -36,6 +37,7 @@ mod sse;
mod stream_support;
mod tree;
mod tree_view_state;
mod ui_debug;
pub(crate) mod ui_preferences;
pub(crate) mod web_shell;
mod ws;
@@ -162,6 +164,10 @@ pub fn build_router(state: AppState) -> Router {
)
.route("/pdf-preview", get(web_shell::pdf_preview_page))
.route("/api/pdfjs/{*asset_path}", get(web_shell::pdfjs_asset))
.route(
"/api/mnote-browser-runtime/mnote-ui-runtime.js",
get(web_shell::mnote_ui_runtime_asset),
)
.route(
"/api/mnote-browser-runtime/resource-open-runtime.js",
get(web_shell::resource_open_runtime_asset),
@@ -379,6 +385,112 @@ pub fn build_router(state: AppState) -> Router {
"/api/ai/agent-profiles",
get(hermes_client::list_agent_profiles),
)
.route(
"/api/page-ai/opencode/status",
get(page_ai_opencode::status),
)
.route(
"/api/page-ai/opencode/session",
post(page_ai_opencode::bind_session),
)
.route(
"/api/page-ai/opencode/sessions",
get(page_ai_opencode::sessions),
)
.route("/api/page-ai/opencode/abort", post(page_ai_opencode::abort))
.route("/api/page-ai/opencode/todo", get(page_ai_opencode::todo))
.route("/api/page-ai/opencode/diff", get(page_ai_opencode::diff))
.route(
"/api/page-ai/opencode/messages",
get(page_ai_opencode::messages),
)
.route(
"/api/page-ai/opencode/prompt",
post(page_ai_opencode::prompt),
)
.route(
"/api/page-ai/opencode/permissions",
get(page_ai_opencode::permissions),
)
.route(
"/api/page-ai/opencode/permission/reply",
post(page_ai_opencode::reply_permission),
)
.route(
"/api/page-ai/opencode/events",
get(page_ai_opencode::events),
)
.route("/page-ai/opencode", any(page_ai_opencode::proxy_root))
.route(
"/page-ai/opencode/assets/{*path}",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/favicon-96x96-v3.png",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/favicon-v3.svg",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/favicon-v3.ico",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/apple-touch-icon-v3.png",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/site.webmanifest",
any(page_ai_opencode::proxy_assets),
)
.route(
"/page-ai/opencode/social-share.png",
any(page_ai_opencode::proxy_assets),
)
.route("/page-ai/opencode/{*path}", any(page_ai_opencode::proxy))
.route("/assets/{*path}", any(page_ai_opencode::proxy_assets))
.route("/global/{*path}", any(page_ai_opencode::proxy_assets))
.route("/favicon-96x96-v3.png", any(page_ai_opencode::proxy_current_path))
.route("/favicon-v3.svg", any(page_ai_opencode::proxy_current_path))
.route("/favicon-v3.ico", any(page_ai_opencode::proxy_current_path))
.route("/apple-touch-icon-v3.png", any(page_ai_opencode::proxy_current_path))
.route("/site.webmanifest", any(page_ai_opencode::proxy_current_path))
.route("/social-share.png", any(page_ai_opencode::proxy_current_path))
.route("/provider", any(page_ai_opencode::proxy_current_path))
.route("/path", any(page_ai_opencode::proxy_current_path))
.route("/project", any(page_ai_opencode::proxy_current_path))
.route("/project/{*path}", any(page_ai_opencode::proxy_current_path))
.route("/lsp", any(page_ai_opencode::proxy_current_path))
.route("/command", any(page_ai_opencode::proxy_current_path))
.route("/mcp", any(page_ai_opencode::proxy_current_path))
.route("/agent", any(page_ai_opencode::proxy_current_path))
.route("/config", any(page_ai_opencode::proxy_current_path))
.route("/vcs", any(page_ai_opencode::proxy_current_path))
.route("/permission", any(page_ai_opencode::proxy_current_path))
.route("/question", any(page_ai_opencode::proxy_current_path))
.route("/event", any(page_ai_opencode::proxy_current_path))
.route("/session", any(page_ai_opencode::proxy_current_path))
.route("/session/{*path}", any(page_ai_opencode::proxy_current_path))
.route("/new-session", any(page_ai_opencode::proxy_current_path))
.route("/{opencode_dir}/session", any(page_ai_opencode::proxy_current_path))
.route("/{opencode_dir}/session/{*path}", any(page_ai_opencode::proxy_current_path))
.route("/api/page-ai/board/status", get(page_ai_board::status))
.route("/api/page-ai/board/workers", get(page_ai_board::workers))
.route(
"/api/page-ai/board/workflows",
get(page_ai_board::workflows),
)
.route("/api/page-ai/board/runs", post(page_ai_board::create_run))
.route(
"/api/page-ai/board/runs/{run_id}",
get(page_ai_board::get_run),
)
.route(
"/api/page-ai/board/runs/{run_id}/cancel",
post(page_ai_board::cancel_run),
)
.route(
"/api/sidebar/shortcuts",
get(sidebar_shortcuts::list_shortcuts).post(sidebar_shortcuts::upsert_shortcut),
@@ -0,0 +1,334 @@
use crate::app::AppState;
use crate::context::RequestContext;
use crate::error::WebError;
use axum::extract::{Path, State};
use axum::{Extension, Json};
use reqwest::Method;
use serde_json::{json, Value};
use std::time::Duration;
const DEFAULT_BOARD_BASE_URL: &str = "http://127.0.0.1:3901/api";
const DEFAULT_BOARD_PROJECT_ID: &str = "51067826-50c7-4869-a8cd-5496f08ca8e6";
const DEFAULT_PAGE_AI_WORKFLOW_ID: &str = "builtin-mnote-page-ai-chat";
const DEFAULT_PAGE_AI_WORKER_PRESET_ID: &str = "mnote-page-ai-zcode";
const DEFAULT_PAGE_AI_MODEL_OVERRIDE: &str = "zcode-default";
fn page_ai_worker_options() -> Value {
json!([{
"id": DEFAULT_PAGE_AI_WORKER_PRESET_ID,
"workerPresetId": DEFAULT_PAGE_AI_WORKER_PRESET_ID,
"name": "MNote 页面 AI · ZCode",
"surface": "mnote-page-ai",
"role": "developer",
"agentType": "zcode",
"capabilities": ["text", "repo-edit", "terminal", "mnote-capability-envelope", "local-markdown-edit"],
"modelOptions": page_ai_model_options(),
}])
}
fn page_ai_workflow_options() -> Value {
json!([{
"id": DEFAULT_PAGE_AI_WORKFLOW_ID,
"workflowId": DEFAULT_PAGE_AI_WORKFLOW_ID,
"name": "MNote 页面 AI",
"surface": "mnote-page-ai",
"stages": ["answer"],
}])
}
fn page_ai_model_options() -> Value {
json!([
{ "id": "zcode-default", "label": "默认", "default": true },
{ "id": "zcode-fast", "label": "快速" },
{ "id": "zcode-strong", "label": "强力" },
])
}
fn validate_page_ai_route(
context: &RequestContext,
workflow_id: &str,
worker_preset_id: &str,
model_override: &str,
) -> Result<(), WebError> {
if workflow_id != DEFAULT_PAGE_AI_WORKFLOW_ID
|| worker_preset_id != DEFAULT_PAGE_AI_WORKER_PRESET_ID
{
return Err(WebError::bad_request_code(
"page_ai_board_route_not_allowed",
"Page AI 只能使用 mnote-page-ai 白名单 worker/workflow",
)
.with_context(context));
}
let allowed_models = ["zcode-default", "zcode-fast", "zcode-strong"];
if !allowed_models.contains(&model_override) {
return Err(WebError::bad_request_code(
"page_ai_board_model_not_allowed",
"Page AI 只能使用当前 MNote worker 允许的模型档位",
)
.with_context(context));
}
Ok(())
}
fn ensure_authenticated(context: &RequestContext) -> Result<(), WebError> {
let has_actor = context.auth.actor_id.trim() != "anonymous";
if has_actor || context.auth.authorization.is_some() || context.auth.cookie_header.is_some() {
return Ok(());
}
Err(WebError::new(
axum::http::StatusCode::UNAUTHORIZED,
"page_ai_board_unauthorized",
"页面 AI Agent Board bridge 需要登录后访问",
)
.with_context(context))
}
fn board_base_url() -> String {
std::env::var("MNOTE_AGENT_BOARD_API_BASE")
.or_else(|_| {
std::env::var("MNOTE_AGENT_BOARD_BASE_URL")
.map(|value| format!("{}/api", value.trim_end_matches('/')))
})
.unwrap_or_else(|_| DEFAULT_BOARD_BASE_URL.to_string())
.trim_end_matches('/')
.to_string()
}
fn default_project_id() -> String {
std::env::var("MNOTE_AGENT_BOARD_PROJECT_ID")
.ok()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_BOARD_PROJECT_ID.to_string())
}
async fn board_request(
context: &RequestContext,
method: Method,
path: &str,
body: Option<Value>,
) -> Result<Value, WebError> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|error| {
WebError::internal(format!("Agent Board client 构造失败: {error}"))
.with_context(context)
})?;
let url = format!("{}{}", board_base_url(), path);
let mut request = client
.request(method, &url)
.header("accept", "application/json");
if let Some(body) = body {
request = request.json(&body);
}
let response = request.send().await.map_err(|error| {
WebError::bad_gateway_code(
"page_ai_board_unreachable",
format!("无法连接 Agent Board: {error}"),
)
.with_context(context)
})?;
let status = response.status();
let payload = response.json::<Value>().await.unwrap_or_else(|_| json!({}));
if !status.is_success() {
return Err(WebError::bad_gateway_code(
"page_ai_board_error",
format!("Agent Board 返回 HTTP {status}: {payload}"),
)
.with_context(context)
.with_details(payload));
}
Ok(payload)
}
pub async fn status(
Extension(context): Extension<RequestContext>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let payload = board_request(&context, Method::GET, "/health", None).await?;
Ok(Json(json!({
"ok": true,
"schema": "mnote.page_ai_board_status.v1",
"baseUrl": board_base_url(),
"projectId": default_project_id(),
"board": payload,
})))
}
pub async fn workers(
Extension(context): Extension<RequestContext>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let project_id = default_project_id();
let payload = board_request(
&context,
Method::GET,
&format!("/workers/catalog?projectId={project_id}"),
None,
)
.await
.unwrap_or_else(|_| json!(null));
Ok(Json(json!({
"ok": true,
"schema": "agent_board.page_ai_route.v2",
"surface": "mnote-page-ai",
"projectId": project_id,
"workerPresetId": DEFAULT_PAGE_AI_WORKER_PRESET_ID,
"workerName": "MNote 页面 AI · ZCode",
"allowedWorkerPresetIds": [DEFAULT_PAGE_AI_WORKER_PRESET_ID],
"modelOverride": DEFAULT_PAGE_AI_MODEL_OVERRIDE,
"modelOptions": page_ai_model_options(),
"workers": page_ai_worker_options(),
"boardCatalog": payload,
})))
}
pub async fn workflows(
Extension(context): Extension<RequestContext>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let project_id = default_project_id();
let payload = board_request(
&context,
Method::GET,
&format!("/workflow-presets?projectId={project_id}"),
None,
)
.await
.unwrap_or_else(|_| json!(null));
Ok(Json(json!({
"ok": true,
"schema": "agent_board.page_ai_route.v2",
"surface": "mnote-page-ai",
"projectId": project_id,
"workflowId": DEFAULT_PAGE_AI_WORKFLOW_ID,
"workflowName": "MNote 页面 AI",
"allowedWorkflowIds": [DEFAULT_PAGE_AI_WORKFLOW_ID],
"requiresConfirmation": false,
"requires": {
"filesystem": true,
"write": false,
"browser": false,
"vision": false,
},
"stages": ["answer"],
"workflows": page_ai_workflow_options(),
"boardCatalog": payload,
})))
}
pub async fn create_run(
State(_state): State<AppState>,
Extension(context): Extension<RequestContext>,
Json(mut body): Json<Value>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let project_id = body
.get("projectId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.unwrap_or_else(default_project_id);
let envelope = body.get("envelope").cloned().unwrap_or_else(|| json!({}));
let user_message = body
.get("message")
.and_then(Value::as_str)
.unwrap_or("请处理当前页面任务")
.trim();
let workflow_id = body
.get("workflowId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_PAGE_AI_WORKFLOW_ID)
.to_string();
let worker_preset_id = body
.get("workerPresetId")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_PAGE_AI_WORKER_PRESET_ID)
.to_string();
let model_override = body
.get("modelOverride")
.and_then(Value::as_str)
.or_else(|| envelope.get("modelOverride").and_then(Value::as_str))
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(DEFAULT_PAGE_AI_MODEL_OVERRIDE)
.to_string();
validate_page_ai_route(&context, &workflow_id, &worker_preset_id, &model_override)?;
let board_message = format!(
"你正在处理 MNote Page AI 发来的任务。你的最终回复会直接显示在页面 AI 对话里。\n\n用户请求:\n{user_message}\n\nMNote Page AI envelope\n```json\n{}\n```\n\n要求:\n1. 只读问答要像普通页面 AI 一样直接回答用户,不要输出 Board 任务报告。\n2. 如果任务要求编辑页面,只修改 envelope.primaryTarget 指向的真实文件,不要调用 MNote 内部页面写入接口。\n3. 你的源头最终回答必须是自然语言 final answer;同时在结构化 receipt.finalAnswer/changedFiles/verification/remaining 中写入运行记录。\n4. 为兼容旧运行器,<task-summary> 可以包含 ## FINAL_ANSWER 段,但不要把 Completed/Comments/Remaining 当作用户主回答。",
serde_json::to_string_pretty(&envelope).unwrap_or_else(|_| "{}".to_string())
);
body["projectId"] = Value::String(project_id.clone());
body["message"] = Value::String(board_message);
body["envelope"] = envelope;
body["surface"] = Value::String("mnote-page-ai".into());
body["workflowId"] = Value::String(workflow_id.clone());
body["workerPresetId"] = Value::String(worker_preset_id.clone());
body["modelOverride"] = Value::String(model_override.clone());
if body.get("autoRun").is_none() {
body["autoRun"] = Value::Bool(true);
}
let payload = board_request(&context, Method::POST, "/workflow-runs", Some(body)).await?;
Ok(Json(json!({
"ok": true,
"schema": "mnote.page_ai_board_run.v1",
"surface": "mnote-page-ai",
"projectId": project_id,
"workflowId": workflow_id,
"workerPresetId": worker_preset_id,
"modelOverride": model_override,
"board": payload,
})))
}
pub async fn get_run(
Extension(context): Extension<RequestContext>,
Path(run_id): Path<String>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let details = board_request(
&context,
Method::GET,
&format!("/workflow-runs/{run_id}"),
None,
)
.await?;
let receipt = board_request(
&context,
Method::GET,
&format!("/workflow-runs/{run_id}/receipt"),
None,
)
.await
.unwrap_or_else(|_| json!(null));
Ok(Json(json!({
"ok": true,
"schema": "mnote.page_ai_board_run_status.v1",
"runId": run_id,
"board": details,
"receipt": receipt,
})))
}
pub async fn cancel_run(
Extension(context): Extension<RequestContext>,
Path(run_id): Path<String>,
) -> Result<Json<Value>, WebError> {
ensure_authenticated(&context)?;
let payload = board_request(
&context,
Method::POST,
&format!("/workflow-runs/{run_id}/cancel"),
Some(json!({})),
)
.await?;
Ok(Json(
json!({ "ok": true, "runId": run_id, "board": payload }),
))
}
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -19,10 +19,9 @@ pub async fn components_shell(
Extension(context): Extension<RequestContext>,
) -> Result<Response, WebError> {
let workspace_id = "default";
let sidebar_tree_html =
load_sidebar_tree_html(state.config(), &context, workspace_id, None)
.await
.unwrap_or_default();
let sidebar_tree_html = load_sidebar_tree_html(state.config(), &context, workspace_id, None)
.await
.unwrap_or_default();
let body_content = crate::ssr::render_view(leptos::view! {
<UiDebugComponentsPage
@@ -2428,6 +2428,20 @@ pub async fn resource_open_runtime_asset() -> Response {
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn mnote_ui_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/mnote-ui-runtime.js");
Response::builder()
.status(StatusCode::OK)
.header(
header::CONTENT_TYPE,
"application/javascript; charset=utf-8",
)
.header(header::CACHE_CONTROL, runtime_asset_cache_control())
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
.body(browser_runtime_js_body(JS))
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn local_upload_runtime_asset() -> Response {
const JS: &str = include_str!("../../browser/local-upload-runtime.js");
Response::builder()
@@ -163,6 +163,7 @@ pub fn PageLayout(
<div hidden data-testid="mnote-admin-access-policy-template-admin" inner_html={admin_access_policy_template}></div>
<div hidden data-testid="mnote-admin-access-policy-template-user" inner_html={user_access_policy_template}></div>
<script inner_html={crate::ssr::pages::admin::ADMIN_POLICY_SCRIPT.to_string()}></script>
<script type="module" src={browser_runtime_src("mnote-ui-runtime.js")}></script>
<script type="module" src={browser_runtime_src("resource-open-runtime.js")}></script>
<script type="module" src={browser_runtime_src("local-upload-runtime.js")}></script>
<script type="module" src={browser_runtime_src("filetree-runtime.js")}></script>
@@ -206,6 +207,7 @@ pub fn PageLayout(
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" data-state="closed" aria-haspopup="dialog" aria-expanded="false" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
</div>
</div>
<div id="mnote-portal-root" data-mnote-portal-root="true"></div>
</div>
}
}
@@ -235,6 +237,7 @@ mod tests {
// resume/journal contract checks inspect the actual shipped JS.
const SIDEBAR_PAGE_AI_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-runtime.js");
const MNOTE_UI_RUNTIME_JS: &str = include_str!("../../../browser/mnote-ui-runtime.js");
const SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS: &str =
include_str!("../../../browser/sidebar-page-ai-markdown-runtime.js");
const SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS: &str =
@@ -418,6 +421,15 @@ mod tests {
assert!(!html.contains(r#"data-testid="wolai-public-state">全网公开"#));
assert!(html.contains(r#"data-mnote-action="toggle-sidebar-shortcut""#));
assert!(html.contains(r#"data-mnote-shortcut-kind="page""#));
assert!(html.contains(r#"id="mnote-portal-root""#));
assert!(html.contains("/api/mnote-browser-runtime/mnote-ui-runtime.js"));
}
#[test]
fn mnote_ui_runtime_exposes_toast_api() {
assert!(MNOTE_UI_RUNTIME_JS.contains("window.mnote.toast"));
assert!(MNOTE_UI_RUNTIME_JS.contains("data-mnote-toast-region"));
assert!(MNOTE_UI_RUNTIME_JS.contains("mnote:toast"));
}
#[test]
@@ -445,6 +457,10 @@ mod tests {
html.contains("/api/mnote-browser-runtime/local-folder-event-bus-runtime.js?devHot="),
"dev:hot 下 local-folder event bus URL 必须带 cache buster,避免复用旧连接编排逻辑"
);
assert!(
html.contains("/api/mnote-browser-runtime/mnote-ui-runtime.js?devHot="),
"dev:hot 下 UI runtime URL 必须带 cache buster,避免 toast/portal 使用旧版本"
);
}
#[test]
+20
View File
@@ -13,6 +13,7 @@
/// - `styles/base.css`: 基础重置、排版、SVG 图标、滚动条、侧栏/顶栏布局
/// - `styles/pages/home.css`, `shells.css`, `auth.css`: 页面级样式
/// - `styles/components/main.css`: 核心组件样式(编辑器、侧栏、树、附件等)
/// - `styles/components/toast.css`: 全局 toast 与 portal 反馈样式
/// - `styles/components/search.css`: 搜索弹窗组件
/// - `styles/components/page-ai.css`: Page AI 仪表盘组件
/// - `styles/components/ui-debug.css`: UI Debug 组件矩阵
@@ -29,6 +30,8 @@ pub const MNOTE_CSS: &str = concat!(
"\n",
include_str!("styles/components/main.css"),
"\n",
include_str!("styles/components/toast.css"),
"\n",
include_str!("styles/components/search.css"),
"\n",
include_str!("styles/components/page-ai.css"),
@@ -140,6 +143,23 @@ mod tests {
assert!(MNOTE_CSS.contains(".mnote-tree-context-menu"));
}
#[test]
fn mnote_css_contains_ui_foundation_tokens_and_toast() {
assert!(MNOTE_CSS.contains("--mnote-btn-primary-bg"));
assert!(MNOTE_CSS.contains("--mnote-font-size-base"));
assert!(MNOTE_CSS.contains("--wolai-state-focus-ring"));
assert!(MNOTE_CSS.contains(".mnote-toast-region"));
assert!(MNOTE_CSS.contains(".mnote-toast--success"));
}
#[test]
fn mnote_css_does_not_hide_all_closed_state_triggers() {
assert!(!MNOTE_CSS.contains("[data-state=\"closed\"] { display: none"));
assert!(!MNOTE_CSS.contains("[data-state=\"open\"] { display: block"));
assert!(MNOTE_CSS.contains(".wolai-page-ai-drawer[data-state=\"closed\"]"));
assert!(MNOTE_CSS.contains(".wolai-page-settings-popover[data-state=\"closed\"]"));
}
#[test]
fn sidebar_filetree_icons_use_css_masks_instead_of_text_glyphs() {
assert!(MNOTE_CSS.contains("--mnote-filetree-icon-file"));
@@ -819,11 +819,6 @@ html[data-mnote-sidebar-resizing="true"] .mnote-sidebar-resizer::before {
overflow: hidden;
}
/* ===== data-state 通用规则 ===== */
/* popover / dropdown / dialog 打开/关闭状态 */
[data-state="open"] { display: block; }
[data-state="closed"] { display: none !important; }
/* dialog/modal 状态 */
.mnote-profile-dialog[data-state="open"],
.ui-debug-dialog-overlay[data-state="open"],
@@ -2214,3 +2214,298 @@ button.wolai-page-ai-history-main span {
}
}
.wolai-page-ai-drawer[data-page-ai-opencode-host="true"] .wolai-page-ai-panel {
display: flex;
flex-direction: column;
gap: 0;
overflow: hidden;
}
.wolai-page-ai-opencode-header {
flex: 0 0 auto;
}
.wolai-page-ai-opencode-chrome {
display: flex;
flex: 0 0 auto;
flex-direction: column;
gap: 6px;
padding: 8px 12px 10px;
border-bottom: 1px solid rgba(27, 28, 28, 0.08);
background: rgba(247, 247, 245, 0.92);
}
.wolai-page-ai-opencode-row {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 6px;
align-items: baseline;
font-size: 12px;
color: rgba(27, 28, 28, 0.58);
}
.wolai-page-ai-opencode-row strong,
.wolai-page-ai-opencode-row code {
min-width: 0;
overflow: hidden;
color: rgba(27, 28, 28, 0.86);
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-opencode-badges,
.wolai-page-ai-opencode-files {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.wolai-page-ai-opencode-badges span,
.wolai-page-ai-opencode-empty,
.wolai-page-ai-opencode-chip {
display: inline-flex;
align-items: center;
max-width: 100%;
min-height: 24px;
padding: 3px 8px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 999px;
background: #fff;
color: rgba(27, 28, 28, 0.68);
font: inherit;
font-size: 12px;
}
.wolai-page-ai-opencode-chip {
cursor: pointer;
}
.wolai-page-ai-opencode-chip:hover {
border-color: rgba(35, 131, 226, 0.32);
color: var(--wolai-accent, #2383e2);
}
.wolai-page-ai-opencode-chip span {
margin-left: 6px;
color: rgba(27, 28, 28, 0.45);
}
.wolai-page-ai-opencode-frame-wrap {
position: relative;
display: flex;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
background: #fff;
}
.wolai-page-ai-opencode-iframe {
flex: 1 1 auto;
width: 100%;
min-height: 0;
border: 0;
background: #fff;
}
.wolai-page-ai-opencode-iframe[hidden] {
display: none;
}
.wolai-page-ai-opencode-iframe-fallback {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
color: rgba(27, 28, 28, 0.62);
text-align: center;
background: #fff;
}
.wolai-page-ai-opencode-iframe-fallback[hidden] {
display: none;
}
.wolai-page-ai-opencode-chat {
display: flex;
flex: 1 1 auto;
min-height: 0;
flex-direction: column;
background: #fff;
}
.wolai-page-ai-opencode-messages {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
padding: 12px;
}
.wolai-page-ai-opencode-message {
margin: 0 0 10px;
padding: 10px 12px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 12px;
background: rgba(247, 247, 245, 0.9);
}
.wolai-page-ai-opencode-message[data-role="assistant"] {
background: #fff;
}
.wolai-page-ai-opencode-message-role {
margin-bottom: 4px;
color: rgba(27, 28, 28, 0.52);
font-size: 11px;
font-weight: 600;
}
.wolai-page-ai-opencode-message-body {
color: rgba(27, 28, 28, 0.88);
font-size: 13px;
line-height: 1.55;
}
.wolai-page-ai-opencode-composer {
display: flex;
gap: 8px;
padding: 10px 12px 12px;
border-top: 1px solid rgba(27, 28, 28, 0.08);
}
.wolai-page-ai-opencode-composer textarea {
flex: 1 1 auto;
min-height: 42px;
resize: vertical;
border: 1px solid rgba(27, 28, 28, 0.14);
border-radius: 10px;
padding: 8px 10px;
font: inherit;
}
.wolai-page-ai-opencode-send,
.wolai-page-ai-opencode-permission button {
border: 0;
border-radius: 10px;
padding: 0 12px;
background: #1f6feb;
color: #fff;
font-weight: 600;
}
.wolai-page-ai-opencode-permissions {
display: flex;
flex-direction: column;
gap: 6px;
padding: 0 12px;
}
.wolai-page-ai-opencode-permission {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
gap: 6px;
align-items: center;
padding: 8px;
border: 1px solid rgba(227, 115, 14, 0.24);
border-radius: 10px;
background: rgba(255, 247, 237, 0.92);
font-size: 12px;
}
.wolai-page-ai-opencode-permission span {
overflow: hidden;
color: rgba(27, 28, 28, 0.62);
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-opencode-sessions-wrap {
font-size: 12px;
}
.wolai-page-ai-opencode-sessions {
display: grid;
gap: 4px;
margin-top: 6px;
}
.wolai-page-ai-opencode-session-row {
display: grid;
min-width: 0;
gap: 2px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 8px;
padding: 6px 8px;
background: rgba(255, 255, 255, 0.72);
color: inherit;
text-align: left;
}
.wolai-page-ai-opencode-session-row[data-active="true"] {
border-color: rgba(31, 111, 235, 0.38);
background: rgba(31, 111, 235, 0.08);
}
.wolai-page-ai-opencode-session-row span,
.wolai-page-ai-opencode-message-role span {
overflow: hidden;
color: rgba(27, 28, 28, 0.52);
text-overflow: ellipsis;
white-space: nowrap;
}
.wolai-page-ai-opencode-message-role {
display: flex;
justify-content: space-between;
gap: 8px;
}
.wolai-page-ai-opencode-part {
margin-top: 8px;
border: 1px solid rgba(27, 28, 28, 0.08);
border-radius: 10px;
padding: 8px;
background: rgba(250, 250, 249, 0.88);
font-size: 12px;
}
.wolai-page-ai-opencode-part summary {
cursor: pointer;
font-weight: 650;
}
.wolai-page-ai-opencode-part pre,
.wolai-page-ai-opencode-error {
margin: 8px 0 0;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.wolai-page-ai-opencode-tool summary {
display: flex;
justify-content: space-between;
gap: 8px;
}
.wolai-page-ai-opencode-patch {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.wolai-page-ai-opencode-patch button,
.wolai-page-ai-opencode-part button {
border: 1px solid rgba(27, 28, 28, 0.12);
border-radius: 999px;
padding: 3px 8px;
background: #fff;
}
.wolai-page-ai-opencode-todo {
margin: 0;
padding: 0 12px;
list-style-position: inside;
font-size: 12px;
}
@@ -0,0 +1,59 @@
.mnote-toast-region {
position: fixed;
top: var(--wolai-spacing-lg);
right: var(--wolai-spacing-lg);
z-index: var(--wolai-z-toast, 1300);
display: flex;
flex-direction: column;
gap: var(--wolai-spacing-sm);
pointer-events: none;
}
.mnote-toast {
min-width: 220px;
max-width: min(360px, calc(100vw - 32px));
padding: 10px 14px;
border-radius: var(--wolai-radius-md, 6px);
box-shadow: var(--wolai-shadow-overlay, var(--wolai-shadow-lg));
background: var(--wolai-bg);
border: 1px solid var(--wolai-border);
color: var(--wolai-text-primary);
font: var(--mnote-font-weight-medium) var(--mnote-font-size-md) / var(--mnote-line-height-normal) var(--wolai-font-sans);
opacity: 0;
transform: translateY(-6px);
transition: opacity 160ms ease, transform 160ms ease;
}
.mnote-toast--visible {
opacity: 1;
transform: translateY(0);
}
.mnote-toast--success {
border-color: rgba(34, 165, 89, 0.28);
background: #ecfdf3;
color: #166534;
}
.mnote-toast--warning {
border-color: rgba(217, 119, 6, 0.26);
background: #fffbeb;
color: #92400e;
}
.mnote-toast--error {
border-color: rgba(220, 38, 38, 0.24);
background: #fef2f2;
color: #991b1b;
}
@media (max-width: 640px) {
.mnote-toast-region {
left: var(--wolai-spacing-md);
right: var(--wolai-spacing-md);
}
.mnote-toast {
max-width: none;
}
}
@@ -25,6 +25,10 @@
--wolai-accent-hover: #1d4ed8;
--wolai-font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", "PingFang SC", Helvetica, Arial, sans-serif;
--wolai-radius: 4px;
--wolai-radius-sm: 4px;
--wolai-radius-md: 6px;
--wolai-radius-lg: 8px;
--wolai-radius-xl: 12px;
/* font-size scale */
--mnote-font-size-2xs: 10px;
@@ -54,7 +58,11 @@
--wolai-z-popover: 90;
--wolai-z-dropdown: 120;
--wolai-z-context-menu: 1000;
--wolai-z-dialog-backdrop: 1100;
--wolai-z-modal: 1200;
--wolai-z-dialog: 1200;
--wolai-z-toast: 1300;
--wolai-z-tooltip: 1400;
/* spacing tokens */
--wolai-spacing-xs: 4px;
@@ -70,6 +78,7 @@
--wolai-shadow-lg: 0 18px 48px rgba(15, 23, 42, 0.12);
--wolai-shadow-popover: 0 4px 16px rgba(15, 23, 42, 0.10);
--wolai-shadow-modal: 0 18px 54px rgba(15, 23, 42, 0.18);
--wolai-shadow-overlay: 0 18px 48px rgba(15, 23, 42, 0.22), 0 2px 8px rgba(15, 23, 42, 0.08);
/* state tokens */
--wolai-state-hover-bg: rgba(55, 53, 47, 0.06);
+1 -2
View File
@@ -15,7 +15,6 @@ path = "src/main.rs"
[dependencies]
console_error_panic_hook = "0.1.7"
radix-leptos-primitives = "0.9.0"
leptos = { version = "0.8.19", features = ["csr"] }
leptos_dom = "0.8.8"
serde = { version = "1", features = ["derive"] }
@@ -25,7 +24,7 @@ send_wrapper = "0.6"
js-sys = "0.3.77"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-sys = { version = "0.3.77", features = ["CustomEvent", "CustomEventInit", "DataTransfer", "Document", "DomRect", "DragEvent", "Element", "EventTarget", "HtmlElement", "MessageEvent", "MouseEvent", "Node", "Range", "RequestInit", "RequestMode", "Response", "Selection", "Storage", "Window"] }
web-sys = { version = "0.3.77", features = ["CustomEvent", "CustomEventInit", "DataTransfer", "Document", "DomRect", "DragEvent", "Element", "EventTarget", "HtmlElement", "KeyboardEvent", "MessageEvent", "MouseEvent", "Node", "NodeList", "Range", "RequestInit", "RequestMode", "Response", "Selection", "Storage", "Window"] }
[dependencies.leptos-tiptap]
path = "../../../reference-code/leptos-tiptap"
@@ -48,33 +48,33 @@ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembl
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly mount_mindmap_shell: (a: any, b: any) => [number, number, number];
readonly unmount_mindmap_shell: (a: number) => [number, number];
readonly mount: (a: any, b: any) => [number, number, number];
readonly mount_mindmap_shell: (a: any, b: any) => [number, number, number];
readonly unmount: (a: number) => [number, number];
readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void;
readonly intounderlyingsink_write: (a: number, b: any) => any;
readonly intounderlyingsink_close: (a: number) => any;
readonly intounderlyingsink_abort: (a: number, b: any) => any;
readonly unmount_mindmap_shell: (a: number) => [number, number];
readonly __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
readonly intounderlyingbytesource_type: (a: number) => number;
readonly intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
readonly intounderlyingbytesource_start: (a: number, b: any) => void;
readonly intounderlyingbytesource_pull: (a: number, b: any) => any;
readonly intounderlyingbytesource_cancel: (a: number) => void;
readonly intounderlyingbytesource_pull: (a: number, b: any) => any;
readonly intounderlyingbytesource_start: (a: number, b: any) => void;
readonly intounderlyingbytesource_type: (a: number) => number;
readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void;
readonly intounderlyingsink_abort: (a: number, b: any) => any;
readonly intounderlyingsink_close: (a: number) => any;
readonly intounderlyingsink_write: (a: number, b: any) => any;
readonly __wbg_intounderlyingsource_free: (a: number, b: number) => void;
readonly intounderlyingsource_pull: (a: number, b: any) => any;
readonly intounderlyingsource_cancel: (a: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12: (a: number, b: number, c: any) => [number, number];
readonly wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__had771ddc65647798: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c: (a: number, b: number) => void;
readonly intounderlyingsource_pull: (a: number, b: any) => any;
readonly wasm_bindgen__convert__closures_____invoke__h854f4676fa692669: (a: number, b: number, c: any) => [number, number];
readonly wasm_bindgen__convert__closures_____invoke__h1a94ffe1b1ed8350: (a: number, b: number, c: any, d: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h8eb6fe4c74941e76: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h11eca4a91ce748ff: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af_4: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__hc1d64d6259b30403: (a: number, b: number, c: any) => void;
readonly wasm_bindgen__convert__closures_____invoke__hdc3cd9a3517d1395: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__h4fb07e4e902e51ce: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__hec6d5749a9244252: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __externref_table_alloc: () => number;
@@ -812,7 +812,7 @@ function __wbg_get_imports() {
const a = state0.a;
state0.a = 0;
try {
return wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a(a, state0.b, arg0, arg1);
return wasm_bindgen__convert__closures_____invoke__h1a94ffe1b1ed8350(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
@@ -1232,48 +1232,48 @@ function __wbg_get_imports() {
}
}, arguments); },
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1140, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1011, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h8eb6fe4c74941e76);
return ret;
},
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1192, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1140, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af);
return ret;
},
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 969, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1202, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h854f4676fa692669);
return ret;
},
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1090, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1110, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h11eca4a91ce748ff);
return ret;
},
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1140, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af_4);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 637, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 563, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hc1d64d6259b30403);
return ret;
},
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1092, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1108, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hdc3cd9a3517d1395);
return ret;
},
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1107, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1125, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h4fb07e4e902e51ce);
return ret;
},
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hec6d5749a9244252);
return ret;
},
__wbindgen_cast_000000000000000a: function(arg0) {
@@ -1316,47 +1316,47 @@ function __wbg_get_imports() {
};
}
function wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f(arg0, arg1);
function wasm_bindgen__convert__closures_____invoke__hdc3cd9a3517d1395(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__hdc3cd9a3517d1395(arg0, arg1);
}
function wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5(arg0, arg1);
function wasm_bindgen__convert__closures_____invoke__h4fb07e4e902e51ce(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__h4fb07e4e902e51ce(arg0, arg1);
}
function wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c(arg0, arg1);
function wasm_bindgen__convert__closures_____invoke__hec6d5749a9244252(arg0, arg1) {
wasm.wasm_bindgen__convert__closures_____invoke__hec6d5749a9244252(arg0, arg1);
}
function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__h8eb6fe4c74941e76(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h8eb6fe4c74941e76(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__had771ddc65647798(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__h11eca4a91ce748ff(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h11eca4a91ce748ff(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af_4(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af_4(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__hc1d64d6259b30403(arg0, arg1, arg2) {
wasm.wasm_bindgen__convert__closures_____invoke__hc1d64d6259b30403(arg0, arg1, arg2);
}
function wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2) {
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12(arg0, arg1, arg2);
function wasm_bindgen__convert__closures_____invoke__h854f4676fa692669(arg0, arg1, arg2) {
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h854f4676fa692669(arg0, arg1, arg2);
if (ret[1]) {
throw takeFromExternrefTable0(ret[0]);
}
}
function wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a(arg0, arg1, arg2, arg3) {
wasm.wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a(arg0, arg1, arg2, arg3);
function wasm_bindgen__convert__closures_____invoke__h1a94ffe1b1ed8350(arg0, arg1, arg2, arg3) {
wasm.wasm_bindgen__convert__closures_____invoke__h1a94ffe1b1ed8350(arg0, arg1, arg2, arg3);
}
@@ -1,33 +1,33 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const mount_mindmap_shell: (a: any, b: any) => [number, number, number];
export const unmount_mindmap_shell: (a: number) => [number, number];
export const mount: (a: any, b: any) => [number, number, number];
export const mount_mindmap_shell: (a: any, b: any) => [number, number, number];
export const unmount: (a: number) => [number, number];
export const __wbg_intounderlyingsink_free: (a: number, b: number) => void;
export const intounderlyingsink_write: (a: number, b: any) => any;
export const intounderlyingsink_close: (a: number) => any;
export const intounderlyingsink_abort: (a: number, b: any) => any;
export const unmount_mindmap_shell: (a: number) => [number, number];
export const __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
export const intounderlyingbytesource_type: (a: number) => number;
export const intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
export const intounderlyingbytesource_start: (a: number, b: any) => void;
export const intounderlyingbytesource_pull: (a: number, b: any) => any;
export const intounderlyingbytesource_cancel: (a: number) => void;
export const intounderlyingbytesource_pull: (a: number, b: any) => any;
export const intounderlyingbytesource_start: (a: number, b: any) => void;
export const intounderlyingbytesource_type: (a: number) => number;
export const __wbg_intounderlyingsink_free: (a: number, b: number) => void;
export const intounderlyingsink_abort: (a: number, b: any) => any;
export const intounderlyingsink_close: (a: number) => any;
export const intounderlyingsink_write: (a: number, b: any) => any;
export const __wbg_intounderlyingsource_free: (a: number, b: number) => void;
export const intounderlyingsource_pull: (a: number, b: any) => any;
export const intounderlyingsource_cancel: (a: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h0accad0f5d87788a: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__had771ddc65647798: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c: (a: number, b: number) => void;
export const intounderlyingsource_pull: (a: number, b: any) => any;
export const wasm_bindgen__convert__closures_____invoke__h854f4676fa692669: (a: number, b: number, c: any) => [number, number];
export const wasm_bindgen__convert__closures_____invoke__h1a94ffe1b1ed8350: (a: number, b: number, c: any, d: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h8eb6fe4c74941e76: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h11eca4a91ce748ff: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__h62d2a368ae4b53af_4: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__hc1d64d6259b30403: (a: number, b: number, c: any) => void;
export const wasm_bindgen__convert__closures_____invoke__hdc3cd9a3517d1395: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__h4fb07e4e902e51ce: (a: number, b: number) => void;
export const wasm_bindgen__convert__closures_____invoke__hec6d5749a9244252: (a: number, b: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __externref_table_alloc: () => number;
@@ -132,7 +132,6 @@ impl EditorButtonProps {
/// - data-testid
/// - tooltipdata-tooltip
/// - onClick 阻止默认并调用回调
#[component]
pub(crate) fn EditorButton(props: EditorButtonProps) -> impl IntoView {
let on_click = props.on_click.clone();
let click_handler = move |event: ev::MouseEvent| {
@@ -9,10 +9,9 @@
//! 这些函数替代原 lib.rs / overlays.rs / view 中的手写 Escape keydown 和
//! document mousedown listener,对齐 Radix DismissableLayer + FocusScope 语义。
use std::{cell::RefCell, rc::Rc};
use wasm_bindgen::{closure::Closure, JsCast};
use web_sys::{
window, Document, Element, EventTarget, HtmlElement, KeyboardEvent, MouseEvent,
};
use web_sys::{window, Element, HtmlElement, KeyboardEvent, MouseEvent};
use crate::editor_runtime::dom_events::target_element;
@@ -24,18 +23,20 @@ pub(crate) fn register_dismissable_layer(
container_id: &str,
on_dismiss: Box<dyn FnMut()>,
) -> DismissHandle {
let dismiss = std::cell::RefCell::new(on_dismiss);
let dismiss = Rc::new(RefCell::new(on_dismiss));
let win = window();
let doc = win.as_ref().and_then(|w| w.document());
// Escape keydown(捕获阶段,确保在 Tiptap 之前处理)
let container_id_esc = container_id.to_string();
let doc_esc = doc.clone();
let dismiss_for_esc = Rc::clone(&dismiss);
let dismiss_esc = Closure::wrap(Box::new(move |event: KeyboardEvent| {
if event.key() != "Escape" {
return;
}
// 只处理 container 可见的情况
if let Some(ref doc) = doc {
if let Some(ref doc) = doc_esc {
if let Ok(Some(el)) = doc.query_selector(&format!("#{container_id_esc}")) {
if !is_element_visible(&el) {
return;
@@ -44,7 +45,7 @@ pub(crate) fn register_dismissable_layer(
event.stop_propagation();
}
}
if let Ok(mut f) = dismiss.try_borrow_mut() {
if let Ok(mut f) = dismiss_for_esc.try_borrow_mut() {
f();
}
}) as Box<dyn FnMut(_)>);
@@ -59,6 +60,7 @@ pub(crate) fn register_dismissable_layer(
// 点击外部 handlermousedown,因为 click 可能在 focus 转移之后才触发)
let container_id_click = container_id.to_string();
let dismiss_for_click = Rc::clone(&dismiss);
let dismiss_click = Closure::wrap(Box::new(move |event: MouseEvent| {
let target = event.target();
let Some(target_el) = target.and_then(target_element) else {
@@ -72,7 +74,7 @@ pub(crate) fn register_dismissable_layer(
if let Ok(Some(_)) = target_el.closest(".ProseMirror") {
return;
}
if let Ok(mut f) = dismiss.try_borrow_mut() {
if let Ok(mut f) = dismiss_for_click.try_borrow_mut() {
f();
}
}) as Box<dyn FnMut(_)>);
@@ -49,6 +49,7 @@ pub(crate) const ICON_MAP: &[(&str, &str)] = &[
("autorenew", "autorenew"),
("link", "link"),
("arrow_outward", "arrow_outward"),
("arrow_drop_down", "arrow_drop_down"),
("history", "history"),
("chat", "chat"),
("palette", "palette"),
@@ -76,7 +77,7 @@ pub(crate) fn icon_ligature(name: &str) -> Option<&'static str> {
///
/// The `name` parameter must be a Material Symbols ligature name
/// (e.g. `"auto_awesome"`, `"chevron_right"`, `"looks_one"`).
pub(crate) fn material_icon(name: &str) -> impl IntoView {
pub(crate) fn material_icon(name: &'static str) -> impl IntoView {
view! {
<span class="material-symbols-outlined" aria-hidden="true">{name}</span>
}