feat: add evidence search and stabilize pdf previews

- add document evidence parsing/search/open routes, Hermes tool wiring, local index settings/status, and the document-evidence skill plus design notes
- fix PDF resource tabs by rendering PDFs inline with pdf.js canvases instead of iframe preview pages, release PDF documents on close, and document the fourth-PDF stall bug
- keep PDF preview at 2x rendering while removing the previous lazy-load/placeholder direction, and make dev:hot bind loopback defaults externally reachable

Verification:
- node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
- node scripts/task-dev-hot-plan-test.js
- cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar
- cargo test -p mnote-web --manifest-path rust/Cargo.toml document_shell_returns_page_aggregate_snapshot
- cargo build -p mnote-web --manifest-path rust/Cargo.toml
- browser smoke: sequentially opened the four tea_seed_oil_cosmetic PDFs; fourth PDF rendered 15/15 canvases, iframeCount=0, browser errors=0
This commit is contained in:
lix-2026
2026-06-04 18:51:16 +08:00
parent 9f4b5c4d48
commit 627213dc01
51 changed files with 15960 additions and 1844 deletions
@@ -253,6 +253,29 @@ import {
return '';
}
};
const cssSafe = (value) => {
const text = String(value || '');
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
return text.replace(/["\\]/g, '\\$&');
};
const applyDocumentEvidenceLocatorFromUrl = (root) => {
if (!(root instanceof HTMLElement)) return;
const url = currentUrl();
const blockId = String(url.searchParams.get('blockId') || url.searchParams.get('evidenceBlockId') || '').trim();
const lineRange = String(url.searchParams.get('lineRange') || '').trim();
if (!blockId && !lineRange) return;
root.setAttribute('data-mnote-evidence-open', 'true');
if (blockId) root.setAttribute('data-mnote-evidence-block-id', blockId);
if (lineRange) root.setAttribute('data-mnote-evidence-line-range', lineRange);
const target = blockId ? root.querySelector(`[data-block-id="${cssSafe(blockId)}"]`) : null;
if (target instanceof HTMLElement) {
root.querySelectorAll('[data-mnote-evidence-text-highlight="true"]').forEach((node) => {
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-text-highlight');
});
target.setAttribute('data-mnote-evidence-text-highlight', 'true');
target.scrollIntoView({ block: 'center', inline: 'nearest' });
}
};
const restoreResourceTabInputFromUrl = () => {
const url = currentUrl();
@@ -785,6 +808,7 @@ import {
if (session.projectionSource) runtimeDescriptor.root.setAttribute('data-mnote-projection-source', session.projectionSource);
if (session.blockProjectionVersion) runtimeDescriptor.root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
enhanceEditorAttachmentLinksSoon();
applyDocumentEvidenceLocatorFromUrl(runtimeDescriptor.root);
};
view.onError = (event) => {
const payload = normalizeEnvelopePayload(event);
@@ -784,6 +784,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
console.warn('mnote mindmap resource tab unmount failed', error);
}
}
releaseInlinePdfResource(entry);
if (entry.tab instanceof HTMLElement) entry.tab.remove();
if (entry.panel instanceof HTMLElement) entry.panel.remove();
resourceTabRegistry.delete(key);
@@ -883,6 +884,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const releaseResourceTabEntryRuntime = (entry) => {
if (!entry) return;
releaseInlinePdfResource(entry);
if (entry.view) {
unmountEditorViewBinding(entry.view, { releaseSession: true });
entry.view = null;
@@ -926,6 +928,144 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return url.toString();
};
const normalizeEvidenceBBox = (value) => {
if (!value) return null;
if (Array.isArray(value) && value.length >= 4) {
const values = value.slice(0, 4).map((item) => Number(item));
return values.every(Number.isFinite) ? { x0: values[0], y0: values[1], x1: values[2], y1: values[3] } : null;
}
if (typeof value === 'object') {
const bbox = {
x0: Number(value.x0),
y0: Number(value.y0),
x1: Number(value.x1),
y1: Number(value.y1),
};
return Object.values(bbox).every(Number.isFinite) ? bbox : null;
}
if (typeof value === 'string') {
const parts = value.split(',').map((item) => Number(item.trim()));
return parts.length >= 4 && parts.slice(0, 4).every(Number.isFinite)
? { x0: parts[0], y0: parts[1], x1: parts[2], y1: parts[3] }
: null;
}
return null;
};
const normalizeEvidenceLocatorInput = (input = {}) => {
const locator = input.evidenceLocator && typeof input.evidenceLocator === 'object'
? input.evidenceLocator
: input.locator && typeof input.locator === 'object'
? input.locator
: null;
const params = locator?.openAction?.params && typeof locator.openAction.params === 'object' ? locator.openAction.params : {};
const page = Number(input.page ?? locator?.page ?? params.page);
const bbox = normalizeEvidenceBBox(input.bbox ?? locator?.bbox ?? params.bbox);
const sourceMapPath = String(input.sourceMapPath || locator?.sourceMapPath || params.sourceMapPath || '').trim();
const blockId = String(input.blockId || locator?.blockId || params.blockId || '').trim();
const lineRange = input.lineRange || locator?.lineRange || params.lineRange || null;
const charRange = input.charRange || locator?.charRange || params.charRange || null;
if (!locator && !Number.isFinite(page) && !bbox && !sourceMapPath && !blockId && !lineRange && !charRange) return null;
return {
schema: 'mnote.evidence_locator.v1',
...(locator || {}),
page: Number.isFinite(page) ? page : null,
bbox,
sourceMapPath,
blockId,
lineRange,
charRange,
};
};
const evidenceBBoxParam = (bbox) => {
const normalized = normalizeEvidenceBBox(bbox);
return normalized ? [normalized.x0, normalized.y0, normalized.x1, normalized.y1].join(',') : '';
};
const applyEvidenceLocatorToFrame = (frame, locator) => {
if (!(frame instanceof HTMLIFrameElement) || !locator) return;
try {
const url = new URL(frame.getAttribute('src') || frame.src || '', window.location.origin);
if (Number.isFinite(Number(locator.page))) url.searchParams.set('page', String(Number(locator.page)));
const bbox = evidenceBBoxParam(locator.bbox);
if (bbox) url.searchParams.set('bbox', bbox);
if (locator.sourceMapPath) url.searchParams.set('sourceMapPath', String(locator.sourceMapPath));
if (locator.blockId) url.searchParams.set('blockId', String(locator.blockId));
frame.src = url.toString();
} catch (_) {}
};
const applyEvidenceLocatorToImagePanel = (entry, locator) => {
if (!(entry?.panel instanceof HTMLElement) || !locator) return;
const bbox = normalizeEvidenceBBox(locator.bbox);
let overlay = entry.panel.querySelector('[data-mnote-evidence-bbox-highlight]');
if (!bbox) {
if (overlay instanceof HTMLElement) overlay.hidden = true;
return;
}
if (!(overlay instanceof HTMLElement)) {
overlay = document.createElement('div');
overlay.className = 'mnote-resource-tab-bbox-highlight';
overlay.setAttribute('data-mnote-evidence-bbox-highlight', 'true');
entry.panel.append(overlay);
}
overlay.hidden = false;
overlay.style.left = `${Math.max(0, bbox.x0)}px`;
overlay.style.top = `${Math.max(0, bbox.y0)}px`;
overlay.style.width = `${Math.max(1, bbox.x1 - bbox.x0)}px`;
overlay.style.height = `${Math.max(1, bbox.y1 - bbox.y0)}px`;
};
const applyEvidenceLocatorToTextPanel = (entry, locator) => {
if (!(entry?.panel instanceof HTMLElement) || !locator?.blockId) return;
const root = entry.panel.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
if (!(root instanceof HTMLElement)) return;
const selector = `[data-block-id="${cssSafe(locator.blockId)}"]`;
const target = root.querySelector(selector);
if (!(target instanceof HTMLElement)) return;
root.querySelectorAll('[data-mnote-evidence-text-highlight="true"]').forEach((node) => {
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-text-highlight');
});
target.setAttribute('data-mnote-evidence-text-highlight', 'true');
target.scrollIntoView({ block: 'center', inline: 'nearest' });
};
const applyEvidenceLocatorToInlinePdfPanel = (entry, locator) => {
if (!(entry?.panel instanceof HTMLElement) || !locator) return;
const pageNumber = Number(locator.page || 0);
if (!Number.isFinite(pageNumber) || pageNumber <= 0) return;
const canvas = entry.panel.querySelector(`canvas.mnote-pdf-page[data-page-number="${pageNumber}"]`);
if (!(canvas instanceof HTMLCanvasElement)) return;
entry.panel.querySelectorAll('canvas.mnote-pdf-page[data-mnote-evidence-page="true"]').forEach((node) => {
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-page');
});
canvas.setAttribute('data-mnote-evidence-page', 'true');
canvas.scrollIntoView({ block: 'center', inline: 'nearest' });
};
const applyEvidenceLocatorToEntry = (entry, input = {}) => {
if (!(entry?.panel instanceof HTMLElement)) return;
const locator = normalizeEvidenceLocatorInput(input);
if (!locator) return;
entry.evidenceLocator = locator;
entry.panel.setAttribute('data-mnote-evidence-locator', JSON.stringify(locator));
entry.panel.setAttribute('data-mnote-evidence-open', 'true');
if (Number.isFinite(Number(locator.page))) entry.panel.setAttribute('data-mnote-evidence-page', String(Number(locator.page)));
if (locator.blockId) entry.panel.setAttribute('data-mnote-evidence-block-id', String(locator.blockId));
if (locator.sourceMapPath) entry.panel.setAttribute('data-mnote-evidence-source-map-path', String(locator.sourceMapPath));
const bbox = evidenceBBoxParam(locator.bbox);
if (bbox) entry.panel.setAttribute('data-mnote-evidence-bbox', bbox);
const frame = entry.panel.querySelector('iframe.mnote-resource-tab-frame');
if (frame instanceof HTMLIFrameElement) applyEvidenceLocatorToFrame(frame, locator);
if (entry.kind === 'image') applyEvidenceLocatorToImagePanel(entry, locator);
if (entry.kind === 'pdf') applyEvidenceLocatorToInlinePdfPanel(entry, locator);
if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
window.setTimeout(() => applyEvidenceLocatorToTextPanel(entry, locator), 80);
window.setTimeout(() => applyEvidenceLocatorToTextPanel(entry, locator), 450);
}
};
const renderPassiveResourceMissing = (entry, message) => {
if (!(entry?.panel instanceof HTMLElement)) return;
entry.panel.setAttribute('data-mnote-resource-missing', 'true');
@@ -945,6 +1085,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const frame = entry.panel.querySelector('iframe.mnote-resource-tab-frame');
if (frame instanceof HTMLIFrameElement) {
frame.src = withResourceReloadToken(frame.getAttribute('src') || frame.src || '');
return;
}
if (entry.kind === 'pdf' && entry.inlinePdfSourceHref) {
void openPassiveResourceTab(entry, { ...(entry.lastPassiveInput || {}), href: withResourceReloadToken(entry.inlinePdfSourceHref) });
}
};
@@ -986,6 +1130,115 @@ export const createResourceTabRuntime = (dependencies = {}) => {
});
};
const releaseInlinePdfResource = (entry) => {
if (!entry) return;
const pdf = entry.inlinePdfDocument;
entry.inlinePdfDocument = null;
entry.inlinePdfRenderToken = null;
if (!pdf) return;
try {
void pdf.destroy();
} catch (_) {}
};
const pdfFileUrlFromPreviewHref = (href) => {
const value = String(href || '').trim();
if (!value) return '';
try {
const url = new URL(value, window.location.origin);
return String(url.searchParams.get('fileUrl') || value).trim();
} catch (_) {
return value;
}
};
const renderInlinePdfPage = async (entry, viewer, pdf, pageNumber, evidenceLocator) => {
const page = await pdf.getPage(pageNumber);
const baseViewport = page.getViewport({ scale: 1 });
const availableWidth = Math.max(280, viewer.clientWidth - 20);
const scale = Math.max(0.6, Math.min(2.4, availableWidth / baseViewport.width));
const viewport = page.getViewport({ scale });
const outputScale = Math.min(2.5, Math.max(2, window.devicePixelRatio || 1));
const canvas = document.createElement('canvas');
canvas.className = 'mnote-pdf-page';
canvas.setAttribute('data-page-number', String(pageNumber));
canvas.width = Math.floor(viewport.width * outputScale);
canvas.height = Math.floor(viewport.height * outputScale);
canvas.style.display = 'block';
canvas.style.maxWidth = '100%';
canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.height = `${Math.floor(viewport.height)}px`;
canvas.style.margin = '0 auto 14px';
canvas.style.background = '#fff';
canvas.style.border = '1px solid #d8d8d2';
canvas.style.boxShadow = '0 2px 10px rgba(25, 25, 22, .08)';
const context = canvas.getContext('2d', { alpha: false });
if (!context) return;
await page.render({
canvasContext: context,
viewport,
transform: outputScale !== 1 ? [outputScale, 0, 0, outputScale, 0, 0] : null,
}).promise;
const evidencePage = Number(evidenceLocator?.page || 0);
if (evidencePage === pageNumber) {
canvas.setAttribute('data-mnote-evidence-page', 'true');
const bbox = normalizeEvidenceBBox(evidenceLocator?.bbox);
if (bbox) {
const rect = viewport.convertToViewportRectangle([bbox.x0, bbox.y0, bbox.x1, bbox.y1]);
const x = Math.min(rect[0], rect[2]);
const y = Math.min(rect[1], rect[3]);
const width = Math.max(1, Math.abs(rect[2] - rect[0]));
const height = Math.max(1, Math.abs(rect[3] - rect[1]));
context.save();
context.scale(outputScale, outputScale);
context.fillStyle = 'rgba(37, 99, 235, 0.18)';
context.strokeStyle = 'rgba(37, 99, 235, 0.9)';
context.lineWidth = 2;
context.fillRect(x, y, width, height);
context.strokeRect(x, y, width, height);
context.restore();
}
}
if (entry.inlinePdfDocument === pdf) viewer.append(canvas);
if (evidencePage === pageNumber) window.setTimeout(() => canvas.scrollIntoView({ block: 'center', inline: 'nearest' }), 0);
};
const openInlinePdfResourceTab = async (entry, input) => {
const href = String(input.officeUrl || input.href || '').trim();
const fileUrl = pdfFileUrlFromPreviewHref(href);
if (!(entry?.panel instanceof HTMLElement) || !fileUrl) return false;
releaseInlinePdfResource(entry);
const renderToken = {};
entry.passiveFrameSrc = href;
entry.inlinePdfSourceHref = href;
entry.inlinePdfRenderToken = renderToken;
entry.lastPassiveInput = { ...input };
entry.panel.replaceChildren();
const viewer = document.createElement('div');
viewer.className = 'mnote-pdf-viewer';
viewer.setAttribute('data-mnote-inline-pdf-viewer', 'true');
viewer.style.width = '100%';
viewer.style.maxWidth = '1180px';
viewer.style.margin = '0 auto';
viewer.style.padding = '8px 12px 28px';
entry.panel.append(viewer);
const pdfjsLib = await import('/api/pdfjs/pdf.mjs');
pdfjsLib.GlobalWorkerOptions.workerSrc = '/api/pdfjs/pdf.worker.mjs';
const sameOrigin = fileUrl.startsWith('/') || fileUrl.startsWith(window.location.origin);
const pdf = await pdfjsLib.getDocument({ url: fileUrl, withCredentials: sameOrigin, disableWorker: true }).promise;
entry.inlinePdfDocument = pdf;
const total = Number(pdf.numPages || 0);
viewer.setAttribute('data-mnote-pdf-status', `0 / ${total}`);
const evidenceLocator = normalizeEvidenceLocatorInput(input);
for (let pageNumber = 1; pageNumber <= total; pageNumber += 1) {
if (entry.inlinePdfDocument !== pdf || entry.inlinePdfRenderToken !== renderToken) return true;
await renderInlinePdfPage(entry, viewer, pdf, pageNumber, evidenceLocator);
viewer.setAttribute('data-mnote-pdf-status', `${pageNumber} / ${total}`);
}
installPassiveResourceWatch(entry);
return true;
};
const isLocalOcrSourceEntry = (entry) => {
if (!entry || String(entry.sourceKind || '').trim() !== 'local_folder') return false;
if (!String(entry.rootUri || '').trim() || !String(entry.path || '').trim()) return false;
@@ -1136,7 +1389,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
if (!(toggle instanceof HTMLButtonElement)) return null;
if (toggle.getAttribute('data-mnote-local-ocr-bound') === 'true') return toggle;
toggle.setAttribute('data-mnote-local-ocr-bound', 'true');
toggle.addEventListener('click', () => {
toggle.addEventListener('click', (event) => {
if (toggle.getAttribute('data-mnote-action') === 'open-ocr-settings') {
event.preventDefault();
window.dispatchEvent(new CustomEvent('mnote:open-local-ocr-settings'));
return;
}
void runManualLocalOcrForActiveTarget(toggle).catch((error) => {
console.warn('mnote local OCR 手动入口失败', error);
});
@@ -1187,10 +1445,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
toggle = document.createElement('button');
toggle.type = 'button';
toggle.className = 'wolai-icon-button mnote-local-ocr-task-toggle';
toggle.setAttribute('title', 'OCR 任务');
toggle.setAttribute('aria-label', 'OCR 任务');
toggle.setAttribute('title', 'OCR 设置');
toggle.setAttribute('aria-label', 'OCR 设置');
toggle.setAttribute('data-testid', 'mnote-local-ocr-task-toggle');
toggle.setAttribute('data-mnote-action', 'toggle-ocr-tasks');
toggle.setAttribute('data-mnote-action', 'open-ocr-settings');
toggle.innerHTML = '<span class="material-symbols-outlined" data-icon="document_scanner" aria-hidden="true"></span><span class="mnote-local-ocr-task-badge" data-mnote-local-ocr-task-count hidden>0</span>';
if (actions instanceof HTMLElement) actions.appendChild(toggle);
else document.body.appendChild(toggle);
@@ -1276,7 +1534,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const runningCount = jobs.filter((job) => !['done', 'failed', 'stale'].includes(String(job?.status || ''))).length;
const toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
if (toggle instanceof HTMLButtonElement) {
const label = runningCount > 0 ? `${runningCount} 个 OCR 处理中` : (jobs.length > 0 ? `${jobs.length} 个 OCR 任务` : 'OCR 任务');
const label = runningCount > 0 ? `${runningCount} 个 OCR 处理中,打开 OCR 设置` : (jobs.length > 0 ? `${jobs.length} 个 OCR 任务,打开 OCR 设置` : 'OCR 设置');
toggle.setAttribute('title', label);
toggle.setAttribute('aria-label', label);
toggle.setAttribute('aria-expanded', localOcrTaskState.drawerOpen ? 'true' : 'false');
@@ -1595,6 +1853,22 @@ export const createResourceTabRuntime = (dependencies = {}) => {
return jobs;
};
window.addEventListener('mnote:local-ocr-settings-action', (event) => {
const action = String(event?.detail?.action || '').trim();
if (action === 'run-active') {
const toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
void runManualLocalOcrForActiveTarget(toggle instanceof HTMLButtonElement ? toggle : null).catch((error) => {
console.warn('mnote local OCR 设置入口识别失败', error);
});
return;
}
if (action === 'tasks') {
ensureLocalOcrTaskDock();
localOcrTaskState.drawerOpen = true;
renderLocalOcrTaskDock();
}
});
const renderLocalOcrToolbar = (entry) => {
if (!isLocalOcrSourceEntry(entry) || !(entry.panel instanceof HTMLElement)) return;
const toolbar = entry.panel.querySelector('[data-testid="mnote-local-ocr-toolbar"]');
@@ -1765,15 +2039,16 @@ export const createResourceTabRuntime = (dependencies = {}) => {
});
};
const openPassiveResourceTab = (entry, input) => {
const openPassiveResourceTab = async (entry, input) => {
const href = String(input.officeUrl || input.href || '').trim();
if (entry.kind === 'image') {
entry.panel.innerHTML = '<img class="mnote-resource-tab-image" alt="">';
entry.panel.innerHTML = '<div class="mnote-resource-tab-image-shell"><img class="mnote-resource-tab-image" alt=""><div class="mnote-resource-tab-bbox-highlight" data-mnote-evidence-bbox-highlight="true" hidden></div></div>';
const img = entry.panel.querySelector('img');
if (img instanceof HTMLImageElement) {
img.src = href;
img.alt = entry.title;
}
applyEvidenceLocatorToEntry(entry, input);
ensureLocalOcrTaskDock();
ensureLocalOcrTaskEvents(entry.rootUri);
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
@@ -1781,6 +2056,18 @@ export const createResourceTabRuntime = (dependencies = {}) => {
installPassiveResourceWatch(entry);
return;
}
if (entry.kind === 'pdf') {
await openInlinePdfResourceTab(entry, input);
applyEvidenceLocatorToEntry(entry, input);
if (isLocalOcrSourceEntry(entry)) {
ensureLocalOcrTaskDock();
ensureLocalOcrTaskEvents(entry.rootUri);
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
void maybeAutoCreateLocalOcrJob(entry).catch((error) => console.warn('mnote local OCR 自动任务失败', error));
}
installPassiveResourceWatch(entry);
return;
}
entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
const frame = entry.panel.querySelector('iframe');
if (frame instanceof HTMLIFrameElement) {
@@ -1792,7 +2079,9 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}, { once: true });
}
frame.src = href;
entry.passiveFrameSrc = href;
}
applyEvidenceLocatorToEntry(entry, input);
if (isLocalOcrSourceEntry(entry)) {
ensureLocalOcrTaskDock();
ensureLocalOcrTaskEvents(entry.rootUri);
@@ -1802,6 +2091,16 @@ export const createResourceTabRuntime = (dependencies = {}) => {
installPassiveResourceWatch(entry);
};
const refreshExistingPdfResourceTab = async (entry, input) => {
if (!entry || entry.kind !== 'pdf') return false;
const nextHref = String(input.officeUrl || input.href || '').trim();
if (!nextHref) return false;
if (nextHref !== String(entry.inlinePdfSourceHref || entry.passiveFrameSrc || '').trim()) {
await openPassiveResourceTab(entry, input);
}
return true;
};
const refreshExistingOfficeResourceTab = (entry, input) => {
if (!entry || entry.kind !== 'office') return false;
const nextHref = String(input.officeUrl || input.href || '').trim();
@@ -1810,7 +2109,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const currentHref = frame instanceof HTMLIFrameElement
? String(frame.getAttribute('src') || frame.src || '').trim()
: '';
if (currentHref !== nextHref) openPassiveResourceTab(entry, input);
if (currentHref !== nextHref) void openPassiveResourceTab(entry, input);
return true;
};
@@ -1883,6 +2182,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
const objectIdentity = String(input.objectIdentity || input.assetId || input.href || '').trim();
if (!objectIdentity) return false;
const registryKey = resourceTabRegistryKey(paneRole, objectIdentity);
const requestedKind = normalizeResourceTabKind(input);
if (paneRole === 'secondary') {
resourceTabRegistry.forEach((entry, key) => {
if (normalizePaneRole(entry.paneRole) !== paneRole) return;
@@ -1896,8 +2196,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
}
const existing = resourceTabRegistry.get(registryKey);
if (existing) {
refreshExistingOfficeResourceTab(existing, input);
activateMainEditorTab(registryKey, paneRole);
await refreshExistingPdfResourceTab(existing, input);
refreshExistingOfficeResourceTab(existing, input);
applyEvidenceLocatorToEntry(existing, input);
return true;
}
const entry = createResourceTabDom({ ...input, objectIdentity, paneRole });
@@ -1910,8 +2212,9 @@ export const createResourceTabRuntime = (dependencies = {}) => {
} else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
await openTiptapResourceTab(entry, input);
} else {
openPassiveResourceTab(entry, input);
await openPassiveResourceTab(entry, input);
}
applyEvidenceLocatorToEntry(entry, input);
activateMainEditorTab(registryKey, paneRole);
return true;
} catch (error) {
@@ -74,6 +74,23 @@ export const mergeTiptapMarks = (...groups) => {
});
};
const legacyBlockAttrs = (block) => (
{
...(block?.attrs && typeof block.attrs === 'object' ? block.attrs : {}),
...(block?.props && typeof block.props === 'object' ? block.props : {}),
}
);
const legacyBlockAttr = (block, ...keys) => {
const props = block?.props && typeof block.props === 'object' ? block.props : {};
const attrs = block?.attrs && typeof block.attrs === 'object' ? block.attrs : {};
for (const key of keys) {
if (props[key] !== undefined) return props[key];
if (attrs[key] !== undefined) return attrs[key];
}
return undefined;
};
export const legacyInlineContentToTiptap = (value) => {
if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
@@ -102,16 +119,17 @@ export const legacyInlineContentToTiptap = (value) => {
export const legacyBlockToTiptap = (block, index = 0) => {
const type = typeof block?.type === 'string' ? block.type : 'paragraph';
const blockAttrs = legacyBlockAttrs(block);
const blockId = typeof block?.id === 'string' && block.id.trim()
? block.id.trim()
: typeof block?.blockId === 'string' && block.blockId.trim()
? block.blockId.trim()
: `block-${index + 1}`;
const content = legacyInlineContentToTiptap(block?.content ?? block?.contentNodes);
const textAlign = typeof block?.props?.textAlign === 'string'
? block.props.textAlign
: typeof block?.props?.text_align === 'string'
? block.props.text_align
const textAlign = typeof legacyBlockAttr(block, 'textAlign') === 'string'
? legacyBlockAttr(block, 'textAlign')
: typeof legacyBlockAttr(block, 'text_align') === 'string'
? legacyBlockAttr(block, 'text_align')
: undefined;
const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs;
const nestedChildren = Array.isArray(block?.children)
@@ -130,24 +148,24 @@ export const legacyBlockToTiptap = (block, index = 0) => {
}],
});
if (type === 'heading') {
const level = Number(block?.props?.level || block?.level || 1) || 1;
const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
const level = Number(legacyBlockAttr(block, 'level', 'headingLevel') || block?.level || 1) || 1;
const collapsed = typeof legacyBlockAttr(block, 'collapsed') === 'boolean' ? { collapsed: legacyBlockAttr(block, 'collapsed') } : {};
return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
}
if (type === 'bulletListItem' || type === 'bullet_list_item') return withListChildren('listItem', 'bulletList');
if (type === 'numberedListItem' || type === 'numbered_list_item') return withListChildren('listItem', 'orderedList');
if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
return withListChildren('taskItem', 'taskList', { checked: legacyBlockAttr(block, 'checked') === true });
}
if (type === 'quote' || type === 'blockquote') {
return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] };
}
if (type === 'codeBlock' || type === 'code_block') {
return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content };
return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: legacyBlockAttr(block, 'language') || null }), content };
}
if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') return { type: 'horizontalRule', attrs: { blockId } };
if (type === 'mindmap') {
const attrs = block?.attrs && typeof block.attrs === 'object' ? block.attrs : null;
const attrs = blockAttrs;
const data = block?.props?.data && typeof block.props.data === 'object' ? block.props.data : null;
const mindmapId = firstNonEmptyText(
block?.props?.mindmapId,
@@ -1582,7 +1582,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
var objectKind = row instanceof HTMLElement ? String(row.getAttribute('data-object-kind') || '').trim() : '';
var title = rowTitle(row).toLowerCase();
if (objectKind === 'mindmap' || iconKind === 'mindmap') return 'mindmap';
if (objectKind === 'table' || iconKind === 'table' || iconKind === 'luckysheet' || title.indexOf('.luckysheet') >= 0) return 'table';
if (objectKind === 'table' || iconKind === 'table') return 'table';
return 'file';
}
@@ -1905,7 +1905,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true');
window.dispatchEvent(new CustomEvent('tree:local-command-batch-complete', { detail: { batchId: batchId, action: 'bulk-delete', failed: 0, count: total } }));
recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal', batchId: batchId });
if (currentSourceKind() !== 'local_folder') void refreshLocalFolderSidebarSnapshot();
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
return true;
}
@@ -304,7 +304,14 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
documentId: String(input && input.documentId || currentDocumentId() || '').trim(),
workspaceId: String(input && input.workspaceId || resolveWorkspaceId(document.body) || '').trim(),
workspacePath: workspacePath,
paneRole: String(input && input.paneRole || 'primary').trim() || 'primary'
paneRole: String(input && input.paneRole || 'primary').trim() || 'primary',
evidenceLocator: input && input.evidenceLocator && typeof input.evidenceLocator === 'object' ? input.evidenceLocator : null,
page: input && input.page,
bbox: input && input.bbox,
sourceMapPath: String(input && input.sourceMapPath || '').trim(),
blockId: String(input && input.blockId || '').trim(),
lineRange: input && input.lineRange,
charRange: input && input.charRange
});
}
@@ -324,6 +324,21 @@ export function createSidebarPageSettingsRuntime(context) {
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
}
function updateStandaloneSettingsTriggerState() {
var indexTrigger = document.querySelector('[data-testid="mnote-local-index-settings-toggle"]');
var indexOpen = isLocalIndexSettingsOpen();
if (indexTrigger instanceof HTMLElement) {
indexTrigger.setAttribute('data-state', indexOpen ? 'open' : 'closed');
indexTrigger.setAttribute('aria-expanded', indexOpen ? 'true' : 'false');
}
var ocrTrigger = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
var ocrOpen = isLocalOcrSettingsOpen();
if (ocrTrigger instanceof HTMLElement) {
ocrTrigger.setAttribute('data-state', ocrOpen ? 'open' : 'closed');
ocrTrigger.setAttribute('aria-expanded', ocrOpen ? 'true' : 'false');
}
}
function createPageOptionRow(key, type) {
var inputType = type || 'checkbox';
var supported = pageOptionIsSupported(key);
@@ -412,10 +427,6 @@ export function createSidebarPageSettingsRuntime(context) {
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
input.checked = globalHeadingNumbers;
});
var localOcrPreferences = currentLocalOcrPreferences();
popover.querySelectorAll('[data-local-ocr-option-checkbox="autoEnabled"]').forEach(function(input) {
input.checked = localOcrPreferences['localOcr.autoEnabled'] === true;
});
var preferences = currentPageWidthPreferences();
popover.querySelectorAll('[data-page-width-select]').forEach(function(select) {
var type = select.getAttribute('data-page-width-select') || '';
@@ -430,6 +441,13 @@ export function createSidebarPageSettingsRuntime(context) {
});
}
function renderLocalOcrOptions(popover) {
var localOcrPreferences = currentLocalOcrPreferences();
popover.querySelectorAll('[data-local-ocr-option-checkbox="autoEnabled"]').forEach(function(input) {
input.checked = localOcrPreferences['localOcr.autoEnabled'] === true;
});
}
function createPageFontRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
@@ -566,7 +584,6 @@ export function createSidebarPageSettingsRuntime(context) {
'<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">' +
@@ -583,22 +600,8 @@ export function createSidebarPageSettingsRuntime(context) {
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() +
createLocalOcrAutoRow() +
createPageWidthRows() +
'</div>' +
'<div class="wolai-page-settings-actions">' +
@@ -614,6 +617,73 @@ export function createSidebarPageSettingsRuntime(context) {
return popover;
}
function createLocalIndexSettingsPanelHtml() {
return '' +
'<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>' +
'</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>' +
'<section class="wolai-page-settings-index-group">' +
'<div class="wolai-page-settings-index-title">索引范围</div>' +
'<div class="wolai-page-settings-index-range-list" data-testid="wolai-page-settings-local-index-ranges"></div>' +
'<button type="button" class="wolai-page-settings-index-add" data-local-index-action="add-path">新增范围</button>' +
'<div class="wolai-page-settings-index-schedule">' +
'<label><span>索引时间</span><select data-testid="wolai-page-settings-local-index-schedule-mode"><option value="daily">每日</option><option value="once">指定日期</option><option value="manual">手动</option></select></label>' +
'<label><span>时间</span><input type="time" data-testid="wolai-page-settings-local-index-schedule-time" value="02:00"></label>' +
'<label><span>日期</span><input type="date" data-testid="wolai-page-settings-local-index-schedule-date"></label>' +
'<label class="wolai-page-settings-index-inline"><input type="checkbox" data-testid="wolai-page-settings-local-index-run-on-change"><span>文档变化时立即索引</span></label>' +
'</div>' +
'<div class="wolai-page-settings-index-actions">' +
'<button type="button" data-local-index-action="save">保存设置</button>' +
'<button type="button" data-local-index-action="refresh">重建索引</button>' +
'</div>' +
'</section>' +
'</div>' +
'</div>';
}
function ensureLocalIndexSettingsPopover() {
var existing = document.querySelector('[data-testid="mnote-local-index-settings-popover"]');
if (existing instanceof HTMLElement) return existing;
var popover = document.createElement('div');
popover.className = 'wolai-page-settings-popover mnote-local-index-settings-popover';
popover.setAttribute('data-testid', 'mnote-local-index-settings-popover');
popover.setAttribute('data-mnote-surface', 'local-index-settings');
popover.hidden = true;
popover.innerHTML = createLocalIndexSettingsPanelHtml();
document.body.appendChild(popover);
return popover;
}
function ensureLocalOcrSettingsPopover() {
var existing = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]');
if (existing instanceof HTMLElement) return existing;
var popover = document.createElement('div');
popover.className = 'wolai-page-settings-popover mnote-local-ocr-settings-popover';
popover.setAttribute('data-testid', 'mnote-local-ocr-settings-popover');
popover.setAttribute('data-mnote-surface', 'local-ocr-settings');
popover.hidden = true;
popover.innerHTML = '' +
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-local-ocr-settings-panel" role="dialog" aria-modal="false" aria-label="OCR 设置">' +
'<div class="mnote-settings-panel-head">' +
'<strong>OCR 设置</strong>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭 OCR 设置">×</button>' +
'</div>' +
'<div class="wolai-page-settings-section">' +
createLocalOcrAutoRow() +
'<div class="wolai-page-settings-index-actions">' +
'<button type="button" data-local-ocr-settings-action="run-active">识别当前资源</button>' +
'<button type="button" data-local-ocr-settings-action="tasks">查看 OCR 任务</button>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(popover);
return popover;
}
function pageSettingsLocalIndexScopeKey() {
return [
currentSourceKind(),
@@ -624,79 +694,124 @@ export function createSidebarPageSettingsRuntime(context) {
}
function pageSettingsLocalIndexIsAvailable() {
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri());
}
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();
popover = popover || ensureLocalIndexSettingsPopover();
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;
var rangesNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-ranges"]');
var scheduleModeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-mode"]');
var scheduleTimeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-time"]');
var scheduleDateNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-date"]');
var runOnChangeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-run-on-change"]');
if (!(statusNode instanceof HTMLElement)) return;
if (!pageSettingsLocalIndexIsAvailable()) {
statusNode.textContent = '本地索引仅在本地工作区页面可用';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
statusNode.textContent = '本地索引仅在本地工作区可用';
renderLocalIndexRangeRows(popover, [], 'fault', true, {});
setLocalIndexScheduleControlsDisabled(true);
return;
}
var summary = pageUiState.localIndexSummary || {};
if (summary.loading) {
statusNode.textContent = '正在读取本地索引...';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
renderLocalIndexRangeRows(popover, currentLocalIndexRangeValues(popover), 'indexing', true, summary.status || {});
setLocalIndexScheduleControlsDisabled(true);
return;
}
if (summary.error) {
statusNode.textContent = '本地索引读取失败';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
renderLocalIndexRangeRows(popover, currentLocalIndexRangeValues(popover), 'fault', false, summary.status || {});
setLocalIndexScheduleControlsDisabled(false);
return;
}
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
statusNode.textContent = '切换到索引页签后读取本地索引';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
statusNode.textContent = '打开索引设置后读取本地索引';
renderLocalIndexRangeRows(popover, [], 'indexing', true, {});
setLocalIndexScheduleControlsDisabled(true);
return;
}
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
var status = summary.status || {};
var settings = status.settings && typeof status.settings === 'object' ? status.settings : {};
var includePaths = Array.isArray(settings.includePaths) ? settings.includePaths : ['.'];
statusNode.textContent = '索引 ' + Number(status.documentCount || 0) + ' 个页面 / ' + Number(status.resourceCount || 0) + ' 个资源 / ' + Number(status.evidenceBlockCount || 0) + ' 个证据块' + (status.scheduledDue ? ',已到计划时间' : '');
if (!(rangesNode instanceof HTMLElement) || !rangesNode.contains(document.activeElement)) {
renderLocalIndexRangeRows(popover, includePaths, localIndexStatusKind(status), false, status);
}
if (scheduleModeNode instanceof HTMLSelectElement && document.activeElement !== scheduleModeNode) {
scheduleModeNode.value = String(settings.scheduleMode || 'daily');
}
if (scheduleTimeNode instanceof HTMLInputElement && document.activeElement !== scheduleTimeNode) {
scheduleTimeNode.value = String(settings.scheduleTime || '02:00');
}
if (scheduleDateNode instanceof HTMLInputElement && document.activeElement !== scheduleDateNode) {
scheduleDateNode.value = String(settings.scheduleDate || '');
}
if (runOnChangeNode instanceof HTMLInputElement) {
runOnChangeNode.checked = settings.runOnChange === true;
}
setLocalIndexScheduleControlsDisabled(false);
}
function localIndexStatusKind(status) {
if (!status || typeof status !== 'object') return 'fault';
if (status.indexExists !== true) return 'fault';
if (status.cacheMatchesSettings === true && status.scheduledDue !== true) return 'indexed';
return 'indexing';
}
function localIndexStatusLabel(kind) {
if (kind === 'indexed') return '已索引';
if (kind === 'indexing') return '索引中';
return '索引故障';
}
function localIndexPathStatusKind(path, status, fallbackKind) {
if (fallbackKind === 'fault' || fallbackKind === 'indexing') return fallbackKind;
var indexedPaths = Array.isArray(status && status.indexedPaths) ? status.indexedPaths : [];
var normalizedPath = String(path || '').trim() || '.';
if (indexedPaths.indexOf(normalizedPath) >= 0 && status.cacheMatchesSettings === true) return 'indexed';
if (status.indexExists === true) return 'indexing';
return 'fault';
}
function renderLocalIndexRangeRows(popover, includePaths, fallbackKind, disabled, status) {
var rangesNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-ranges"]');
if (!(rangesNode instanceof HTMLElement)) return;
var rawPaths = Array.isArray(includePaths) ? includePaths.map(function(value) {
return String(value || '').trim();
}) : [];
var paths = disabled ? rawPaths.filter(Boolean) : rawPaths;
if (!paths.length && !disabled) paths = ['.'];
rangesNode.innerHTML = paths.map(function(path, index) {
var kind = localIndexPathStatusKind(path, status || {}, fallbackKind || 'fault');
return '' +
'<div class="wolai-page-settings-index-range-row" data-local-index-range-row="true">' +
'<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" value="' + escapeHtml(path) + '" spellcheck="false"' + (disabled ? ' disabled' : '') + ' />' +
'<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>' +
'</div>';
}).join('');
}
function setLocalIndexScheduleControlsDisabled(disabled) {
var popover = ensureLocalIndexSettingsPopover();
[
'[data-testid="wolai-page-settings-local-index-schedule-mode"]',
'[data-testid="wolai-page-settings-local-index-schedule-time"]',
'[data-testid="wolai-page-settings-local-index-schedule-date"]',
'[data-testid="wolai-page-settings-local-index-run-on-change"]'
].forEach(function(selector) {
var node = popover.querySelector(selector);
if (node instanceof HTMLInputElement || node instanceof HTMLSelectElement) node.disabled = disabled;
});
}
async function loadPageSettingsLocalIndex(force) {
@@ -713,50 +828,148 @@ export function createSidebarPageSettingsRuntime(context) {
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: true,
error: '',
backlinks: null,
tags: null
error: ''
};
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);
var statusUrl = '/api/search/local-index/status?' + baseParams.toString();
var response = await fetch(statusUrl, { headers: { accept: 'application/json' } });
var statusPayload = await response.json().catch(function(){ return null; });
if (!response.ok || !statusPayload || statusPayload.ok !== true) {
throw new Error('status_' + response.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 : []
status: statusPayload.result || {}
};
} catch (error) {
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: false,
error: error instanceof Error ? error.message : String(error),
backlinks: [],
tags: []
status: {}
};
}
renderPageSettingsLocalIndex();
}
function localIndexIncludePathsFromForm() {
var popover = ensureLocalIndexSettingsPopover();
var values = currentLocalIndexRangeValues(popover).map(function(value) {
return value.trim();
}).filter(Boolean);
return values.length ? values : ['.'];
}
function currentLocalIndexRangeValues(popover) {
var root = popover || ensureLocalIndexSettingsPopover();
return Array.from(root.querySelectorAll('[data-local-index-range-input]')).map(function(input) {
return input instanceof HTMLInputElement ? input.value : '';
});
}
function localIndexSchedulePayloadFromForm() {
var popover = ensureLocalIndexSettingsPopover();
var modeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-mode"]');
var timeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-time"]');
var dateNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-schedule-date"]');
var runOnChangeNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-run-on-change"]');
return {
scheduleMode: modeNode instanceof HTMLSelectElement ? modeNode.value : 'daily',
scheduleTime: timeNode instanceof HTMLInputElement ? (timeNode.value || '02:00') : '02:00',
scheduleDate: dateNode instanceof HTMLInputElement ? dateNode.value : '',
runOnChange: runOnChangeNode instanceof HTMLInputElement ? runOnChangeNode.checked : false
};
}
function addLocalIndexRange() {
var popover = ensureLocalIndexSettingsPopover();
var summary = pageUiState.localIndexSummary || {};
renderLocalIndexRangeRows(popover, currentLocalIndexRangeValues(popover).concat(['']), localIndexStatusKind(summary.status || {}), false, summary.status || {});
var inputs = Array.from(popover.querySelectorAll('[data-local-index-range-input]'));
var last = inputs[inputs.length - 1];
if (last instanceof HTMLInputElement) last.focus();
}
function removeLocalIndexRange(index) {
var popover = ensureLocalIndexSettingsPopover();
var summary = pageUiState.localIndexSummary || {};
var values = currentLocalIndexRangeValues(popover);
values.splice(Number(index || 0), 1);
if (!values.length) values = [''];
renderLocalIndexRangeRows(popover, values, localIndexStatusKind(summary.status || {}), false, summary.status || {});
}
async function persistLocalIndexSettings() {
if (!pageSettingsLocalIndexIsAvailable()) return;
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
scopeKey: pageSettingsLocalIndexScopeKey(),
loading: true,
error: ''
});
renderPageSettingsLocalIndex();
try {
var response = await fetch('/api/search/local-index/settings', {
method: 'PUT',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(Object.assign({
workspaceId: resolveWorkspaceId(document.body),
rootUri: currentRootUri(),
includePaths: localIndexIncludePathsFromForm()
}, localIndexSchedulePayloadFromForm()))
});
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 : 'local_index_settings_' + response.status);
}
await loadPageSettingsLocalIndex(true);
} catch (error) {
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
scopeKey: pageSettingsLocalIndexScopeKey(),
loading: false,
error: error instanceof Error ? error.message : String(error)
});
renderPageSettingsLocalIndex();
}
}
async function refreshLocalIndex() {
if (!pageSettingsLocalIndexIsAvailable()) return;
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
scopeKey: pageSettingsLocalIndexScopeKey(),
loading: true,
error: ''
});
renderPageSettingsLocalIndex();
try {
var response = await fetch('/api/search/local-index/refresh', {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
rootUri: currentRootUri()
})
});
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 : 'local_index_refresh_' + response.status);
}
await loadPageSettingsLocalIndex(true);
} catch (error) {
pageUiState.localIndexSummary = Object.assign({}, pageUiState.localIndexSummary || {}, {
scopeKey: pageSettingsLocalIndexScopeKey(),
loading: false,
error: error instanceof Error ? error.message : String(error)
});
renderPageSettingsLocalIndex();
}
}
function renderPageSettingsPopover() {
var popover = ensurePageSettingsPopover();
var options = currentPageOptions();
@@ -780,7 +993,6 @@ export function createSidebarPageSettingsRuntime(context) {
'<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
'<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
}
renderPageSettingsLocalIndex(popover);
}
function setActivePageSettingsTab(tabName) {
@@ -793,7 +1005,6 @@ export function createSidebarPageSettingsRuntime(context) {
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) {
@@ -848,9 +1059,8 @@ export function createSidebarPageSettingsRuntime(context) {
}
async function loadPageWidthPreferences() {
if (!currentDocumentId()) return;
var params = new URLSearchParams();
params.set('documentId', currentDocumentId());
if (currentDocumentId()) params.set('documentId', currentDocumentId());
params.set('workspaceId', resolveWorkspaceId(document.body));
var sourcePayload = currentWorkspaceSourcePayload();
Object.keys(sourcePayload || {}).forEach(function(key) {
@@ -868,6 +1078,7 @@ export function createSidebarPageSettingsRuntime(context) {
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
applyPageOptionsToShell();
if (isPageSettingsOpen()) renderPageSettingsPopover();
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
} catch (_) {}
}
@@ -876,7 +1087,8 @@ export function createSidebarPageSettingsRuntime(context) {
var next = Object.assign({}, previous, { 'localOcr.autoEnabled': Boolean(enabled) });
pageUiState.localOcrPreferences = next;
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, next);
renderPageSettingsPopover();
if (isPageSettingsOpen()) renderPageSettingsPopover();
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
try {
var response = await fetch('/api/ui/preferences', {
method: 'PUT',
@@ -895,11 +1107,13 @@ export function createSidebarPageSettingsRuntime(context) {
pageUiState.localOcrPreferences = Object.assign({ 'localOcr.autoEnabled': false }, payload.result && payload.result.localOcrPreferences || {});
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, pageUiState.localOcrPreferences);
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
renderPageSettingsPopover();
if (isPageSettingsOpen()) renderPageSettingsPopover();
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
} catch (error) {
pageUiState.localOcrPreferences = previous;
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, previous);
renderPageSettingsPopover();
if (isPageSettingsOpen()) renderPageSettingsPopover();
if (isLocalOcrSettingsOpen()) renderLocalOcrOptions(ensureLocalOcrSettingsPopover());
document.documentElement.setAttribute('data-mnote-local-ocr-preference-error', error instanceof Error ? error.message : String(error));
}
}
@@ -953,16 +1167,51 @@ export function createSidebarPageSettingsRuntime(context) {
return popover instanceof HTMLElement && !popover.hidden;
}
function openPageSettingsPopover() {
function isLocalIndexSettingsOpen() {
var popover = document.querySelector('[data-testid="mnote-local-index-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
}
function isLocalOcrSettingsOpen() {
var popover = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
}
function isAnySettingsOpen() {
return isPageSettingsOpen() || isLocalIndexSettingsOpen() || isLocalOcrSettingsOpen();
}
function openPageSettingsPopover(initialTab) {
if (!currentDocumentId()) return;
closeLocalIndexSettingsPopover();
closeLocalOcrSettingsPopover();
var popover = ensurePageSettingsPopover();
renderPageSettingsPopover();
setActivePageSettingsTab('page');
setActivePageSettingsTab(initialTab || 'page');
popover.hidden = false;
pageUiState.pageSettingsOpen = true;
updatePageSettingsTriggerState();
}
function openPageIndexSettingsPopover() {
closePageSettingsPopover();
closeLocalOcrSettingsPopover();
var popover = ensureLocalIndexSettingsPopover();
renderPageSettingsLocalIndex(popover);
popover.hidden = false;
void loadPageSettingsLocalIndex(false);
updateStandaloneSettingsTriggerState();
}
function openLocalOcrSettingsPopover() {
closePageSettingsPopover();
closeLocalIndexSettingsPopover();
var popover = ensureLocalOcrSettingsPopover();
renderLocalOcrOptions(popover);
popover.hidden = false;
updateStandaloneSettingsTriggerState();
}
function closePageSettingsPopover() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
@@ -970,6 +1219,24 @@ export function createSidebarPageSettingsRuntime(context) {
updatePageSettingsTriggerState();
}
function closeLocalIndexSettingsPopover() {
var popover = document.querySelector('[data-testid="mnote-local-index-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
updateStandaloneSettingsTriggerState();
}
function closeLocalOcrSettingsPopover() {
var popover = document.querySelector('[data-testid="mnote-local-ocr-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
updateStandaloneSettingsTriggerState();
}
function closeAllSettingsPopovers() {
closePageSettingsPopover();
closeLocalIndexSettingsPopover();
closeLocalOcrSettingsPopover();
}
function togglePageSettingsPopover() {
if (isPageSettingsOpen()) closePageSettingsPopover();
else openPageSettingsPopover();
@@ -979,28 +1246,46 @@ export function createSidebarPageSettingsRuntime(context) {
applyPageOptionsToShell();
});
window.addEventListener('mnote:open-local-ocr-settings', function() {
openLocalOcrSettingsPopover();
});
return {
addLocalIndexRange,
applyPageOptionsToShell,
closeAllSettingsPopovers,
closePageHistoryDrawer,
closeLocalIndexSettingsPopover,
closeLocalOcrSettingsPopover,
closePageSettingsPopover,
closePageShareDialog,
currentLocalOcrPreferences,
currentPageOptions,
ensureHistorySnapshotsSeeded,
ensureLocalIndexSettingsPopover,
ensureLocalOcrSettingsPopover,
ensurePageHistoryDrawer,
ensurePageSettingsPopover,
ensurePageShareDialog,
isAnySettingsOpen,
isLocalIndexSettingsOpen,
isLocalOcrSettingsOpen,
isPageSettingsOpen,
openPageHistoryDrawer,
openLocalOcrSettingsPopover,
openPageSettingsPopover,
openPageIndexSettingsPopover,
openPageShareDialog,
pageOptionIsSupported,
persistLocalOcrAutoPreference,
persistLocalIndexSettings,
persistPageOptionsPatch,
persistPageWidthPreference,
recordPageHistorySnapshot,
removeLocalIndexRange,
renderPageSettingsPopover,
setActivePageSettingsTab,
refreshLocalIndex,
togglePageSettingsPopover,
updatePageSettingsTriggerState,
writeGlobalShowHeadingNumbers,
@@ -43,7 +43,11 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
var fileTreeLazyCacheRootUri = '';
var fileTreeExpansionStorageRootUri = '';
var fileTreeExpansionRestoreTimer = 0;
var fileTreeVisibleHydrateTimer = 0;
var fileTreeVisibleHydrateQueued = false;
var fileTreeCommandBatchRefreshParents = new Map();
var FILETREE_VISIBLE_EXPANDED_HYDRATE_MAX = 20;
var FILETREE_VISIBLE_EXPANDED_HYDRATE_BATCH = 2;
var FILETREE_EXPANSION_STORAGE_KEY = 'mnote.localFileTree.expandedRelativePaths.v1';
var SIDEBAR_TREE_VIEW_STATE_KEY = 'mnote.sidebarTreeViewState.v1:{userId}:{workspaceId}:{sourceKind}:{treeKind}:{rootUriHash}:{scopeHash}';
var SIDEBAR_TREE_VIEW_STATE_API_KEY = 'sidebarTreeViewState.v1';
@@ -337,6 +341,24 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
parents.add(normalized);
}
function watchBatchPathIsMarkdown(item) {
var relativePath = String(item && (item.relativePath || item.relative_path) || '').trim().toLowerCase();
return relativePath.endsWith('.md') || relativePath.endsWith('.markdown');
}
function watchBatchEventKind(item) {
return String(item && (item.kind || item.eventKind || item.event_kind) || '').trim();
}
function watchBatchNeedsPageTreeRefresh(item) {
if (!watchBatchPathIsMarkdown(item)) return false;
var kind = watchBatchEventKind(item);
return kind.indexOf('Create') >= 0
|| kind.indexOf('Remove') >= 0
|| kind.indexOf('Modify(Name') >= 0
|| kind.indexOf('Rename') >= 0;
}
function addAffectedParentsFromCommandResult(parents, result) {
var affectedParents = Array.isArray(result && result.affectedParents)
? result.affectedParents
@@ -452,10 +474,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
affectedParents.forEach(function(parent) {
addCommandRefreshParent(parents, parent && (parent.relativePath || parent.relative_path));
});
var needsSidebarRefresh = changedPaths.some(function(item) {
var relativePath = String(item && (item.relativePath || item.relative_path) || '').trim().toLowerCase();
return relativePath.endsWith('.md') || relativePath.endsWith('.markdown');
});
var needsSidebarRefresh = changedPaths.some(watchBatchNeedsPageTreeRefresh);
document.documentElement.setAttribute('data-mnote-local-folder-watch-batch-applied', String(batch.revision || 'true'));
void Promise.all(Array.from(parents).map(function(parentRelativePath) {
return refreshFileTreeParent(parentRelativePath).catch(function(error) {
@@ -465,6 +484,8 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
})).then(function() {
if (needsSidebarRefresh) {
void refreshLocalFolderSidebarSnapshot();
} else {
document.documentElement.setAttribute('data-mnote-local-folder-watch-sidebar-refresh-skipped', 'content-only');
}
markLocalFolderWatchApplied('watch_batch');
});
@@ -587,6 +608,11 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
return Boolean(resolved && Array.isArray(resolved.items));
}
function hasProjectionItemArray(projection) {
var resolved = readProjection(projection);
return Boolean(resolved && Array.isArray(resolved.items));
}
function nodeIdOf(item) {
var runtimeFn = fileTreeRuntimeFunction('nodeIdOf');
if (runtimeFn) return runtimeFn(item);
@@ -1238,6 +1264,81 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
}, 0);
}
function scheduleFileTreeIdleTask(callback, timeout) {
if (typeof window.requestIdleCallback === 'function') {
return window.requestIdleCallback(callback, { timeout: timeout || 800 });
}
if (typeof window.requestAnimationFrame === 'function') {
return window.requestAnimationFrame(function() {
window.setTimeout(callback, 0);
});
}
return window.setTimeout(callback, 0);
}
function isFileTreeRowVisibleForHydrate(row) {
if (!(row instanceof HTMLElement)) return false;
if (typeof row.getBoundingClientRect !== 'function') return true;
var rect = row.getBoundingClientRect();
var viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
if (!viewportHeight) return true;
return rect.bottom >= -64 && rect.top <= viewportHeight + 256;
}
function fileTreeRowNeedsVisibleHydrate(row) {
if (!(row instanceof HTMLElement)) return false;
if (currentSourceKind() !== 'local_folder') return false;
if (!currentRootUri()) return false;
if (row.getAttribute('data-shell-mode') !== 'filetree') return false;
if (String(row.getAttribute('data-row-kind') || '').trim() !== 'folder') return false;
if (row.getAttribute('aria-expanded') === 'true') {
var relativePath = localFileTreeRelativePathFromRow(row);
if (!relativePath || !fileTreeExpandedRelativePaths.has(relativePath)) return false;
if (row.getAttribute('data-filetree-children-loaded') !== 'true' && row.getAttribute('data-filetree-children-loading') !== 'true') {
return isFileTreeRowVisibleForHydrate(row);
}
}
return false;
}
function collectVisibleExpandedFileTreeRowsForHydrate() {
var rows = [];
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
if (rows.length >= FILETREE_VISIBLE_EXPANDED_HYDRATE_MAX) return;
if (fileTreeRowNeedsVisibleHydrate(row)) rows.push(row);
});
return rows;
}
function scheduleHydrateVisibleExpandedFileTreeRows(reason) {
if (fileTreeVisibleHydrateQueued) return;
if (currentSourceKind() !== 'local_folder') return;
fileTreeVisibleHydrateQueued = true;
fileTreeVisibleHydrateTimer = scheduleFileTreeIdleTask(function() {
fileTreeVisibleHydrateTimer = 0;
fileTreeVisibleHydrateQueued = false;
hydrateVisibleExpandedFileTreeRows(reason || 'idle');
}, 800);
}
function hydrateVisibleExpandedFileTreeRows(reason) {
if (currentSourceKind() !== 'local_folder') return false;
var rows = collectVisibleExpandedFileTreeRowsForHydrate();
if (!rows.length) return false;
var batch = rows.slice(0, FILETREE_VISIBLE_EXPANDED_HYDRATE_BATCH);
batch.forEach(function(row) {
var button = row.querySelector('[data-rust-action="toggle"]');
void loadFileTreeChildren(row, button, { persist: false, idleHydrate: true }).then(function(loaded) {
if (loaded) {
document.documentElement.setAttribute('data-mnote-filetree-idle-hydrate-applied', String(reason || 'idle'));
scheduleHydrateVisibleExpandedFileTreeRows('cascade');
}
});
});
if (rows.length > batch.length) scheduleHydrateVisibleExpandedFileTreeRows('batch');
return true;
}
function rememberFileTreeExpansionState() {
var changed = false;
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
@@ -1300,6 +1401,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
tree.replaceChildren(template.content.cloneNode(true));
reprojectFileTreeSelectionState();
scheduleRestorePersistedFileTreeExpansionState();
scheduleHydrateVisibleExpandedFileTreeRows('render');
return true;
}
@@ -1329,28 +1431,29 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
setTreeRowExpanded(row, button, wasExpanded);
syncSidebarFileTreeSelection();
reprojectFileTreeSelectionState();
scheduleHydrateVisibleExpandedFileTreeRows('patch');
return true;
}
function renderSidebarSnapshot(payload) {
var renderedPage = false;
if (hasProjectionItems(payload)) {
if (hasProjectionItemArray(payload)) {
renderedPage = true;
void renderPageProjection(payload);
}
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
var renderedFile = fileProjection && hasProjectionItemArray(fileProjection) ? renderFileProjection(fileProjection) : false;
return renderedPage || renderedFile;
}
function renderLiveSidebarSnapshot(payload) {
var renderedPage = false;
if (hasProjectionItems(payload)) {
if (hasProjectionItemArray(payload)) {
renderedPage = true;
void renderPageProjection(payload);
}
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
var hasFileProjection = fileProjection && hasProjectionItems(fileProjection);
var hasFileProjection = fileProjection && hasProjectionItemArray(fileProjection);
if (currentFileTreeScope()) {
if (!hasFileProjection) return renderedPage;
var projectionParent = projectionParentRelativePath(fileProjection);
@@ -1473,9 +1576,8 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
var sidebarPayload = sidebarResponse && sidebarResponse.ok ? await sidebarResponse.json().catch(function() { return null; }) : null;
var resolvedSidebarPayload = sidebarPayload ? (sidebarPayload.result || sidebarPayload) : null;
var renderedPage = false;
if (resolvedSidebarPayload && hasProjectionItems(resolvedSidebarPayload)) {
renderedPage = true;
void renderPageProjection(resolvedSidebarPayload);
if (resolvedSidebarPayload && hasProjectionItemArray(resolvedSidebarPayload)) {
renderedPage = await renderPageProjection(resolvedSidebarPayload);
}
if (renderedFile && fileTreeScope) {
var fileRoot = document.getElementById('sidebar-file-tree-root');
@@ -1745,6 +1847,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
ensureFileTreeLazyCacheScope();
if (!fileTreeExpandedRelativePaths.size) return false;
var restored = false;
var needsVisibleHydrate = false;
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]').forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
if (String(row.getAttribute('data-row-kind') || '').trim() !== 'folder') return;
@@ -1763,12 +1866,12 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
restored = true;
return;
}
void loadFileTreeChildren(row, button, { persist: false }).then(function(loaded) {
if (loaded) restorePersistedFileTreeExpansionState();
});
setTreeRowExpanded(row, button, true, { persist: false });
needsVisibleHydrate = true;
restored = true;
});
if (restored) document.documentElement.setAttribute('data-mnote-filetree-expansion-restored', 'true');
if (needsVisibleHydrate) scheduleHydrateVisibleExpandedFileTreeRows('restore');
return restored;
}
@@ -1784,6 +1887,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
flushPendingSidebarTreeViewState('filetree');
flushPendingSidebarTreeViewState('pagetree');
});
document.addEventListener('scroll', function() {
scheduleHydrateVisibleExpandedFileTreeRows('scroll');
}, true);
window.addEventListener('tree:title-updated', function(event) {
var detail = event.detail || {};
@@ -1927,6 +2033,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
refreshLocalFolderSidebarSnapshot,
refreshLocalFolderAfterCommand,
removeDocumentRowForMode,
renderPageProjection,
renderSidebarSnapshot,
revealFileTreeResource,
restorePersistedFileTreeExpansionState,
@@ -200,17 +200,25 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
});
const applyPageOptionsToShell = (...args) => sidebarPageSettings.applyPageOptionsToShell(...args);
const closePageHistoryDrawer = (...args) => sidebarPageSettings.closePageHistoryDrawer(...args);
const closeAllSettingsPopovers = (...args) => sidebarPageSettings.closeAllSettingsPopovers(...args);
const closePageSettingsPopover = (...args) => sidebarPageSettings.closePageSettingsPopover(...args);
const closePageShareDialog = (...args) => sidebarPageSettings.closePageShareDialog(...args);
const currentPageOptions = (...args) => sidebarPageSettings.currentPageOptions(...args);
const ensureHistorySnapshotsSeeded = (...args) => sidebarPageSettings.ensureHistorySnapshotsSeeded(...args);
const isAnySettingsOpen = (...args) => sidebarPageSettings.isAnySettingsOpen(...args);
const isPageSettingsOpen = (...args) => sidebarPageSettings.isPageSettingsOpen(...args);
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
const openPageIndexSettingsPopover = (...args) => sidebarPageSettings.openPageIndexSettingsPopover(...args);
const openLocalOcrSettingsPopover = (...args) => sidebarPageSettings.openLocalOcrSettingsPopover(...args);
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
const addLocalIndexRange = (...args) => sidebarPageSettings.addLocalIndexRange(...args);
const persistLocalOcrAutoPreference = (...args) => sidebarPageSettings.persistLocalOcrAutoPreference(...args);
const persistLocalIndexSettings = (...args) => sidebarPageSettings.persistLocalIndexSettings(...args);
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
const persistPageWidthPreference = (...args) => sidebarPageSettings.persistPageWidthPreference(...args);
const refreshLocalIndex = (...args) => sidebarPageSettings.refreshLocalIndex(...args);
const removeLocalIndexRange = (...args) => sidebarPageSettings.removeLocalIndexRange(...args);
const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args);
const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args);
@@ -351,6 +359,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const applyRemoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyRemoveDocumentDelta(...args);
const setTreeLiveApplyError = (...args) => sidebarTreeLiveApply.setTreeLiveApplyError(...args);
const objectIdentityAttr = (...args) => sidebarTreeLiveApply.objectIdentityAttr(...args);
const renderPageProjection = (...args) => sidebarTreeLiveApply.renderPageProjection(...args);
const renderSidebarSnapshot = (...args) => sidebarTreeLiveApply.renderSidebarSnapshot(...args);
const refreshLocalFolderSidebarSnapshot = (...args) => sidebarTreeLiveApply.refreshLocalFolderSidebarSnapshot(...args);
const startLocalFolderSidebarWatch = (...args) => sidebarTreeLiveApply.startLocalFolderSidebarWatch(...args);
@@ -432,6 +441,21 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return true;
}
function openCurrentNavigationPage(trigger) {
if (currentSourceKind() === 'local_folder') {
var rootUri = currentRootUri();
var scope = currentFileTreeScope();
var workspaceId = currentWorkspaceId() || sidebarShortcutWorkspaceId() || resolveWorkspaceId(trigger || document.body);
var title = scope ? scope.split('/').filter(Boolean).pop() : '本地文件夹';
return openNavigationPageForFolder(rootUri, scope, workspaceId, title || scope || '本地文件夹');
}
var targetUrl = new URL('/', window.location.origin);
var workspaceId = currentWorkspaceId() || resolveWorkspaceId(trigger || document.body);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
window.location.assign(targetUrl.pathname + targetUrl.search);
return true;
}
function currentTopbarTitle() {
var title = document.querySelector('[data-page-title-current="true"]');
return title && title.textContent ? title.textContent.trim() : '无标题';
@@ -821,9 +845,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var sidebarPayload = await sidebarResponse.json().catch(function() { return null; });
if (!sidebarResponse.ok) throw new Error(sidebarPayload && (sidebarPayload.error || sidebarPayload.message) || 'local_page_tree_failed_' + sidebarResponse.status);
var sidebarProjection = sidebarPayload && (sidebarPayload.result || sidebarPayload) || {};
var renderedPage = renderSidebarSnapshot(Object.assign({}, sidebarProjection, {
dataset: Object.assign({}, sidebarProjection.dataset || {})
}));
var renderedPage = sidebarProjection ? await renderPageProjection(sidebarProjection) : false;
root = document.getElementById('sidebar-file-tree-root');
if (root instanceof HTMLElement) {
root.setAttribute('data-workspace-id', workspaceId);
@@ -2198,6 +2220,83 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
}
function searchResultEvidenceLocator(item) {
if (item && item.evidence && item.evidence.source && typeof item.evidence.source === 'object') return item.evidence.source;
if (item && item.source && item.source.locator && typeof item.source.locator === 'object') return item.source.locator;
if (item && Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].source && typeof item.evidence[0].source === 'object') return item.evidence[0].source;
return null;
}
function evidenceLocatorResourceKind(locator, fallback) {
return searchText(locator && (locator.resourceKind || locator.resource_kind) || fallback || '').toLowerCase();
}
function evidenceLocatorBbox(locator) {
var bbox = locator && locator.bbox;
if (Array.isArray(bbox)) return bbox.slice(0, 4).join(',');
if (bbox && typeof bbox === 'object') return [bbox.x0, bbox.y0, bbox.x1, bbox.y1].join(',');
return '';
}
function evidenceLocatorOpenUrl(locator) {
var action = locator && locator.openAction && typeof locator.openAction === 'object' ? locator.openAction : null;
return searchText(action && action.url);
}
async function openEvidenceSearchResult(item, event) {
var locator = searchResultEvidenceLocator(item);
if (!locator) {
var fallbackId = searchText(item && (item.documentId || item.nodeId || item.id));
if (fallbackId) window.location.assign('/documents/' + encodeURIComponent(fallbackId));
return;
}
var resourcePath = searchText(locator.resourcePath || locator.resource_path || locator.ownerDocumentPath || locator.owner_document_path);
var ownerDocumentId = searchText(locator.ownerDocumentId || locator.owner_document_id || item.documentId || '');
var resourceKind = evidenceLocatorResourceKind(locator, item && item.resourceType);
var openTarget = event && event.altKey ? 'side' : 'active-tab';
if (resourcePath && resourceKind && resourceKind !== 'markdown') {
var fileName = resourcePath.split('/').filter(Boolean).pop() || resourcePath;
var localFileUrl = buildLocalFileOpenUrl(resourcePath, false);
var href = resourceKind === 'pdf' ? buildPdfPreviewOpenUrl(localFileUrl, fileName) : localFileUrl;
await openLocalResourceInActiveTab({
path: resourcePath,
title: fileName,
kind: resourceKind,
href: href,
assetId: 'local-file:' + resourcePath,
documentId: ownerDocumentId || currentDocumentId() || '',
ownerDocumentId: ownerDocumentId || currentDocumentId() || '',
workspaceId: resolveWorkspaceId(document.body) || '',
sourceKind: currentSourceKind() || 'local_folder',
rootUri: searchText(locator.rootUri || locator.root_uri || currentRootUri()),
paneRole: openTarget === 'side' ? 'secondary' : 'primary',
evidenceLocator: locator,
page: locator.page,
bbox: locator.bbox,
sourceMapPath: searchText(locator.sourceMapPath || locator.source_map_path),
blockId: searchText(locator.blockId || locator.block_id),
lineRange: locator.lineRange || locator.line_range || null,
charRange: locator.charRange || locator.char_range || null
});
closeSearchModal();
return;
}
var url = evidenceLocatorOpenUrl(locator) || ('/documents/' + encodeURIComponent(ownerDocumentId || currentDocumentId() || ''));
try {
var target = new URL(url, window.location.origin);
var blockId = searchText(locator.blockId || locator.block_id);
var sourceMapPath = searchText(locator.sourceMapPath || locator.source_map_path);
if (blockId) target.searchParams.set('blockId', blockId);
if (locator.page) target.searchParams.set('page', String(locator.page));
var bbox = evidenceLocatorBbox(locator);
if (bbox) target.searchParams.set('bbox', bbox);
if (sourceMapPath) target.searchParams.set('sourceMapPath', sourceMapPath);
window.location.assign(target.pathname + target.search + target.hash);
} catch (_) {
window.location.assign(url);
}
}
function renderSearchRecentState(overlay) {
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
@@ -2246,7 +2345,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: false,
includeOcr: true,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
@@ -2263,16 +2362,20 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
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 snippet = searchText(item.snippet || item.evidence && item.evidence.quote || (Array.isArray(item.evidence) && item.evidence[0] && (item.evidence[0].quote || 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) + '">' +
var locator = searchResultEvidenceLocator(item);
var locatorAttr = locator ? escapeHtml(JSON.stringify(locator)) : '';
var index = items.indexOf(item);
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-search-result-index="' + String(index) + '" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '" data-resource-type="' + escapeHtml(resourceType) + '"' + (locatorAttr ? ' data-evidence-locator="' + locatorAttr + '"' : '') + '>' +
'<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('');
window.__mnoteSearchResults = items;
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
@@ -2473,9 +2576,30 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var indexSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-index-settings"]');
if (indexSettingsTrigger) {
e.preventDefault();
openPageIndexSettingsPopover();
return;
}
var ocrSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-ocr-settings"], [data-mnote-action="toggle-ocr-tasks"]');
if (ocrSettingsTrigger) {
e.preventDefault();
openLocalOcrSettingsPopover();
return;
}
var settingsClose = closestAction(e.target, '[data-settings-action="close"]');
if (settingsClose) {
e.preventDefault();
closeAllSettingsPopovers();
return;
}
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
if (isPageSettingsOpen() && !pageSettingsPanel) {
closePageSettingsPopover();
if (isAnySettingsOpen() && !pageSettingsPanel) {
closeAllSettingsPopovers();
}
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
@@ -2485,6 +2609,39 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var localIndexAction = closestAction(e.target, '[data-local-index-action]');
if (localIndexAction) {
e.preventDefault();
var localIndexActionName = localIndexAction.getAttribute('data-local-index-action') || '';
if (localIndexActionName === 'save') {
void persistLocalIndexSettings();
return;
}
if (localIndexActionName === 'refresh') {
void refreshLocalIndex();
return;
}
if (localIndexActionName === 'add-path') {
addLocalIndexRange();
return;
}
if (localIndexActionName === 'remove-path') {
removeLocalIndexRange(localIndexAction.getAttribute('data-local-index-path-index') || '0');
return;
}
}
var localOcrSettingsAction = closestAction(e.target, '[data-local-ocr-settings-action]');
if (localOcrSettingsAction) {
e.preventDefault();
var localOcrSettingsActionName = localOcrSettingsAction.getAttribute('data-local-ocr-settings-action') || '';
closeAllSettingsPopovers();
window.dispatchEvent(new CustomEvent('mnote:local-ocr-settings-action', {
detail: { action: localOcrSettingsActionName }
}));
return;
}
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
if (pageSettingsAction) {
e.preventDefault();
@@ -2507,6 +2664,13 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var navigationPageTrigger = closestAction(e.target, '[data-mnote-action="open-navigation-page"]');
if (navigationPageTrigger) {
e.preventDefault();
openCurrentNavigationPage(navigationPageTrigger);
return;
}
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
@@ -2518,6 +2682,24 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
closeSearchModal();
return;
}
var searchResultRow = closestAction(e.target, '[data-testid="wolai-search-result-row"]');
if (searchResultRow) {
e.preventDefault();
var index = Number(searchResultRow.getAttribute('data-search-result-index') || -1);
var item = Array.isArray(window.__mnoteSearchResults) && index >= 0 ? window.__mnoteSearchResults[index] : null;
if (!item) {
var locatorPayload = searchResultRow.getAttribute('data-evidence-locator') || '';
try {
var locator = locatorPayload ? JSON.parse(locatorPayload) : null;
item = locator ? { evidence: { source: locator }, documentId: searchResultRow.getAttribute('data-document-id') || '' } : null;
} catch (_) {
item = null;
}
}
void openEvidenceSearchResult(item || { documentId: searchResultRow.getAttribute('data-document-id') || '' }, e)
.catch(function(error) { console.warn('mnote evidence 搜索结果打开失败', error); });
return;
}
var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
if (sourceMenuTrigger) {
@@ -2847,9 +3029,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
}
}
if (event.key === 'Escape' && isPageSettingsOpen()) {
if (event.key === 'Escape' && isAnySettingsOpen()) {
event.preventDefault();
closePageSettingsPopover();
closeAllSettingsPopovers();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {