Improve LightRAG knowledge search locator alignment

This commit is contained in:
lix-2026
2026-06-08 20:35:49 +08:00
parent 9551d4c1dc
commit 0e8b03daf8
28 changed files with 5769 additions and 140 deletions
@@ -270,12 +270,27 @@ import {
const resourceHrefFromUrlState = (rootUri, path, title) => {
const fileUrl = localFileOpenUrl(rootUri, path);
if (!fileUrl) return '';
if (/\.pdf$/i.test(String(title || path || ''))) {
const name = String(title || path || '');
if (/\.pdf$/i.test(name)) {
const url = new URL('/pdf-preview', window.location.origin);
url.searchParams.set('fileUrl', fileUrl);
url.searchParams.set('fileName', title || path);
return url.toString();
}
const officeMatch = name.match(/\.([a-z0-9]+)$/i);
const officeType = officeMatch ? officeMatch[1].toLowerCase() : '';
if (['doc', 'docx', 'odt', 'rtf', 'xls', 'xlsx', 'ods', 'csv', 'ppt', 'pptx', 'odp'].indexOf(officeType) >= 0) {
const url = new URL('/office-preview', window.location.origin);
url.searchParams.set('fileUrl', fileUrl);
url.searchParams.set('fileName', title || path);
url.searchParams.set('fileType', officeType);
const workspaceId = currentWebShellWorkspaceId();
const sourceKind = currentWebShellSourceKind() || 'local_folder';
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
if (sourceKind) url.searchParams.set('sourceKind', sourceKind);
if (rootUri) url.searchParams.set('rootUri', rootUri);
return url.toString();
}
return fileUrl;
};
const applyDocumentEvidenceLocatorFromUrl = (root) => {
@@ -337,6 +352,7 @@ import {
bbox: url.searchParams.get('bbox') || undefined,
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
blockId: String(url.searchParams.get('blockId') || '').trim(),
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
lineRange: url.searchParams.get('lineRange') || null,
charRange: url.searchParams.get('charRange') || null,
};
@@ -984,9 +984,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
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 evidenceText = String(input.evidenceText || input.query || locator?.evidenceText || locator?.query || params.evidenceText || params.query || '').trim();
const searchQuery = String(input.searchQuery || locator?.searchQuery || params.searchQuery || '').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;
if (!locator && !Number.isFinite(page) && !bbox && !sourceMapPath && !blockId && !evidenceText && !lineRange && !charRange) return null;
return {
schema: 'mnote.evidence_locator.v1',
...(locator || {}),
@@ -994,6 +996,8 @@ export const createResourceTabRuntime = (dependencies = {}) => {
bbox,
sourceMapPath,
blockId,
evidenceText,
searchQuery,
lineRange,
charRange,
};
@@ -1013,13 +1017,29 @@ export const createResourceTabRuntime = (dependencies = {}) => {
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));
if (locator.evidenceText) url.searchParams.set('evidenceText', String(locator.evidenceText));
if (locator.searchQuery) url.searchParams.set('searchQuery', String(locator.searchQuery));
if (url.pathname === '/office-preview' && frame.contentWindow) {
const currentSrc = new URL(frame.getAttribute('src') || frame.src || '', window.location.origin);
if (officePreviewBaseHref(currentSrc.toString()) !== officePreviewBaseHref(url.toString())) {
frame.src = url.toString();
return;
}
const frameDoc = frame.contentDocument || null;
const previewReady = frameDoc?.documentElement?.getAttribute('data-mnote-office-preview-status') === '完成'
|| (frameDoc?.readyState === 'complete' && Boolean(frameDoc?.body?.dataset?.fileUrl));
if (!previewReady) {
frame.src = url.toString();
return;
}
frame.contentWindow.postMessage({
type: 'mnote:office-evidence-locator',
page: locator.page,
bbox,
sourceMapPath: locator.sourceMapPath || '',
blockId: locator.blockId || '',
evidenceText: locator.evidenceText || '',
searchQuery: locator.searchQuery || '',
}, window.location.origin);
return;
}
@@ -1095,6 +1115,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
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.evidenceText) entry.panel.setAttribute('data-mnote-evidence-text', String(locator.evidenceText).slice(0, 240));
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);
@@ -2286,7 +2307,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
if (!raw) return '';
try {
const url = new URL(raw, window.location.origin);
['page', 'bbox', 'sourceMapPath', 'blockId', 'lineRange', 'charRange', 'mnoteResourceReload'].forEach((key) => {
['page', 'bbox', 'sourceMapPath', 'blockId', 'lineRange', 'charRange', 'evidenceText', 'query', 'searchQuery', 'mnoteResourceReload'].forEach((key) => {
url.searchParams.delete(key);
});
return url.pathname + '?' + url.searchParams.toString();
@@ -240,10 +240,10 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
var relativePath = String(input.path || workspacePath && (workspacePath.relativePath || workspacePath.localRelativePath) || '').trim();
var rootUri = String(input.rootUri || workspacePath && workspacePath.rootUri || currentRootUri() || '').trim();
var fallback = String(input.fallback || '').trim();
if (relativePath && rootUri) return 'resource:file:' + rootUri + ':' + relativePath;
if (objectKind === 'mindmap' && assetId) return 'resource:mindmap:' + documentId + ':' + assetId;
if ((objectKind === 'only_office' || objectKind === 'office') && assetId) return 'resource:onlyoffice:' + documentId + ':' + assetId;
if (objectKind === 'pdf' && assetId) return 'resource:pdf:' + documentId + ':' + assetId;
if (relativePath && rootUri) return 'resource:file:' + rootUri + ':' + relativePath;
if (assetId && documentId) return 'resource:' + (objectKind || 'attachment') + ':' + documentId + ':' + assetId;
return fallback || assetId || '';
}
@@ -282,26 +282,32 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
if (!relativePath || !rootUri) return false;
var title = String(input && input.title || '').trim() || relativePath.split('/').pop() || relativePath;
var kind = String(input && input.kind || fileTreeIconKindForFileName(title) || '').trim();
var documentId = String(input && input.documentId || currentDocumentId() || '').trim();
var assetId = String(input && input.assetId || '').trim() || 'local-file:' + relativePath;
var officeUrl = String(input && input.officeUrl || '').trim();
if (!officeUrl && kind === 'office') {
officeUrl = buildLocalOnlyOfficeOpenUrl(relativePath, title, documentId, assetId, 'view');
}
var workspacePath = input && input.workspacePath && typeof input.workspacePath === 'object' ? input.workspacePath : null;
var objectIdentity = resourceObjectIdentityFromWorkspacePath({
workspacePath: workspacePath,
objectKind: kind,
documentId: input && input.documentId,
assetId: input && input.assetId,
documentId: documentId,
assetId: assetId,
path: relativePath,
rootUri: rootUri
});
return await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: objectIdentity,
assetId: String(input && input.assetId || '').trim() || 'local-file:' + relativePath,
assetId: assetId,
title: title,
fileName: title,
kind: kind,
rootUri: rootUri,
path: relativePath,
href: String(input && input.href || buildLocalFileOpenUrl(relativePath, false) || '').trim(),
officeUrl: String(input && input.officeUrl || '').trim(),
documentId: String(input && input.documentId || currentDocumentId() || '').trim(),
officeUrl: officeUrl,
documentId: documentId,
workspaceId: String(input && input.workspaceId || resolveWorkspaceId(document.body) || '').trim(),
workspacePath: workspacePath,
paneRole: String(input && input.paneRole || 'primary').trim() || 'primary',
@@ -310,6 +316,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
bbox: input && input.bbox,
sourceMapPath: String(input && input.sourceMapPath || '').trim(),
blockId: String(input && input.blockId || '').trim(),
evidenceText: String(input && input.evidenceText || '').trim(),
lineRange: input && input.lineRange,
charRange: input && input.charRange
});
@@ -19,47 +19,260 @@ export function createSidebarPageAiMarkdownRuntime(context) {
function renderPageAiMarkdownInline(text) {
var html = escapeHtml(String(text || ''));
var codeSpans = [];
var htmlSpans = [];
function stashHtml(value) {
var key = '\u0000HTML' + htmlSpans.length + '\u0000';
htmlSpans.push(value);
return key;
}
html = html.replace(/`([^`\n]+)`/g, function(_, code) {
var key = '\u0000CODE' + codeSpans.length + '\u0000';
codeSpans.push('<code>' + code + '</code>');
return key;
});
html = html.replace(/\[([^\]\n]+)\]\(([^)\s]+)\)/g, function(match, label, href) {
html = html.replace(/\[((?:\\.|[^\]\n])+)\]\(([^)\n]+)\)/g, function(match, label, href) {
var normalizedHref = normalizePageAiMarkdownHref(href);
if (!normalizedHref) return match;
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
return '<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + label + '</a>';
return stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + unescapePageAiMarkdownLabel(label) + '</a>');
});
html = html.replace(/(^|[\s(])((?:https?:\/\/|\/documents\/|mnote:\/\/open(?:Resource)?)[^\s<>()]+[^\s<>().,;:!?])/g, function(_, prefix, href) {
var normalizedHref = normalizePageAiMarkdownHref(href);
if (!normalizedHref) return prefix + href;
var citationAttr = isMnoteCitationHref(normalizedHref) ? ' data-page-ai-citation-link="true"' : '';
return prefix + stashHtml('<a href="' + escapeHtml(normalizedHref) + '" target="_blank" rel="noopener noreferrer"' + citationAttr + '>' + escapeHtml(normalizedHref) + '</a>');
});
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
html = html.replace(/(^|[^*])\*([^*\n]+)\*/g, '$1<em>$2</em>');
codeSpans.forEach(function(value, index) {
html = html.replace('\u0000CODE' + index + '\u0000', value);
});
htmlSpans.forEach(function(value, index) {
html = html.replace('\u0000HTML' + index + '\u0000', value);
});
return html;
}
function normalizePageAiMarkdownHref(value) {
var href = String(value || '').trim()
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'");
while (href.includes('&amp;')) href = href.replace(/&amp;/g, '&');
if (href.startsWith('<') && href.endsWith('>')) href = href.slice(1, -1).trim();
if (!href || /[\u0000-\u001f<>"']/.test(href)) return '';
var lower = href.toLowerCase();
if (lower.startsWith('javascript:') || lower.startsWith('data:') || lower.startsWith('vbscript:')) return '';
if (lower.startsWith('mnote://open')) href = normalizePageAiMnoteOpenHref(href);
href = normalizePageAiLegacyCitationHref(href);
if (href.startsWith('/') || href.startsWith('#')) return href;
if (/^https?:\/\//i.test(href) || /^mailto:/i.test(href)) return href;
return '';
}
function isMnoteCitationHref(href) {
function unescapePageAiMarkdownLabel(value) {
return String(value || '').replace(/\\([\\\[\]()*_`])/g, '$1');
}
function normalizePageAiMnoteOpenHref(href) {
try {
var url = new URL(href);
if (url.protocol !== 'mnote:' || (url.hostname !== 'open' && url.hostname !== 'openResource')) return href;
var path = String(url.searchParams.get('path') || url.searchParams.get('resourcePath') || '').trim();
if (!path) return '';
var params = new URLSearchParams();
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
if (!rootUri) {
try {
rootUri = new URL(window.location.href).searchParams.get('rootUri') || '';
} catch (_locationError) {}
}
if (rootUri) params.set('rootUri', rootUri);
params.set('path', path);
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
var value = String(url.searchParams.get(key) || '').trim();
if (value) params.set(key, value);
});
return '/api/local-folder/files/open?' + params.toString();
} catch (_error) {
return href;
}
}
function normalizePageAiLegacyCitationHref(href) {
try {
var url = new URL(href, window.location.origin);
return url.origin === window.location.origin && url.pathname.startsWith('/documents/');
var unwrappedHref = normalizePageAiSearchWrappedCitationHref(url);
if (unwrappedHref) return normalizePageAiLegacyCitationHref(unwrappedHref);
if (!isPageAiSameMnoteOrigin(url) && !isPageAiPortableMnoteCitationUrl(url)) return href;
if (url.pathname === '/api/local-folder/files/open') return url.pathname + url.search;
if (!url.pathname.startsWith('/documents/')) return href;
if (isPageAiUnsafeCitationDocumentId(url)) {
var fileOpenHref = normalizePageAiCitationFileOpenHref(url);
if (fileOpenHref) return fileOpenHref;
}
var decodedHash = '';
try {
decodedHash = decodeURIComponent(url.hash || '');
} catch (_decodeError) {
decodedHash = url.hash || '';
}
var marker = '#resource-tab-';
var markerIndex = decodedHash.indexOf(marker);
if (markerIndex < 0 || url.searchParams.get('resourceTab')) {
return url.pathname + url.search + url.hash;
}
var identity = decodedHash.slice(markerIndex + marker.length).replace(/^#+/, '').trim();
if (!identity.startsWith('resource:file:')) return url.pathname + url.search + url.hash;
url.searchParams.set('resourceTab', identity);
if (!url.searchParams.get('sourceKind')) url.searchParams.set('sourceKind', 'local_folder');
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
var prefix = 'resource:file:' + rootUri + ':';
if (rootUri && identity.startsWith(prefix) && !url.searchParams.get('resourcePath')) {
url.searchParams.set('resourcePath', identity.slice(prefix.length).replace(/^\/+/, ''));
}
url.hash = '';
return url.pathname + url.search;
} catch (_error) {
return href;
}
}
function normalizePageAiSearchWrappedCitationHref(url) {
var raw = '';
['wd', 'q', 'query'].some(function(key) {
raw = String(url.searchParams.get(key) || '').trim();
return Boolean(raw);
});
if (!raw) return '';
if (/^documents\//i.test(raw)) raw = '/' + raw;
if (!/^\/documents\//i.test(raw) && !/^https?:\/\/[^/]+\/documents\//i.test(raw) && !/^mnote:\/\/open/i.test(raw)) return '';
if (/^mnote:\/\/open/i.test(raw)) return normalizePageAiMnoteOpenHref(raw);
var nested = new URL(raw, window.location.origin);
if (!nested.pathname.startsWith('/documents/')) return '';
['sourceKind', 'rootUri', 'workspaceId', 'resourceTab', 'resourcePath', 'page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
if (nested.searchParams.get(key)) return;
var value = String(url.searchParams.get(key) || '').trim();
if (value) nested.searchParams.set(key, value);
});
return nested.pathname + nested.search;
}
function normalizePageAiCitationFileOpenHref(url) {
var rootUri = String(url.searchParams.get('rootUri') || '').trim();
var path = String(url.searchParams.get('resourcePath') || '').trim();
if (!rootUri || !path) return '';
var params = new URLSearchParams();
params.set('rootUri', rootUri);
params.set('path', path);
['page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
var value = String(url.searchParams.get(key) || '').trim();
if (value) params.set(key, value);
});
return '/api/local-folder/files/open?' + params.toString();
}
function isPageAiUnsafeCitationDocumentId(url) {
try {
var raw = String(url.pathname || '').replace(/^\/documents\//, '').split('/')[0] || '';
if (!raw) return false;
var decoded = decodeURIComponent(raw);
return decoded.indexOf('%') >= 0 || decoded.indexOf('\uFFFD') >= 0;
} catch (_error) {
return true;
}
}
function isPageAiPortableMnoteCitationUrl(url) {
try {
if (url.pathname === '/api/local-folder/files/open') {
return Boolean(String(url.searchParams.get('rootUri') || '').trim()) &&
Boolean(String(url.searchParams.get('path') || '').trim());
}
if (!url.pathname.startsWith('/documents/')) return false;
var resourceTab = String(url.searchParams.get('resourceTab') || '').trim();
return String(url.searchParams.get('sourceKind') || '').trim() === 'local_folder' ||
Boolean(String(url.searchParams.get('rootUri') || '').trim()) ||
Boolean(String(url.searchParams.get('resourcePath') || '').trim()) ||
resourceTab.startsWith('resource:file:');
} catch (_error) {
return false;
}
}
function isPageAiMarkdownTableDivider(line) {
var cells = splitPageAiMarkdownTableRow(line);
if (cells.length < 2) return false;
return cells.every(function(cell) {
return /^:?-{3,}:?$/.test(cell.trim());
});
}
function splitPageAiMarkdownTableRow(line) {
var text = String(line || '').trim();
if (!text.includes('|')) return [];
if (text.startsWith('|')) text = text.slice(1);
if (text.endsWith('|')) text = text.slice(0, -1);
return text.split('|').map(function(cell) { return cell.trim(); });
}
function renderPageAiMarkdownTable(lines, startIndex) {
if (startIndex + 1 >= lines.length || !isPageAiMarkdownTableDivider(lines[startIndex + 1])) return null;
var header = splitPageAiMarkdownTableRow(lines[startIndex]);
var divider = splitPageAiMarkdownTableRow(lines[startIndex + 1]);
if (!header.length || header.length !== divider.length) return null;
var rows = [];
var index = startIndex + 2;
while (index < lines.length && lines[index].trim() && lines[index].includes('|')) {
var cells = splitPageAiMarkdownTableRow(lines[index]);
if (!cells.length) break;
rows.push(cells);
index += 1;
}
function cellHtml(tag, value) {
return '<' + tag + '>' + renderPageAiMarkdownInline(value) + '</' + tag + '>';
}
var head = '<thead><tr>' + header.map(function(cell) { return cellHtml('th', cell); }).join('') + '</tr></thead>';
var body = rows.length
? '<tbody>' + rows.map(function(row) {
return '<tr>' + header.map(function(_, cellIndex) { return cellHtml('td', row[cellIndex] || ''); }).join('') + '</tr>';
}).join('') + '</tbody>'
: '';
return {
html: '<div class="wolai-page-ai-markdown-table-wrap"><table>' + head + body + '</table></div>',
nextIndex: index
};
}
function isMnoteCitationHref(href) {
try {
var url = new URL(href, window.location.origin);
if (!isPageAiSameMnoteOrigin(url)) return false;
return url.pathname.startsWith('/documents/') || url.pathname === '/api/local-folder/files/open';
} catch (_error) {
return false;
}
}
function isPageAiSameMnoteOrigin(url) {
try {
var current = new URL(window.location.origin);
if (url.origin === current.origin) return true;
if (url.hostname === 'mnote.local') return true;
var localNames = ['localhost', '127.0.0.1', '::1', 'mnote.local'];
return localNames.indexOf(url.hostname) >= 0 &&
localNames.indexOf(current.hostname) >= 0 &&
String(url.port || defaultPortForProtocol(url.protocol)) === String(current.port || defaultPortForProtocol(current.protocol));
} catch (_error) {
return false;
}
}
function defaultPortForProtocol(protocol) {
return protocol === 'https:' ? '443' : protocol === 'http:' ? '80' : '';
}
function renderPageAiMarkdown(content) {
var lines = String(content || '').replace(/\r\n?/g, '\n').split('\n');
var blocks = [];
@@ -88,6 +301,17 @@ export function createSidebarPageAiMarkdownRuntime(context) {
blocks.push('<pre><code>' + escapeHtml(codeLines.join('\n')) + '</code></pre>');
continue;
}
if (/^\s*-{3,}\s*$/.test(line)) {
blocks.push('<hr />');
index += 1;
continue;
}
var table = renderPageAiMarkdownTable(lines, index);
if (table) {
blocks.push(table.html);
index = table.nextIndex;
continue;
}
var heading = line.match(/^(#{1,6})\s+(.+)$/);
if (heading) {
var level = Math.min(6, heading[1].length);
@@ -758,6 +758,7 @@ export function createSidebarPageAiRenderRuntime(context) {
drawer.setAttribute('data-mnote-surface', 'page-ai');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-ai-resize-handle" data-page-ai-resize-handle title="拖动调整页面 AI 宽度" aria-hidden="true"></div>' +
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
@@ -50,6 +50,10 @@ export function createSidebarPageAiRuntime(context) {
{ id: 'changed_files', label: '最近修改' }
];
var pageAiDelegatesInstalled = false;
var PAGE_AI_DRAWER_WIDTH_STORAGE_KEY = 'mnote.page_ai.drawer_width';
var PAGE_AI_DRAWER_DEFAULT_WIDTH = 440;
var PAGE_AI_DRAWER_MIN_WIDTH = 340;
var PAGE_AI_DRAWER_MAX_WIDTH = 760;
function clonePageAiDefaultValue(value) {
if (Array.isArray(value)) return value.slice();
@@ -57,6 +61,70 @@ export function createSidebarPageAiRuntime(context) {
return value;
}
function pageAiClampDrawerWidth(width) {
var viewportMax = Math.max(PAGE_AI_DRAWER_MIN_WIDTH, Math.min(PAGE_AI_DRAWER_MAX_WIDTH, window.innerWidth - 24));
var numeric = Number(width);
if (!Number.isFinite(numeric) || numeric <= 0) numeric = PAGE_AI_DRAWER_DEFAULT_WIDTH;
return Math.max(PAGE_AI_DRAWER_MIN_WIDTH, Math.min(viewportMax, Math.round(numeric)));
}
function pageAiStoredDrawerWidth() {
try {
return pageAiClampDrawerWidth(window.localStorage ? window.localStorage.getItem(PAGE_AI_DRAWER_WIDTH_STORAGE_KEY) : PAGE_AI_DRAWER_DEFAULT_WIDTH);
} catch (_error) {
return pageAiClampDrawerWidth(PAGE_AI_DRAWER_DEFAULT_WIDTH);
}
}
function pageAiApplyDrawerWidth(drawer, width) {
var target = drawer instanceof HTMLElement ? drawer : document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (!(target instanceof HTMLElement)) return;
var next = pageAiClampDrawerWidth(width == null ? pageAiStoredDrawerWidth() : width);
target.style.setProperty('--mnote-page-ai-width', next + 'px');
}
function handlePageAiPointerDown(event, helpers) {
var closestAction = helpers && helpers.closestAction;
if (typeof closestAction !== 'function') return false;
var handle = closestAction(event.target, '[data-page-ai-resize-handle]');
if (!(handle instanceof HTMLElement)) return false;
var drawer = handle.closest('[data-testid="wolai-page-ai-drawer"]');
var panel = drawer instanceof HTMLElement ? drawer.querySelector('.wolai-page-ai-panel') : null;
if (!(drawer instanceof HTMLElement) || !(panel instanceof HTMLElement)) return false;
event.preventDefault();
var pointerId = event.pointerId;
var startX = Number(event.clientX || 0);
var startWidth = panel.getBoundingClientRect().width || pageAiStoredDrawerWidth();
drawer.setAttribute('data-page-ai-resizing', 'true');
document.documentElement.setAttribute('data-mnote-page-ai-resizing', 'true');
try {
handle.setPointerCapture(pointerId);
} catch (_error) {}
function move(nextEvent) {
var nextWidth = startWidth + (startX - Number(nextEvent.clientX || 0));
pageAiApplyDrawerWidth(drawer, nextWidth);
}
function finish(nextEvent) {
move(nextEvent);
var value = drawer.style.getPropertyValue('--mnote-page-ai-width').replace('px', '').trim();
try {
if (window.localStorage) window.localStorage.setItem(PAGE_AI_DRAWER_WIDTH_STORAGE_KEY, String(pageAiClampDrawerWidth(value)));
} catch (_error) {}
drawer.removeAttribute('data-page-ai-resizing');
document.documentElement.removeAttribute('data-mnote-page-ai-resizing');
window.removeEventListener('pointermove', move, true);
window.removeEventListener('pointerup', finish, true);
window.removeEventListener('pointercancel', finish, true);
try {
handle.releasePointerCapture(pointerId);
} catch (_error) {}
}
window.addEventListener('pointermove', move, true);
window.addEventListener('pointerup', finish, true);
window.addEventListener('pointercancel', finish, true);
return true;
}
function ensurePageAiStateFacade(state) {
var defaults = {
pageAiOpen: false,
@@ -743,9 +811,12 @@ export function createSidebarPageAiRuntime(context) {
var prefix = '/documents/';
if (!url.pathname.startsWith(prefix)) return '';
try {
return decodeURIComponent(url.pathname.slice(prefix.length).split('/')[0] || '');
var raw = url.pathname.slice(prefix.length).split('/')[0] || '';
var decoded = decodeURIComponent(raw);
if (decoded.indexOf('%') >= 0 || decoded.indexOf('\uFFFD') >= 0) return '';
return decoded;
} catch (_error) {
return url.pathname.slice(prefix.length).split('/')[0] || '';
return '';
}
}
@@ -753,6 +824,14 @@ export function createSidebarPageAiRuntime(context) {
var explicitPath = String(url.searchParams.get('resourcePath') || '').trim();
if (explicitPath) return explicitPath;
var raw = String(url.searchParams.get('resourceTab') || '').trim();
if (!raw) {
try {
var decodedHash = decodeURIComponent(String(url.hash || ''));
var marker = '#resource-tab-';
var markerIndex = decodedHash.indexOf(marker);
if (markerIndex >= 0) raw = decodedHash.slice(markerIndex + marker.length).replace(/^#+/, '').trim();
} catch (_error) {}
}
if (raw.indexOf('::') >= 0) raw = raw.slice(raw.indexOf('::') + 2);
if (!raw.startsWith('resource:file:')) return '';
var rest = raw.slice('resource:file:'.length);
@@ -761,6 +840,40 @@ export function createSidebarPageAiRuntime(context) {
return rest.slice(prefix.length).replace(/^\/+/, '');
}
function pageAiSameMnoteOrigin(url) {
try {
var current = new URL(window.location.origin);
if (url.origin === current.origin) return true;
if (url.hostname === 'mnote.local') return true;
var localNames = ['localhost', '127.0.0.1', '::1', 'mnote.local'];
function defaultPort(protocol) {
return protocol === 'https:' ? '443' : protocol === 'http:' ? '80' : '';
}
return localNames.indexOf(url.hostname) >= 0 &&
localNames.indexOf(current.hostname) >= 0 &&
String(url.port || defaultPort(url.protocol)) === String(current.port || defaultPort(current.protocol));
} catch (_error) {
return false;
}
}
function pageAiPortableCitationUrl(url) {
try {
if (url.pathname === '/api/local-folder/files/open') {
return Boolean(String(url.searchParams.get('rootUri') || '').trim()) &&
Boolean(String(url.searchParams.get('path') || '').trim());
}
if (!url.pathname.startsWith('/documents/')) return false;
var resourceTab = String(url.searchParams.get('resourceTab') || '').trim();
return String(url.searchParams.get('sourceKind') || '').trim() === 'local_folder' ||
Boolean(String(url.searchParams.get('rootUri') || '').trim()) ||
Boolean(String(url.searchParams.get('resourcePath') || '').trim()) ||
resourceTab.startsWith('resource:file:');
} catch (_error) {
return false;
}
}
function pageAiOpenCitationUrl(href) {
var url;
try {
@@ -768,7 +881,34 @@ export function createSidebarPageAiRuntime(context) {
} catch (_error) {
return false;
}
if (url.origin !== window.location.origin || !url.pathname.startsWith('/documents/')) return false;
var unwrappedHref = pageAiUnwrapSearchCitationUrl(url);
if (unwrappedHref) return pageAiOpenCitationUrl(unwrappedHref);
if (!pageAiSameMnoteOrigin(url) && !pageAiPortableCitationUrl(url)) return false;
if (url.pathname === '/api/local-folder/files/open') {
var fileRootUri = String(url.searchParams.get('rootUri') || currentRootUri() || '').trim();
var filePath = String(url.searchParams.get('path') || '').trim();
if (!filePath) return false;
void openLocalResourceInActiveTab({
path: filePath,
rootUri: fileRootUri,
documentId: currentDocumentId() || '',
workspaceId: resolveWorkspaceId(document.body) || '',
sourceKind: currentSourceKind() || 'local_folder',
page: url.searchParams.get('page') || undefined,
bbox: pageAiEvidenceBboxFromParam(url.searchParams.get('bbox')),
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
blockId: String(url.searchParams.get('blockId') || '').trim(),
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
lineRange: pageAiEvidenceRangeFromParam(url.searchParams.get('lineRange')),
charRange: pageAiEvidenceRangeFromParam(url.searchParams.get('charRange')),
openTarget: 'active-tab',
paneRole: 'primary'
}).then(function(opened) {
if (!opened) window.open(url.toString(), '_blank', 'noopener,noreferrer');
});
return true;
}
if (!url.pathname.startsWith('/documents/')) return false;
var rootUri = String(url.searchParams.get('rootUri') || currentRootUri() || '').trim();
var resourcePath = pageAiCitationResourcePath(url, rootUri);
var documentId = pageAiDocumentIdFromUrl(url) || currentDocumentId() || '';
@@ -784,6 +924,7 @@ export function createSidebarPageAiRuntime(context) {
bbox: pageAiEvidenceBboxFromParam(url.searchParams.get('bbox')),
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
blockId: String(url.searchParams.get('blockId') || '').trim(),
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
lineRange: pageAiEvidenceRangeFromParam(url.searchParams.get('lineRange')),
charRange: pageAiEvidenceRangeFromParam(url.searchParams.get('charRange')),
openTarget: 'active-tab',
@@ -1035,6 +1176,30 @@ export function createSidebarPageAiRuntime(context) {
return true;
}
function pageAiUnwrapSearchCitationUrl(url) {
try {
var raw = '';
['wd', 'q', 'query'].some(function(key) {
raw = String(url.searchParams.get(key) || '').trim();
return Boolean(raw);
});
if (!raw) return '';
if (/^documents\//i.test(raw)) raw = '/' + raw;
if (!/^\/documents\//i.test(raw) && !/^https?:\/\/[^/]+\/documents\//i.test(raw) && !/^mnote:\/\/open/i.test(raw)) return '';
if (/^mnote:\/\/open/i.test(raw)) return raw;
var nested = new URL(raw, window.location.origin);
if (!nested.pathname.startsWith('/documents/')) return '';
['sourceKind', 'rootUri', 'workspaceId', 'resourceTab', 'resourcePath', 'page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
if (nested.searchParams.get(key)) return;
var value = String(url.searchParams.get(key) || '').trim();
if (value) nested.searchParams.set(key, value);
});
return nested.pathname + nested.search;
} catch (_error) {
return '';
}
}
function pageAiPreviewValue(value) {
if (value == null || value === '') return '';
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
@@ -1259,9 +1424,12 @@ export function createSidebarPageAiRuntime(context) {
if (argsSummary) existing.argsSummary = argsSummary;
if (resultSummary) existing.resultSummary = resultSummary;
if (locations.length) existing.locations = locations;
existing.rawOutput = toolEvent && toolEvent.output;
existing.rawResult = resultSource;
if (status === 'completed') {
pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId);
}
return existing;
}
async function pageAiCancelQueuedRun(queueId) {
@@ -1735,6 +1903,7 @@ export function createSidebarPageAiRuntime(context) {
renderPageAiProviderButtons();
renderPageAiControls();
var drawer = ensurePageAiDrawer();
pageAiApplyDrawerWidth(drawer);
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
@@ -1840,13 +2009,113 @@ export function createSidebarPageAiRuntime(context) {
return message.content;
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
function pageAiAppendCitationSection(content, citations) {
var text = String(content || '');
var unique = [];
pageAiNormalizeArray(citations).forEach(function(value) {
var citation = String(value || '').trim();
if (!citation || unique.indexOf(citation) >= 0) return;
unique.push(citation);
});
var missing = unique.filter(function(citation) {
return text.indexOf(citation) < 0;
});
if (!missing.length) return text;
return text.replace(/\s+$/g, '') + '\n\n**引用**\n' + missing.map(function(citation) {
return '- ' + citation;
}).join('\n');
}
function pageAiPromptNeedsKnowledgeRagCitations(prompt) {
var text = searchText(prompt);
if (!text) return false;
var asksCitation = ['链接', '引用', '来源', '出处', '证据', '定位', 'link', 'citation', 'source'].some(function(word) {
return text.indexOf(word) >= 0;
});
if (!asksCitation) return false;
return ['lightrag', '资料库', '知识库', 'rag', '文献', '论文', '保护基'].some(function(word) {
return text.indexOf(word) >= 0;
});
}
function pageAiCleanDegradedCitationNotes(content) {
return String(content || '')
.split('\n')
.filter(function(line) {
var text = String(line || '');
return text.indexOf('来源定位降级') < 0 && text.toLowerCase().indexOf('locator degraded') < 0;
})
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function pageAiCollectKnowledgeRagApiCitations(payload) {
var references = pageAiNormalizeArray(payload && payload.references);
var citations = references.map(function(reference) {
return String(reference && reference.citationMarkdown || '').trim();
}).filter(Boolean);
var hasPreciseCitation = references.some(function(reference) {
return String(reference && reference.citationMarkdown || '').trim() && reference && reference.locatorDegraded !== true;
});
var seen = {};
return citations.filter(function(citation) {
if (seen[citation]) return false;
if (hasPreciseCitation && citation.indexOf('来源定位降级') >= 0) return false;
seen[citation] = true;
return true;
}).slice(0, 5);
}
async function pageAiAppendKnowledgeRagFallbackCitations(runId, promptText) {
if (!pageAiPromptNeedsKnowledgeRagCitations(promptText)) return;
var rootUri = currentRootUri();
if (!rootUri) return;
try {
var response = await fetch('/api/knowledge-rag/query', {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body) || undefined,
rootUri: rootUri,
query: String(promptText || '').trim(),
mode: 'mix',
topK: 8,
chunkTopK: 8,
includeChunkContent: true
})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) return;
var citations = pageAiCollectKnowledgeRagApiCitations(payload);
if (!citations.length) return;
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.runId === runId;
}) || pageUiState.pageAiMessages.filter(function(item) { return item.role === 'assistant'; }).slice(-1)[0];
if (!message) return;
var hasPrecise = citations.some(function(citation) { return citation.indexOf('来源定位降级') < 0; });
var baseContent = hasPrecise ? pageAiCleanDegradedCitationNotes(message.content) : String(message.content || '');
message.content = pageAiAppendCitationSection(baseContent, citations);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
} catch (error) {
console.warn('MNote Page AI 自动追加 LightRAG 引用失败', error);
}
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText, citations) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
var text = String(finalText || (message && message.content) || '');
var content = humanizePageAiResponse(text, promptText);
var hasPreciseCitation = pageAiNormalizeArray(citations).some(function(citation) {
return String(citation || '').indexOf('来源定位降级') < 0;
});
var contentBase = humanizePageAiResponse(text, promptText);
if (hasPreciseCitation) contentBase = pageAiCleanDegradedCitationNotes(contentBase);
var content = pageAiAppendCitationSection(contentBase, citations);
if (message) {
message.content = content;
message.streaming = false;
@@ -1859,6 +2128,123 @@ export function createSidebarPageAiRuntime(context) {
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
void pageAiAppendKnowledgeRagFallbackCitations(id, promptText);
}
function pageAiToolOutputText(value) {
var parts = [];
function visit(node) {
if (node == null) return;
if (typeof node === 'string') {
parts.push(node);
return;
}
if (typeof node === 'number' || typeof node === 'boolean') return;
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
if (typeof node !== 'object') return;
if (typeof node.text === 'string') parts.push(node.text);
if (typeof node.content === 'string') parts.push(node.content);
if (node.content && typeof node.content === 'object') visit(node.content);
if (node.output && typeof node.output === 'object') visit(node.output);
if (node.result && typeof node.result === 'object') visit(node.result);
}
visit(value);
return parts.join('\n');
}
function pageAiParseJsonMaybe(value) {
if (value && typeof value === 'object') return value;
var text = String(value || '').trim();
if (!text || (text[0] !== '{' && text[0] !== '[')) return null;
try {
return JSON.parse(text);
} catch (_error) {
var firstLine = text.split('\n')[0].trim();
if (firstLine && firstLine !== text && (firstLine[0] === '{' || firstLine[0] === '[')) {
try {
return JSON.parse(firstLine);
} catch (_lineError) {}
}
return null;
}
}
function pageAiCollectCitationMarkdowns(value) {
var citations = [];
var seen = {};
function add(value) {
var citation = String(value || '').trim();
if (!citation || seen[citation]) return;
seen[citation] = true;
citations.push(citation);
}
function visit(node) {
if (node == null) return;
if (typeof node === 'string') {
var parsed = pageAiParseJsonMaybe(node);
if (parsed) visit(parsed);
return;
}
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
if (typeof node !== 'object') return;
if (typeof node.citationMarkdown === 'string') add(node.citationMarkdown);
if (Array.isArray(node.citationMarkdowns)) {
node.citationMarkdowns.forEach(function(value) {
if (typeof value === 'string') add(value);
else visit(value);
});
}
Object.keys(node).forEach(function(key) {
visit(node[key]);
});
}
visit(value);
return citations;
}
function pageAiCollectKnowledgeRagCitations(toolItem, toolEvent) {
var toolName = String((toolItem && toolItem.toolName) || (toolEvent && (toolEvent.name || toolEvent.tool || toolEvent.toolName)) || '');
var sources = [
toolEvent,
toolItem,
toolEvent && toolEvent.output,
toolEvent && toolEvent.result,
toolEvent && toolEvent.summary,
toolItem && toolItem.rawOutput,
toolItem && toolItem.rawResult,
toolItem && toolItem.resultSummary
];
var citations = [];
var outputText = '';
sources.forEach(function(source) {
citations = citations.concat(pageAiCollectCitationMarkdowns(source));
var sourceText = pageAiToolOutputText(source);
if (sourceText) outputText += '\n' + sourceText;
var parsed = pageAiParseJsonMaybe(sourceText);
if (parsed) citations = citations.concat(pageAiCollectCitationMarkdowns(parsed));
});
var looksLikeKnowledgeRag = toolName.indexOf('knowledge_rag') >= 0 ||
toolName.indexOf('knowledge.rag') >= 0 ||
outputText.indexOf('mnote.knowledge_rag') >= 0 ||
outputText.indexOf('uiCitations') >= 0;
if (!looksLikeKnowledgeRag && citations.length === 0) return [];
var hasPreciseCitation = citations.some(function(value) {
return String(value || '').indexOf('来源定位降级') < 0;
});
var seen = {};
return citations.filter(function(value) {
var citation = String(value || '').trim();
if (!citation || seen[citation]) return false;
if (hasPreciseCitation && citation.indexOf('来源定位降级') >= 0) return false;
seen[citation] = true;
return true;
});
}
function pageAiLooksLikeBlockEdit(prompt) {
@@ -2078,6 +2464,7 @@ export function createSidebarPageAiRuntime(context) {
throw new Error(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
}
var assistantText = '';
var autoCitations = [];
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
@@ -2222,7 +2609,15 @@ export function createSidebarPageAiRuntime(context) {
} catch (_) {}
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
var toolEventPayload = null;
try {
toolEventPayload = JSON.parse(payloadText || 'null');
} catch (_) {}
var toolItem = pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
if (eventName === 'tool.completed') {
var completedCitations = pageAiCollectKnowledgeRagCitations(toolItem, toolEventPayload);
if (completedCitations.length) autoCitations = completedCitations;
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
@@ -2231,7 +2626,7 @@ export function createSidebarPageAiRuntime(context) {
}
});
if (!pageUiState.pageAiStoppedRunIds[runId]) {
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt);
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt, autoCitations);
}
currentSession = pageAiCurrentSession();
if (currentSession) {
@@ -2596,6 +2991,9 @@ export function createSidebarPageAiRuntime(context) {
document.addEventListener('click', function(event) {
handlePageAiClick(event, helpers);
});
document.addEventListener('pointerdown', function(event) {
handlePageAiPointerDown(event, helpers);
});
document.addEventListener('keydown', function(event) {
handlePageAiKeyDown(event, helpers);
});
@@ -1103,6 +1103,32 @@ export function createSidebarPageSettingsRuntime(context) {
});
}
function knowledgeRagPipelineProgress(status) {
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
var progress = pipeline.progress && typeof pipeline.progress === 'object' ? pipeline.progress : null;
if (!progress) return null;
var current = Number(progress.current || 0);
var total = Number(progress.total || 0);
if (!Number.isFinite(current) || !Number.isFinite(total) || current <= 0 || total <= 0) return null;
return {
current: Math.min(current, total),
total: total,
docId: String(progress.docId || ''),
latestMessage: String(pipeline.latestMessage || '')
};
}
function knowledgeRagSourceProgressHtml(progress) {
if (!progress) return '';
var percent = Math.max(0, Math.min(100, Math.round((progress.current / progress.total) * 100)));
var label = progress.current + ' / ' + progress.total + ' · ' + percent + '%';
return '' +
'<div class="mnote-knowledge-rag-source-progress" role="progressbar" aria-valuemin="0" aria-valuemax="' + String(progress.total) + '" aria-valuenow="' + String(progress.current) + '" aria-label="' + escapeHtml('索引进度 ' + label) + '">' +
'<div><i style="width:' + String(percent) + '%"></i></div>' +
'<span>' + escapeHtml(label) + '</span>' +
'</div>';
}
function scheduleKnowledgeRagStatusBridge(reason) {
if (!knowledgeRagIsAvailable()) return;
if (knowledgeRagBridgeTimer) window.clearTimeout(knowledgeRagBridgeTimer);
@@ -1207,18 +1233,21 @@ export function createSidebarPageSettingsRuntime(context) {
sourcesNode.innerHTML = '<div class="wolai-page-settings-index-empty">当前分类没有资料来源</div>';
return;
}
var pipelineProgress = knowledgeRagPipelineProgress(status || {});
sourcesNode.innerHTML = items.map(function(row) {
if (row.kind === 'provider') {
var doc = row.doc || {};
var providerPath = String(doc.filePath || doc.id || '');
var docStatus = String(doc.status || doc.statusGroup || '');
var failedDoc = row.category === 'unmapped_failed';
var providerProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === String(doc.id || '') ? pipelineProgress : null;
return '' +
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-kind="provider" data-knowledge-rag-source-path="' + escapeHtml(providerPath) + '">' +
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatus || 'LightRAG 文档') + '"></span>' +
'<div class="mnote-knowledge-rag-source-main">' +
'<strong title="' + escapeHtml(providerPath) + '">' + escapeHtml(providerPath || 'LightRAG 文档') + '</strong>' +
'<span>' + escapeHtml('LightRAG 未映射 · ' + (docStatus || 'unknown') + (doc.id ? ' · ' + doc.id : '')) + '</span>' +
knowledgeRagSourceProgressHtml(providerProgress) +
'</div>' +
'<div class="mnote-knowledge-rag-source-actions"><button type="button" disabled>只读</button></div>' +
'</div>';
@@ -1234,23 +1263,25 @@ export function createSidebarPageSettingsRuntime(context) {
var indexed = entry && entry.indexedAtMs && docId && !deleted && !stale && !failed && !deleting && !deletedDone;
var kind = deletedDone ? '' : (deleting ? 'indexing' : (failed || stale || deleted ? 'fault' : (indexed ? 'indexed' : 'indexing')));
var title = String(entry && entry.sourceRootRelativePath || entry && entry.lightRagFilePath || '');
var status = deletedDone ? 'LightRAG 已移除'
var statusLabel = deletedDone ? 'LightRAG 已移除'
: deleting ? '删除已提交'
: deleted ? '已删除'
: stale ? '已过期'
: failed ? '索引失败'
: indexed ? '已索引'
: (docId || providerStatus ? '正在索引' : '待提交');
var meta = docId ? status + ' · ' + docId : status;
var meta = docId ? statusLabel + ' · ' + docId : statusLabel;
var sourcePath = String(entry && entry.sourceRootRelativePath || '');
var canReindex = Boolean(sourcePath && !deleting);
var canDelete = Boolean(sourcePath && !deleted && !deletedDone && !deleting);
var entryProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === docId && !indexed && !failed && !stale && !deleted && !deletedDone ? pipelineProgress : null;
return '' +
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(status) + '"></span>' +
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(statusLabel) + '"></span>' +
'<div class="mnote-knowledge-rag-source-main">' +
'<strong title="' + escapeHtml(title) + '">' + escapeHtml(title || '未命名来源') + '</strong>' +
'<span>' + escapeHtml(meta) + '</span>' +
knowledgeRagSourceProgressHtml(entryProgress) +
'</div>' +
'<div class="mnote-knowledge-rag-source-actions">' +
'<button type="button" data-knowledge-rag-action="reindex-source" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '"' + (canReindex ? '' : ' disabled') + '>重索引</button>' +
@@ -2139,7 +2139,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
'</div>' +
'<div class="wolai-search-options-right">' +
'<span class="wolai-search-switch-control"><span>全盘资料库</span><button type="button" class="wolai-search-switch" data-search-switch="knowledge" role="switch" aria-checked="false" aria-label="全盘资料库检索"></button></span>' +
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
'<span class="wolai-search-switch-control"><span>折叠同来源</span><button type="button" class="wolai-search-switch is-on" data-search-switch="collapseSource" role="switch" aria-checked="true" aria-label="默认折叠相同来源结果"></button></span>' +
'</div>' +
'</div>' +
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
@@ -2149,12 +2151,22 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
document.body.appendChild(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
if (input) input.addEventListener('input', scheduleSearchResultsRender);
applySearchSwitchState(overlay, { collapseSource: readSearchCollapseSourcesDefault() });
if (input) input.addEventListener('input', function() {
searchUiState.hasRendered = false;
scheduleSearchResultsRender();
});
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
button.addEventListener('click', function() {
var switchName = searchText(button.getAttribute('data-search-switch'));
var isOn = button.getAttribute('aria-checked') !== 'true';
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn);
if (switchName === 'collapseSource') {
writeSearchCollapseSourcesDefault(isOn);
searchUiState.sourceCollapsed = {};
}
searchUiState.hasRendered = false;
scheduleSearchResultsRender();
});
});
@@ -2167,11 +2179,27 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var activeSearchRequestId = 0;
var searchRenderTimer = 0;
var SEARCH_COLLAPSE_SOURCES_KEY = 'mnote.search.collapseSourcesDefault.v1';
var searchUiState = {
query: '',
switches: {},
metaHtml: '',
resultsHtml: '',
items: [],
sourceCollapsed: {},
scrollTop: 0,
signature: '',
hasRendered: false
};
function searchText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
}
function searchNonWhitespaceCharCount(value) {
return String(value == null ? '' : value).replace(/\s+/g, '').length;
}
function currentWorkspaceName() {
var name = document.querySelector('.sidebar-workspace-name');
return searchText(name && name.textContent) || '当前工作区';
@@ -2208,6 +2236,92 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
}
function readSearchCollapseSourcesDefault() {
try {
var stored = window.localStorage && window.localStorage.getItem(SEARCH_COLLAPSE_SOURCES_KEY);
if (stored === '0') return false;
if (stored === '1') return true;
} catch (_) {}
return true;
}
function writeSearchCollapseSourcesDefault(value) {
try {
if (window.localStorage) window.localStorage.setItem(SEARCH_COLLAPSE_SOURCES_KEY, value ? '1' : '0');
} catch (_) {}
}
function collectSearchSwitchState(overlay) {
var state = {};
if (!(overlay instanceof HTMLElement)) return state;
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
if (!(button instanceof HTMLElement)) return;
var name = searchText(button.getAttribute('data-search-switch'));
if (!name) return;
state[name] = button.getAttribute('aria-checked') === 'true';
});
return state;
}
function applySearchSwitchState(overlay, switches) {
if (!(overlay instanceof HTMLElement) || !switches || typeof switches !== 'object') return;
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
if (!(button instanceof HTMLElement)) return;
var name = searchText(button.getAttribute('data-search-switch'));
if (!name || !Object.prototype.hasOwnProperty.call(switches, name)) return;
var isOn = Boolean(switches[name]);
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn);
});
}
function searchRequestSignature(overlay, query) {
var switches = collectSearchSwitchState(overlay);
return JSON.stringify({
query: searchText(query),
knowledge: Boolean(switches.knowledge),
page: Boolean(switches.page),
title: Boolean(switches.title),
exact: Boolean(switches.exact),
collapseSource: Boolean(switches.collapseSource),
workspaceId: resolveWorkspaceId(document.body) || '',
rootUri: currentRootUri() || ''
});
}
function saveSearchUiState(overlay) {
if (!(overlay instanceof HTMLElement)) return;
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
var query = input && 'value' in input ? searchText(input.value) : searchUiState.query;
searchUiState.query = query;
searchUiState.switches = collectSearchSwitchState(overlay);
searchUiState.metaHtml = meta ? meta.innerHTML : searchUiState.metaHtml;
searchUiState.resultsHtml = results ? results.innerHTML : searchUiState.resultsHtml;
searchUiState.items = Array.isArray(window.__mnoteSearchResults) ? window.__mnoteSearchResults.slice() : searchUiState.items;
searchUiState.scrollTop = results instanceof HTMLElement ? results.scrollTop : searchUiState.scrollTop;
searchUiState.signature = query ? searchRequestSignature(overlay, query) : '';
}
function restoreSearchUiState(overlay) {
if (!(overlay instanceof HTMLElement) || !searchUiState.hasRendered) return false;
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (input && 'value' in input) input.value = searchUiState.query || '';
applySearchSwitchState(overlay, searchUiState.switches);
if (options instanceof HTMLElement) options.hidden = !searchText(searchUiState.query);
if (meta && searchUiState.metaHtml) meta.innerHTML = searchUiState.metaHtml;
if (results && searchUiState.resultsHtml) {
results.innerHTML = searchUiState.resultsHtml;
results.scrollTop = Number(searchUiState.scrollTop || 0);
}
window.__mnoteSearchResults = Array.isArray(searchUiState.items) ? searchUiState.items.slice() : [];
return true;
}
function searchHighlightTerms(item, query) {
var info = item && item.matchInfo || item && item.evidence && item.evidence.matchInfo || null;
var terms = info && Array.isArray(info.matchedTerms) ? info.matchedTerms : [];
@@ -2258,13 +2372,113 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return html;
}
function cleanSearchDisplayText(value) {
var text = String(value == null ? '' : value);
text = text.replace(/<drawing\b[^>]*\/?>/gi, ' ');
text = text.replace(/<equation\b[^>]*>([\s\S]*?)<\/equation>/gi, '$1');
text = text.replace(/<drawing\b[^<>]*$/gi, ' ');
text = text.replace(/<\/?equat[^<>]*$/gi, ' ');
text = text.replace(/<\/?e(?:q(?:uation)?)?[^<>]*$/gi, ' ');
text = text.replace(/<[^>]+>/g, ' ');
text = text
.replace(/\b(?:equation|latex|drawing)\b(?:\s+[a-z_-]+=(?:"[^"]*"|'[^']*'|[^\s<>]+))*/gi, ' ')
.replace(/\\left|\\right|\\mathrm|\\text|\\operatorname/g, '')
.replace(/\\gt/g, '>')
.replace(/\\lt/g, '<')
.replace(/\\sim/g, '')
.replace(/\\[a-zA-Z]+/g, ' ')
.replace(/\{([^{}]*)\}/g, '$1')
.replace(/\{([^{}]*)\}/g, '$1')
.replace(/[_^]/g, '')
.replace(/[<>]\/?(?:equation|eq)\b[^<>]*>?/gi, ' ')
.replace(/<\/?e(?:q(?:uation)?)?[^<>\s]*>?/gi, ' ')
.replace(/\s+(?:equation|latex|drawing)\s+/gi, ' ');
return searchText(text);
}
function searchResultEvidenceLocator(item) {
if (item && item.locator && typeof item.locator === 'object') return item.locator;
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 searchResultSourceKey(item) {
var locator = searchResultEvidenceLocator(item);
var locatorPath = locator ? searchText(locator.resourcePath || locator.resource_path || locator.ownerDocumentPath || locator.owner_document_path) : '';
return locatorPath || searchText(item && (item.path || item.documentId || item.nodeId || item.id)) || 'unknown-source';
}
function searchResultSourceTitle(item) {
var key = searchResultSourceKey(item);
var locator = searchResultEvidenceLocator(item);
var path = locator ? searchText(locator.resourcePath || locator.resource_path || locator.ownerDocumentPath || locator.owner_document_path) : '';
var sourcePath = path || searchText(item && item.path) || key;
var title = searchText(item && (item.title || item.name || item.documentTitle));
return title || sourcePath.split('/').filter(Boolean).pop() || sourcePath || '未知来源';
}
function renderSearchResultButton(item, index, query, exact, grouped) {
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
var snippet = cleanSearchDisplayText(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');
var locator = searchResultEvidenceLocator(item);
var locatorAttr = locator ? escapeHtml(JSON.stringify(locator)) : '';
var highlightTerms = searchHighlightTerms(item, query);
var titleHtml = grouped && snippet ? '' : '<span class="wolai-search-result-title">' + highlightSearchText(title, query, exact, highlightTerms) + '</span>';
var page = locator && locator.page != null && locator.page !== '' ? String(locator.page) : '';
var metaParts = grouped ? [] : [escapeHtml(path)];
if (page) metaParts.push('第 ' + escapeHtml(page) + ' 页');
if (!grouped) metaParts.push('<span class="wolai-search-result-type">' + escapeHtml(resourceType) + '</span>');
var metaHtml = metaParts.length ? '<span class="wolai-search-result-path"><span>' + metaParts.join('</span><span>') + '</span></span>' : '';
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="' + escapeHtml(item.provider || '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">' + titleHtml +
'<span class="wolai-search-result-snippet">' + (snippet ? highlightSearchText(snippet, query, exact, highlightTerms) : escapeHtml(path)) + '</span>' +
metaHtml + '</span>' +
'</button>';
}
function renderSearchResultsHtml(items, overlay, query) {
var exact = searchSwitchValue(overlay, 'exact');
if (!searchSwitchValue(overlay, 'collapseSource')) {
return items.map(function(item, index) {
return renderSearchResultButton(item, index, query, exact, false);
}).join('');
}
var groups = [];
var groupByKey = {};
items.forEach(function(item, index) {
var key = searchResultSourceKey(item);
var group = groupByKey[key];
if (!group) {
group = { key: key, title: searchResultSourceTitle(item), path: searchText(item.path || key), resourceType: searchText(item.resourceType || item.resourceKind || 'page'), rows: [] };
groupByKey[key] = group;
groups.push(group);
}
group.rows.push({ item: item, index: index });
});
return groups.map(function(group) {
var collapsed = Object.prototype.hasOwnProperty.call(searchUiState.sourceCollapsed, group.key)
? searchUiState.sourceCollapsed[group.key]
: true;
var children = group.rows.map(function(row) {
return renderSearchResultButton(row.item, row.index, query, exact, true);
}).join('');
return '<section class="wolai-search-source-group" data-search-source-key="' + escapeHtml(group.key) + '">' +
'<button type="button" class="wolai-search-source-header" data-search-source-toggle="true" aria-expanded="' + (collapsed ? 'false' : 'true') + '">' +
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
'<span class="wolai-search-source-main"><span class="wolai-search-result-title">' + escapeHtml(group.title) + '</span>' +
'<span class="wolai-search-result-path"><span class="wolai-search-result-type">' + escapeHtml(group.resourceType) + '</span><span>' + String(group.rows.length) + ' 处</span></span></span>' +
'<span class="wolai-search-source-chevron" aria-hidden="true">' + (collapsed ? '展开' : '收起') + '</span>' +
'</button>' +
'<div class="wolai-search-source-results" data-search-source-results="true"' + (collapsed ? ' hidden' : '') + '>' + children + '</div>' +
'</section>';
}).join('');
}
function evidenceLocatorResourceKind(locator, fallback) {
return searchText(locator && (locator.resourceKind || locator.resource_kind) || fallback || '').toLowerCase();
}
@@ -2316,6 +2530,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
async function openEvidenceSearchResult(item, event) {
var locator = searchResultEvidenceLocator(item);
if (!locator) {
var citationUrl = searchText(item && (item.citationUrl || item.publicPath));
if (citationUrl) {
closeSearchModal();
window.location.assign(citationUrl);
return;
}
var fallbackId = searchText(item && (item.documentId || item.nodeId || item.id));
if (fallbackId) window.location.assign('/documents/' + encodeURIComponent(fallbackId));
return;
@@ -2323,6 +2543,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
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 overlay = document.querySelector('[data-testid="wolai-search-modal"]');
var searchInput = overlay instanceof HTMLElement ? overlay.querySelector('[data-testid="wolai-search-input"]') : null;
var searchQueryText = searchText(item && (item.query || item.searchQuery))
|| searchText(searchInput && 'value' in searchInput ? searchInput.value : '')
|| searchText(searchUiState.query);
var locatorEvidenceText = searchText(locator.evidenceText || locator.query || locator.openAction && locator.openAction.params && locator.openAction.params.query || '');
var openTarget = event && event.altKey ? 'side' : 'active-tab';
if (resourcePath && resourceKind) {
var fileName = resourcePath.split('/').filter(Boolean).pop() || resourcePath;
@@ -2347,6 +2573,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
bbox: locator.bbox,
sourceMapPath: searchText(locator.sourceMapPath || locator.source_map_path),
blockId: evidenceLocatorBlockId(locator),
evidenceText: locatorEvidenceText,
query: searchQueryText,
searchQuery: searchQueryText,
lineRange: evidenceLocatorLineRange(locator),
charRange: evidenceLocatorCharRange(locator)
});
@@ -2379,6 +2608,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
if (results) {
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
}
saveSearchUiState(overlay);
searchUiState.hasRendered = true;
}
function scheduleSearchResultsRender() {
@@ -2401,14 +2632,32 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
if (options instanceof HTMLElement) options.hidden = false;
var knowledgeMode = searchSwitchValue(overlay, 'knowledge');
if (knowledgeMode && searchNonWhitespaceCharCount(query) < 2) {
activeSearchRequestId += 1;
meta.innerHTML = '<span>资料库检索</span><span>请输入至少 2 个字再搜索</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">请输入至少 2 个字再搜索</div>';
window.__mnoteSearchResults = [];
saveSearchUiState(overlay);
searchUiState.hasRendered = true;
return;
}
var requestId = ++activeSearchRequestId;
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
try {
var response = await fetch('/api/search/documents', {
var response = await fetch(knowledgeMode ? '/api/knowledge-rag/search' : '/api/search/documents', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
body: JSON.stringify(knowledgeMode ? {
workspaceId: resolveWorkspaceId(document.body),
rootUri: currentRootUri() || null,
query: query,
mode: 'hybrid',
topK: 12,
chunkTopK: 24,
includeChunkContent: true
} : {
workspaceId: resolveWorkspaceId(document.body),
sourceKind: currentSourceKind() || null,
rootUri: currentRootUri() || null,
@@ -2418,7 +2667,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: true,
includeOcr: false,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
@@ -2428,33 +2677,25 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var payload = await response.json();
if (requestId !== activeSearchRequestId) return;
var items = Array.isArray(payload.results) ? payload.results : [];
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
meta.innerHTML = '<span>' + (knowledgeMode ? '资料库检索' : '工作区搜索') + ' · 共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (!items.length) {
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
window.__mnoteSearchResults = [];
saveSearchUiState(overlay);
searchUiState.hasRendered = true;
return;
}
results.innerHTML = items.map(function(item) {
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
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');
var locator = searchResultEvidenceLocator(item);
var locatorAttr = locator ? escapeHtml(JSON.stringify(locator)) : '';
var index = items.indexOf(item);
var exact = searchSwitchValue(overlay, 'exact');
var highlightTerms = searchHighlightTerms(item, query);
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">' + highlightSearchText(title, query, exact, highlightTerms) + '</span>' +
'<span class="wolai-search-result-snippet">' + (snippet ? highlightSearchText(snippet, query, exact, highlightTerms) : 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;
results.innerHTML = renderSearchResultsHtml(items, overlay, query);
saveSearchUiState(overlay);
searchUiState.hasRendered = true;
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
window.__mnoteSearchResults = [];
saveSearchUiState(overlay);
searchUiState.hasRendered = true;
}
}
@@ -2467,9 +2708,12 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var overlay = ensureSearchModal();
overlay.hidden = false;
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
var restored = restoreSearchUiState(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
void renderSearchResults();
var query = input && 'value' in input ? searchText(input.value) : '';
var signature = query ? searchRequestSignature(overlay, query) : '';
if (!restored || signature !== searchUiState.signature) void renderSearchResults();
if (input) {
setTimeout(function() { input.focus(); input.select(); }, 0);
}
@@ -2477,7 +2721,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
function closeSearchModal() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (overlay instanceof HTMLElement) overlay.hidden = true;
if (overlay instanceof HTMLElement) {
saveSearchUiState(overlay);
overlay.hidden = true;
}
document.documentElement.removeAttribute('data-mnote-search-modal-open');
}
@@ -2486,6 +2733,17 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
else openSearchModal();
}
function isSearchShortcutEvent(event) {
return Boolean(event && (event.metaKey || event.ctrlKey) && !event.shiftKey && !event.altKey && event.key && event.key.toLowerCase() === 'p');
}
document.addEventListener('keydown', function(event) {
if (!isSearchShortcutEvent(event)) return;
event.preventDefault();
event.stopPropagation();
openSearchModal();
}, true);
const sidebarPageAi = createSidebarPageAiRuntime({
buildLocalFileOpenUrl,
currentDocumentId,
@@ -2792,6 +3050,22 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
closeSearchModal();
return;
}
var searchSourceToggle = closestAction(e.target, '[data-search-source-toggle]');
if (searchSourceToggle) {
e.preventDefault();
var sourceGroup = searchSourceToggle.closest('.wolai-search-source-group');
var sourceResults = sourceGroup && sourceGroup.querySelector('[data-search-source-results="true"]');
var sourceKey = sourceGroup ? searchText(sourceGroup.getAttribute('data-search-source-key')) : '';
var nextCollapsed = !(sourceResults instanceof HTMLElement && sourceResults.hidden);
if (sourceResults instanceof HTMLElement) sourceResults.hidden = nextCollapsed;
searchSourceToggle.setAttribute('aria-expanded', nextCollapsed ? 'false' : 'true');
var chevron = searchSourceToggle.querySelector('.wolai-search-source-chevron');
if (chevron) chevron.textContent = nextCollapsed ? '展开' : '收起';
if (sourceKey) searchUiState.sourceCollapsed[sourceKey] = nextCollapsed;
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (overlay instanceof HTMLElement) saveSearchUiState(overlay);
return;
}
var searchResultRow = closestAction(e.target, '[data-testid="wolai-search-result-row"]');
if (searchResultRow) {
e.preventDefault();
@@ -3144,9 +3418,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
closeAllSettingsPopovers();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
if (isSearchShortcutEvent(event)) {
event.preventDefault();
toggleSearchModal();
openSearchModal();
return;
}
if (event.key === 'Escape') {
+146 -1
View File
@@ -11,6 +11,7 @@ use axum::body::Body;
use axum::http::{header, StatusCode};
use axum::response::Response;
use serde_json::{json, Value};
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::broadcast;
use tracing::{info, warn};
@@ -47,6 +48,105 @@ pub struct AcpRunBridge {
event_tx: broadcast::Sender<SseEvent>,
}
fn collect_citation_markdowns_from_value(value: &Value) -> Vec<Value> {
fn add_citation(text: &str, seen: &mut HashSet<String>, out: &mut Vec<Value>) {
let citation = text.trim();
if !citation.is_empty() && seen.insert(citation.to_string()) {
out.push(json!({ "citationMarkdown": citation }));
}
}
fn add_reference_citations(
references: &[Value],
seen: &mut HashSet<String>,
out: &mut Vec<Value>,
) -> bool {
let has_precise = references.iter().any(|reference| {
reference
.get("citationMarkdown")
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
&& reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true)
});
let mut added = false;
for reference in references {
if out.len() >= 8 {
break;
}
let Some(citation) = reference.get("citationMarkdown").and_then(Value::as_str) else {
continue;
};
if has_precise
&& reference.get("locatorDegraded").and_then(Value::as_bool) == Some(true)
{
continue;
}
let before = out.len();
add_citation(citation, seen, out);
added = added || out.len() > before;
}
added
}
fn visit(value: &Value, seen: &mut HashSet<String>, out: &mut Vec<Value>) {
if out.len() >= 8 {
return;
}
match value {
Value::String(text) => {
let trimmed = text.trim();
if (trimmed.starts_with('{') || trimmed.starts_with('['))
&& trimmed.contains("citationMarkdown")
{
if let Ok(parsed) = serde_json::from_str::<Value>(trimmed) {
visit(&parsed, seen, out);
} else if let Some(first_line) = trimmed.lines().next() {
if let Ok(parsed) = serde_json::from_str::<Value>(first_line.trim()) {
visit(&parsed, seen, out);
}
}
}
}
Value::Array(items) => {
for item in items {
visit(item, seen, out);
if out.len() >= 8 {
break;
}
}
}
Value::Object(map) => {
let has_filtered_references = map
.get("references")
.and_then(Value::as_array)
.is_some_and(|references| add_reference_citations(references, seen, out));
if let Some(citation) = map.get("citationMarkdown").and_then(Value::as_str) {
if !has_filtered_references {
add_citation(citation, seen, out);
}
}
for (key, item) in map {
if has_filtered_references
&& matches!(key.as_str(), "references" | "citations" | "uiCitations")
{
continue;
}
visit(item, seen, out);
if out.len() >= 8 {
break;
}
}
}
_ => {}
}
}
let mut seen = HashSet::new();
let mut out = Vec::new();
visit(value, &mut seen, &mut out);
out
}
impl AcpRunBridge {
/// Create a new ACP run: create session + start prompt in background.
///
@@ -213,6 +313,8 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
status,
content,
} => {
let output = json!(content);
let citation_markdowns = collect_citation_markdowns_from_value(&output);
let error = status == crate::acp_types::ToolCallStatus::Failed;
let event = if error {
"tool.failed"
@@ -227,7 +329,8 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
"toolCallId": tool_call_id,
"status": status,
"error": error,
"output": content,
"output": output,
"citationMarkdowns": citation_markdowns,
}),
})
}
@@ -426,6 +529,48 @@ mod tests {
assert_eq!(running.data["status"], "in_progress");
}
#[test]
fn acp_tool_completed_extracts_precise_ui_citations_from_prefixed_text() {
let prefix = json!({
"schema": "mnote.acp.tool_result_ui_citations.v1",
"references": [{
"citationMarkdown": "[来源定位降级:a.md](/documents/a)",
"locatorDegraded": true
}, {
"citationMarkdown": "[b.md · p.2](/documents/b?page=2)",
"locatorDegraded": false
}],
"uiCitations": [{
"citationMarkdown": "[来源定位降级:a.md](/documents/a)"
}]
})
.to_string();
let completed = acp_event_to_sse(AcpSessionEvent::ToolCallUpdate {
tool_call_id: "tool_2".into(),
status: crate::acp_types::ToolCallStatus::Completed,
content: Some(vec![crate::acp_types::ContentBlockWrapper {
wrapper_type: "content".into(),
content: crate::acp_types::TextContent {
content_type: "text".into(),
text: format!("{prefix}\n工具正文"),
},
}]),
})
.expect("tool complete");
assert_eq!(
completed.data["citationMarkdowns"][0]["citationMarkdown"].as_str(),
Some("[b.md · p.2](/documents/b?page=2)")
);
assert_eq!(
completed.data["citationMarkdowns"]
.as_array()
.unwrap()
.len(),
1
);
}
#[test]
fn acp_session_info_update_emits_session_info_updated_sse() {
let sse = acp_event_to_sse(AcpSessionEvent::SessionInfoUpdate {
@@ -112,7 +112,8 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
.collect::<Vec<_>>()
})
.unwrap_or_default();
let citations = references
let citation_references = citation_references_for_ui(&references);
let citations = citation_references
.iter()
.filter_map(|reference| {
reference
@@ -122,16 +123,23 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
let ui_citations = citations
.iter()
.map(|citation| json!({ "citationMarkdown": citation }))
.collect::<Vec<_>>();
json!({
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
"schema": "mnote.knowledge_rag.agent_query_result.v1",
"provider": payload.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
"answerGuidance": "Final answers must cite at least one returned citationMarkdown verbatim. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox.",
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
"references": references,
"citations": citations,
"uiCitations": ui_citations,
"citationRendering": "MNote UI appends uiCitations/citations after the final answer; agent must not hand-write citation links.",
"sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
"sourceScopeMode": payload.get("sourceScopeMode").cloned().unwrap_or_else(|| json!("post_filter_mapped_references")),
"rawScopeFiltered": payload.get("rawScopeFiltered").cloned().unwrap_or(Value::Bool(false)),
"rawMetadataMeaning": "provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote",
"rawStatus": payload.pointer("/raw/status").cloned().unwrap_or(Value::Null),
"rawMessage": payload.pointer("/raw/message").cloned().unwrap_or(Value::Null),
"rawMetadata": payload.pointer("/raw/metadata").cloned().unwrap_or(Value::Null),
@@ -144,6 +152,10 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
.and_then(Value::as_str)
.map(|value| value.chars().take(700).collect::<String>())
.unwrap_or_default();
let quote_diagnostics = reference
.get("contentDiagnostics")
.cloned()
.unwrap_or_else(|| quote_diagnostics(&quote));
json!({
"schema": reference.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.reference.v1")),
"provider": reference.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
@@ -151,13 +163,55 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
"sourceRootRelativePath": reference.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
"chunkId": reference.get("chunkId").cloned().unwrap_or(Value::Null),
"quote": quote,
"quoteSource": reference.get("quoteSource").cloned().unwrap_or(Value::Null),
"locator": reference.get("locator").cloned().unwrap_or(Value::Null),
"locatorDegraded": reference.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
"contentDiagnostics": quote_diagnostics,
"citationMarkdown": reference.get("citationMarkdown").cloned().unwrap_or(Value::Null),
"citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
})
}
fn citation_references_for_ui(references: &[Value]) -> Vec<&Value> {
let has_precise = references.iter().any(|reference| {
reference
.get("citationMarkdown")
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
&& reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true)
});
references
.iter()
.filter(|reference| {
reference
.get("citationMarkdown")
.and_then(Value::as_str)
.is_some_and(|value| !value.trim().is_empty())
&& (!has_precise
|| reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true))
})
.collect()
}
fn quote_diagnostics(quote: &str) -> Value {
let meaningful_lines = quote
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.filter(|line| !line.starts_with('#'))
.filter(|line| !is_markdown_image_line(line))
.collect::<Vec<_>>();
json!({
"quoteEmpty": quote.trim().is_empty(),
"quoteOnlyImagePlaceholder": !quote.trim().is_empty() && meaningful_lines.is_empty(),
"ocrTextExposed": !meaningful_lines.is_empty(),
})
}
fn is_markdown_image_line(line: &str) -> bool {
line.starts_with("![") && line.contains("](") && line.ends_with(')')
}
#[cfg(test)]
mod tests {
use super::*;
@@ -180,6 +234,11 @@ mod tests {
"quote": "scoped quote",
"locatorDegraded": true,
"citationMarkdown": "[来源定位降级:docs/a.md](/documents/local-md:docs~2Fa.md)"
}, {
"sourceRootRelativePath": "docs/b.md",
"quote": "precise quote",
"locatorDegraded": false,
"citationMarkdown": "[docs/b.md · p.2](/documents/local-md:docs~2Fb.md?page=2)"
}]
});
@@ -191,9 +250,47 @@ mod tests {
assert_eq!(compact["rawScopeFiltered"].as_bool(), Some(false));
assert!(compact.get("raw").is_none());
assert!(compact.get("chunks").is_none());
assert_eq!(
compact["rawMetadataMeaning"].as_str(),
Some("provider metadata describes query processing/retrieval bookkeeping; it is not source text evidence unless the same text appears in references[].quote")
);
assert!(compact["answerGuidance"]
.as_str()
.unwrap_or_default()
.contains("Do not copy citationMarkdown"));
assert_eq!(
compact["uiCitations"][0]["citationMarkdown"].as_str(),
Some("[docs/b.md · p.2](/documents/local-md:docs~2Fb.md?page=2)")
);
assert_eq!(compact["uiCitations"].as_array().unwrap().len(), 1);
assert_eq!(
compact["references"][0]["sourceRootRelativePath"].as_str(),
Some("docs/a.md")
);
}
#[test]
fn compact_reference_marks_image_placeholder_as_not_ocr_text() {
let payload = json!({
"references": [{
"sourceRootRelativePath": "docs/image.png",
"quote": "# image.png\n\n![image.png](<image.png>)",
"quoteSource": "chunk",
"locatorDegraded": true,
"citationMarkdown": "[来源定位降级:image.png](/documents/local-md:docs~2FPage.md)"
}]
});
let compact = compact_query_result_for_agent(payload);
let diagnostics = &compact["references"][0]["contentDiagnostics"];
assert_eq!(
diagnostics["quoteOnlyImagePlaceholder"].as_bool(),
Some(true)
);
assert_eq!(diagnostics["ocrTextExposed"].as_bool(), Some(false));
assert_eq!(
compact["references"][0]["quoteSource"].as_str(),
Some("chunk")
);
}
}
@@ -441,7 +441,7 @@ fn knowledge_rag_query_tool() -> Value {
}
json!({
"name": "mnote.knowledge_rag.query",
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的引用",
"description": "向 LightRAG 资料库提问,返回 provider 原始结果和经 MNote registry 映射、sourcePaths 后过滤的 references/citations。用户要求链接、来源、引用或证据时优先调用;返回的 citationMarkdown 由 MNote 前端自动追加成可点击来源定位,agent 不应在最终回答中手写 /documents、mnote:// 或搜索引擎包装链接",
"schemaVersion": TOOL_SCHEMA_VERSION,
"capabilityScope": ["knowledge_rag.read", "evidence.read"],
"status": "available",
@@ -335,6 +335,26 @@ pub(crate) fn citation_url_for_locator(locator: &EvidenceLocator) -> String {
if let Some(source_map_path) = locator.source_map_path.as_deref() {
append_query_param(&mut url, "sourceMapPath", source_map_path);
}
if let Some(query) = locator
.open_action
.params
.get("query")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
append_query_param(&mut url, "evidenceText", query);
}
if let Some(search_query) = locator
.open_action
.params
.get("searchQuery")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
{
append_query_param(&mut url, "searchQuery", search_query);
}
if let Some(line_range) = &locator.line_range {
append_query_param(
&mut url,
File diff suppressed because it is too large Load Diff
@@ -3685,11 +3685,12 @@ fn score_evidence_text_match(text: &str, query: &str) -> Option<EvidenceTextMatc
.map(|index| (alternative.clone(), index))
})
.collect::<Vec<_>>();
let partial_allowed = if terms.len() > 1 {
!partial_matches.is_empty()
} else {
partial_matches.len() >= 2 || term.normalized.chars().count() <= 2
};
let partial_allowed = !partial_matches.is_empty()
&& if terms.len() > 1 {
true
} else {
partial_matches.len() >= 2
};
if partial_allowed {
partial_count += 1;
if let Some((_, index)) = partial_matches.first() {
@@ -4198,6 +4199,12 @@ mod tests {
let _ = fs::remove_dir_all(&root);
}
#[test]
fn local_search_two_char_cjk_query_requires_real_match() {
assert!(score_evidence_text_match("13. N -甲基吗啉 N -氧化物", "吗啉").is_some());
assert!(score_evidence_text_match("Scope Alpha SOURCE SCOPE RAG", "吗啉").is_none());
}
#[test]
fn local_index_settings_restricts_search_and_evidence_scope() {
let root = temp_root("mnote-local-index-settings-scope");
+1
View File
@@ -85,6 +85,7 @@ pub fn build_router(state: AppState) -> Router {
.route("/api/evidence/open", post(evidence::open))
.route("/api/knowledge-rag/status", get(knowledge_rag::status))
.route("/api/knowledge-rag/ingest", post(knowledge_rag::ingest))
.route("/api/knowledge-rag/search", post(knowledge_rag::search))
.route("/api/knowledge-rag/query", post(knowledge_rag::query_rag))
.route(
"/api/knowledge-rag/open-reference",
+19 -4
View File
@@ -207,7 +207,9 @@ pub async fn documents(
&effective_workspace_id,
&root_path,
)?;
local_search_index::query_local_search_index_with_settings(
let include_ocr = filters.include_ocr.unwrap_or(false);
let limit = body.limit.unwrap_or(30);
let local_result = local_search_index::query_local_search_index_with_settings(
&root_path,
root_uri,
&effective_workspace_id,
@@ -215,11 +217,12 @@ pub async fn documents(
&user_settings,
&normalized_query,
page_id.as_deref(),
body.limit.unwrap_or(30),
limit,
filters.title_only.unwrap_or(false),
filters.exact.unwrap_or(false),
filters.include_ocr.unwrap_or(false),
)?
include_ocr,
)?;
local_result
} else {
load_search_results_with_filters(
state.config(),
@@ -1019,6 +1022,18 @@ mod tests {
payload["meta"]["boundary"]["evidenceSqliteFallback"].as_bool(),
Some(false)
);
assert_eq!(
payload["meta"]["boundary"]["kind"].as_str(),
Some("ordinary_local_search")
);
assert_eq!(
payload["meta"]["boundary"]["ocrSidecarFallback"].as_bool(),
Some(false)
);
assert_eq!(
payload["meta"]["boundary"]["knowledgeRag"].as_bool(),
Some(false)
);
assert!(payload["evidence"].as_array().expect("evidence").is_empty());
assert!(home["tags"]
.as_array()
+313 -6
View File
@@ -994,6 +994,10 @@ pub struct OfficePreviewQuery {
source_map_path: Option<String>,
#[serde(default, alias = "blockId")]
block_id: Option<String>,
#[serde(default, alias = "evidenceText")]
evidence_text: Option<String>,
#[serde(default, alias = "searchQuery")]
search_query: Option<String>,
}
pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Response {
@@ -1028,6 +1032,8 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
let target_bbox = query.bbox.unwrap_or_default();
let target_source_map_path = query.source_map_path.unwrap_or_default();
let target_block_id = query.block_id.unwrap_or_default();
let target_evidence_text = query.evidence_text.unwrap_or_default();
let target_search_query = query.search_query.unwrap_or_default();
let html = format!(
r#"<!doctype html>
<html lang="zh-CN">
@@ -1074,7 +1080,7 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
}}
</style>
</head>
<body data-file-url="{file_url}" data-file-name="{file_name}" data-file-type="{file_type}" data-page-width-content-type="{page_width_content_type}" data-workspace-id="{workspace_id}" data-mnote-source-kind="{source_kind}" data-mnote-root-uri="{root_uri}" data-document-id="{document_id}" data-evidence-page="{target_page}" data-evidence-bbox="{target_bbox}" data-evidence-source-map-path="{target_source_map_path}" data-evidence-block-id="{target_block_id}">
<body data-file-url="{file_url}" data-file-name="{file_name}" data-file-type="{file_type}" data-page-width-content-type="{page_width_content_type}" data-workspace-id="{workspace_id}" data-mnote-source-kind="{source_kind}" data-mnote-root-uri="{root_uri}" data-document-id="{document_id}" data-evidence-page="{target_page}" data-evidence-bbox="{target_bbox}" data-evidence-source-map-path="{target_source_map_path}" data-evidence-block-id="{target_block_id}" data-evidence-text="{target_evidence_text}" data-evidence-search-query="{target_search_query}">
<main class="mnote-office-preview">
<section class="mnote-office-viewer" id="mnote-office-viewer"></section>
</main>
@@ -1093,6 +1099,8 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
let evidenceBbox = body.dataset.evidenceBbox || '';
let evidenceSourceMapPath = body.dataset.evidenceSourceMapPath || '';
let evidenceBlockId = body.dataset.evidenceBlockId || '';
let evidenceText = body.dataset.evidenceText || '';
let evidenceSearchQuery = body.dataset.evidenceSearchQuery || '';
let currentPptxBuffer = null;
let pptxRenderToken = 0;
let pptxResizeTimer = 0;
@@ -1160,7 +1168,13 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
}}
function normalizeEvidenceText(value) {{
return String(value || '').replace(/\s+/g, ' ').trim();
return String(value || '')
.replace(/<[^>]+>/g, ' ')
.replace(/&nbsp;|&#160;/gi, ' ')
.replace(/[#*_`~>\[\](){{}}]+/g, ' ')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim();
}}
function markEvidenceTarget(target) {{
@@ -1266,6 +1280,7 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
if (!viewer) return false;
const compactNeedle = normalizeEvidenceText(needle).replace(/\s+/g, '');
if (!compactNeedle) return false;
if (compactNeedle.length > 160) return false;
const refs = [];
let compactText = '';
const walker = document.createTreeWalker(viewer, NodeFilter.SHOW_TEXT);
@@ -1291,6 +1306,12 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
range.setEnd(endRef.node, endRef.offset + 1);
const rect = range.getBoundingClientRect();
if (!rect || rect.width <= 0 || rect.height <= 0) return false;
const rects = Array.from(range.getClientRects()).filter(item => item && item.width > 0 && item.height > 0);
if (rects.length > 6 || rect.height > Math.min(140, window.innerHeight * 0.35)) return false;
const paragraphTarget = startRef.node?.parentElement?.closest('p, li, td, th, blockquote');
if (paragraphTarget instanceof HTMLElement && normalizeEvidenceText(paragraphTarget.textContent).length < 4000) {{
return markEvidenceTarget(paragraphTarget);
}}
let marker = viewer.querySelector('[data-mnote-office-evidence-marker="true"]');
if (!(marker instanceof HTMLElement)) {{
marker = document.createElement('div');
@@ -1361,13 +1382,17 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
let node = walker.nextNode();
while (node) {{
if (normalizeEvidenceText(node.textContent).includes(needle)) {{
const inlineTarget = wrapEvidenceTextNode(node, needle);
if (inlineTarget) return markEvidenceTarget(inlineTarget);
const target = node.parentElement && node.parentElement.closest('p, div, span, table, section') || node.parentElement;
if (target instanceof HTMLElement) {{
const rect = target.getBoundingClientRect();
if (rect.height > window.innerHeight * 1.8 || normalizeEvidenceText(target.textContent).length > 4000) break;
if (needle.length < 20) {{
const paragraphTarget = node.parentElement && node.parentElement.closest('p, li, td, th, blockquote') || target;
if (paragraphTarget instanceof HTMLElement) return markEvidenceTarget(paragraphTarget);
}}
}}
const inlineTarget = wrapEvidenceTextNode(node, needle);
if (inlineTarget) return markEvidenceTarget(inlineTarget);
return markEvidenceTarget(target);
}}
node = walker.nextNode();
@@ -1376,6 +1401,278 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
return false;
}}
function evidenceTextCandidates(text) {{
const raw = String(text || '');
const cleaned = raw
.replace(/<[^>]+>/g, ' ')
.replace(/[#*_`~>\[\](){{}}]+/g, ' ')
.replace(/\s+/g, ' ')
.trim();
const specific = [];
function pushSpecificEvidenceTerm(term) {{
let value = normalizeEvidenceText(term);
if (!value || value.length < 3) return;
const anchor = value.search(/[][\u3400-\u9fffA-Za-z0-9()\\-]{{0,18}}/);
if (anchor > 0) value = value.slice(anchor);
value = value.split(/[:;,\n]/)[0];
if (value.length < 3 || value.length > 48) return;
specific.push(value);
if (/[]$/.test(value) && value.length > 3) specific.push(value.slice(0, -1));
}}
const withoutTags = raw.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
const siliconTerms = cleaned.match(/[\u3400-\u9fffA-Za-z0-9()\\-]{{0,24}}(?:||)?/g) || [];
siliconTerms.forEach(pushSpecificEvidenceTerm);
const cjkTerms = cleaned.match(/[\u3400-\u9fff][\u3400-\u9fffA-Za-z0-9()\\-]{{2,48}}/g) || [];
const fallbackTerms = [];
cjkTerms.forEach(term => {{
const value = normalizeEvidenceText(term);
if (value.length < 3 || /^$/.test(value) || /^$/.test(value)) return;
pushSpecificEvidenceTerm(value);
const rawIndex = withoutTags.indexOf(value);
if (rawIndex >= 0) {{
const rawWindow = withoutTags.slice(rawIndex, rawIndex + value.length + 36).split(/[:;,\n]/)[0];
specific.push(normalizeEvidenceText(rawWindow));
}}
const cleanedIndex = cleaned.indexOf(value);
if (cleanedIndex >= 0) {{
const cleanedWindow = cleaned.slice(cleanedIndex, cleanedIndex + value.length + 36).split(/[:;,\n]/)[0];
specific.push(normalizeEvidenceText(cleanedWindow));
}}
if (value.endsWith('基') && value.length > 3) specific.push(value.slice(0, -1));
fallbackTerms.push(value);
}});
const candidates = [];
cleaned.split(/[;,\n]/).forEach(part => {{
const value = normalizeEvidenceText(part);
if (value.length >= 8) candidates.push(value);
if (value.length >= 28) candidates.push(value.slice(0, 28));
}});
candidates.push(...fallbackTerms);
candidates.push(cleaned, raw);
const seen = new Set();
return specific.concat(candidates)
.map(normalizeEvidenceText)
.filter(value => value.length >= 3 && !seen.has(value) && seen.add(value))
.sort((left, right) => right.length - left.length);
}}
function compactEvidenceText(value) {{
return normalizeEvidenceText(value).replace(/[\s\p{{P}}\p{{S}}]+/gu, '');
}}
function compactEvidenceTextWithoutNumbers(value) {{
return normalizeEvidenceText(value).replace(/[0-9-]+/g, '').replace(/[\s\p{{P}}\p{{S}}]+/gu, '');
}}
function evidenceLeadingAnchors(text) {{
const raw = String(text || '');
const cleaned = normalizeEvidenceText(raw);
const anchors = [];
const queryCompact = compactEvidenceText(evidenceSearchQuery);
function push(value, options) {{
const normalized = normalizeEvidenceText(value);
const allowShort = options && options.allowShort === true;
if (normalized.length < (allowShort ? 3 : 6)) return;
anchors.push(normalized.length > 80 ? normalized.slice(0, 80) : normalized);
if (normalized.length > 24) anchors.push(normalized.slice(0, 24));
}}
function pushQueryNearPrefix(value) {{
const normalized = normalizeEvidenceText(value);
if (!normalized || !queryCompact || !compactEvidenceText(normalized).includes(queryCompact)) return;
push(normalized, {{ allowShort: true }});
const queryIndex = compactEvidenceText(normalized).indexOf(queryCompact);
if (queryIndex >= 0 && normalized.length > 24) push(normalized.slice(0, 36), {{ allowShort: true }});
}}
const prefixWindow = cleaned.slice(0, 260);
const catalogMatches = prefixWindow.match(/[^,;#]{{2,56}}[,]\s*[0-9-]{{1,5}}/g) || [];
catalogMatches.slice(0, 8).forEach(match => {{
const value = normalizeEvidenceText(match);
push(value, {{ allowShort: true }});
const withoutPage = value.replace(/[,]\s*[0-9-]{{1,5}}\s*$/, '');
push(withoutPage, {{ allowShort: true }});
pushQueryNearPrefix(withoutPage);
}});
const headingMatches = prefixWindow.match(/[0-9-]+(?:\.[0-9-]+){{1,5}}\s+[^;]{{2,72}}/g) || [];
headingMatches.slice(0, 4).forEach(match => {{
const firstPart = normalizeEvidenceText(match).split(/[,]/)[0];
push(firstPart, {{ allowShort: true }});
}});
raw.split(/[\n;]/).slice(0, 4).forEach(part => {{
const value = normalizeEvidenceText(part);
if (value) push(value);
}});
cleaned.split(/[;]/).slice(0, 4).forEach(part => {{
const value = normalizeEvidenceText(part);
if (value) push(value);
}});
evidenceTextCandidates(text).forEach(candidate => {{
if (!queryCompact || compactEvidenceText(candidate).includes(queryCompact)) push(candidate);
}});
const seen = new Set();
return anchors
.map(normalizeEvidenceText)
.filter(value => {{
if (!value || seen.has(value)) return false;
const compactValue = compactEvidenceText(value);
const shortQueryAnchor = queryCompact.length >= 2
&& compactValue.includes(queryCompact)
&& compactValue.length >= queryCompact.length + 1
&& /[0-9-A-Za-z]/.test(value);
if (value.length < 6 && !shortQueryAnchor) return false;
seen.add(value);
return true;
}});
}}
function shortLeadingAnchorTarget(element, elements, index) {{
const normalized = normalizeEvidenceText(element && element.textContent || '');
if (normalized.length >= 18) return element;
for (let offset = 1; offset <= 3; offset += 1) {{
const next = elements[index + offset];
if (!(next instanceof HTMLElement)) continue;
const nextText = normalizeEvidenceText(next.textContent || '');
if (nextText.length >= 18 && evidenceElementMatchesSearchQuery(next)) return next;
}}
return element;
}}
function scrollToEvidenceLeadingAnchor(text) {{
if (!viewer) return false;
const anchors = evidenceLeadingAnchors(text);
if (!anchors.length) return false;
const queryCompact = compactEvidenceText(evidenceSearchQuery);
const elements = Array.from(viewer.querySelectorAll('p, li, td, th, blockquote'))
.filter(node => node instanceof HTMLElement)
.filter(node => {{
const normalized = normalizeEvidenceText(node.textContent || '');
return normalized.length >= 3 && normalized.length <= 1200;
}});
for (const anchor of anchors) {{
const compactAnchor = compactEvidenceText(anchor);
const compactAnchorWithoutNumbers = compactEvidenceTextWithoutNumbers(anchor);
const shortQueryAnchor = queryCompact.length >= 2
&& compactAnchor.includes(queryCompact)
&& compactAnchor.length >= queryCompact.length + 1
&& /[0-9-A-Za-z]/.test(anchor);
if (!shortQueryAnchor && compactAnchor.length < 6 && compactAnchorWithoutNumbers.length < 6) continue;
for (const element of elements) {{
const compactElement = compactEvidenceText(element.textContent || '');
if ((compactAnchor.length >= 6 || shortQueryAnchor) && compactElement.includes(compactAnchor)) {{
return markEvidenceTarget(shortQueryAnchor ? shortLeadingAnchorTarget(element, elements, elements.indexOf(element)) : element);
}}
if (
compactAnchorWithoutNumbers.length >= 8
&& /[\u3400-\u9fff]/.test(anchor)
&& compactEvidenceTextWithoutNumbers(element.textContent || '').includes(compactAnchorWithoutNumbers)
) {{
return markEvidenceTarget(element);
}}
}}
}}
return false;
}}
function evidenceParagraphAnchors(text) {{
const cleaned = normalizeEvidenceText(text);
const anchors = [];
function push(value) {{
const normalized = normalizeEvidenceText(value);
if (normalized.length < 6) return;
anchors.push(normalized.length > 140 ? normalized.slice(0, 140) : normalized);
}}
evidenceTextCandidates(text).forEach(push);
cleaned.split(/[;,\n]/).forEach(part => {{
const value = normalizeEvidenceText(part);
if (value.length >= 10) push(value);
if (value.length >= 36) push(value.slice(0, 36));
}});
if (cleaned.length >= 24) {{
for (let index = 0; index < cleaned.length; index += 48) {{
push(cleaned.slice(index, index + 96));
}}
}}
push(cleaned);
const seen = new Set();
return anchors
.map(normalizeEvidenceText)
.filter(value => value.length >= 6 && !seen.has(value) && seen.add(value))
.sort((left, right) => right.length - left.length);
}}
function scoreEvidenceParagraphElement(element, anchors) {{
if (!(element instanceof HTMLElement)) return 0;
const text = normalizeEvidenceText(element.textContent || '');
if (!text || text.length < 3 || text.length > 6000) return 0;
const compactText = compactEvidenceText(text);
const compactSearchQuery = compactEvidenceText(evidenceSearchQuery);
let score = 0;
if (compactSearchQuery.length >= 2 && compactText.includes(compactSearchQuery)) {{
score += 2400;
}}
for (const anchor of anchors) {{
const compactAnchor = compactEvidenceText(anchor);
if (!compactAnchor || compactAnchor.length < 4) continue;
if (text.includes(anchor)) {{
score += anchor.length * anchor.length * 4;
continue;
}}
if (compactText.includes(compactAnchor)) {{
score += compactAnchor.length * compactAnchor.length * 2;
continue;
}}
if (compactAnchor.length >= 14) {{
const prefix = compactAnchor.slice(0, Math.min(36, compactAnchor.length));
if (prefix.length >= 8 && compactText.includes(prefix)) score += prefix.length * 20;
}}
}}
return score;
}}
function evidenceElementMatchesSearchQuery(element) {{
if (!(element instanceof HTMLElement)) return false;
const compactSearchQuery = compactEvidenceText(evidenceSearchQuery);
if (compactSearchQuery.length < 2) return false;
return compactEvidenceText(element.textContent || '').includes(compactSearchQuery);
}}
function scrollToEvidenceParagraph(text) {{
if (!viewer) return false;
if (scrollToEvidenceLeadingAnchor(text)) return true;
const anchors = evidenceParagraphAnchors(text);
if (!anchors.length) return false;
const selector = 'p, li, td, th, blockquote, section.docx, section.mnote-docx, div';
const elements = Array.from(viewer.querySelectorAll(selector))
.filter(node => node instanceof HTMLElement)
.filter(node => {{
const normalized = normalizeEvidenceText(node.textContent || '');
if (normalized.length < 3 || normalized.length > 6000) return false;
const childBlocks = Array.from(node.children || []).filter(child => child instanceof HTMLElement && /^(P|LI|TD|TH|BLOCKQUOTE)$/.test(child.tagName));
return childBlocks.length === 0 || /^(TD|TH|SECTION)$/.test(node.tagName);
}});
let best = null;
let bestWithSearchQuery = null;
for (const element of elements) {{
const score = scoreEvidenceParagraphElement(element, anchors);
if (score <= 0) continue;
if (!best || score > best.score) best = {{ element, score }};
if (evidenceElementMatchesSearchQuery(element) && (!bestWithSearchQuery || score > bestWithSearchQuery.score)) {{
bestWithSearchQuery = {{ element, score }};
}}
}}
const threshold = Math.max(180, Math.min(800, anchors[0].length * 4));
if (bestWithSearchQuery && bestWithSearchQuery.score >= threshold) return markEvidenceTarget(bestWithSearchQuery.element);
if (!best) return false;
if (best.score < threshold) return false;
return markEvidenceTarget(best.element);
}}
function scrollToEvidenceTextCandidates(text) {{
for (const candidate of evidenceTextCandidates(text)) {{
if (scrollToEvidenceText(candidate)) return true;
}}
return false;
}}
function scrollToEvidenceCoordinate(sourceMap, block) {{
if (!viewer || !sourceMap || !block) return false;
const page = pageForEvidenceBlock(sourceMap, block);
@@ -1422,13 +1719,18 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
}}
async function applyEvidenceLocator() {{
if (!viewer || (!evidencePage && !evidenceBlockId && !evidenceBbox)) return;
if (!viewer || (!evidencePage && !evidenceBlockId && !evidenceBbox && !evidenceText)) return;
try {{
const sourceMap = await fetchEvidenceSourceMap();
const block = findEvidenceBlockInSourceMap(sourceMap);
if (block && scrollToEvidenceText(block.text)) return;
if (evidenceText && scrollToEvidenceParagraph(evidenceText)) return;
if (block && scrollToEvidenceParagraph(block.text)) return;
if (evidenceText && scrollToEvidenceTextCandidates(evidenceText)) return;
if (block && scrollToEvidenceTextCandidates(block.text)) return;
if (scrollToEvidenceCoordinate(sourceMap, block)) return;
}} catch (_) {{}}
if (evidenceText && scrollToEvidenceParagraph(evidenceText)) return;
if (evidenceText && scrollToEvidenceTextCandidates(evidenceText)) return;
scrollToEvidencePageFallback();
}}
@@ -1438,10 +1740,14 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
evidenceBbox = String(next.bbox || '');
evidenceSourceMapPath = String(next.sourceMapPath || '');
evidenceBlockId = String(next.blockId || '');
evidenceText = String(next.evidenceText || next.query || '');
evidenceSearchQuery = String(next.searchQuery || '');
body.dataset.evidencePage = evidencePage ? String(evidencePage) : '';
body.dataset.evidenceBbox = evidenceBbox;
body.dataset.evidenceSourceMapPath = evidenceSourceMapPath;
body.dataset.evidenceBlockId = evidenceBlockId;
body.dataset.evidenceText = evidenceText;
body.dataset.evidenceSearchQuery = evidenceSearchQuery;
document.documentElement.removeAttribute('data-mnote-office-evidence-applied');
void applyEvidenceLocator();
}}
@@ -1663,6 +1969,7 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
target_bbox = escape_html(&target_bbox),
target_source_map_path = escape_html(&target_source_map_path),
target_block_id = escape_html(&target_block_id),
target_evidence_text = escape_html(&target_evidence_text),
);
let mut response = Html(html).into_response();
stamp_shell_headers(response.headers_mut(), "office-preview");
@@ -759,9 +759,14 @@ mod tests {
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiClick"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiPointerDown"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("mnote.page_ai.drawer_width"));
assert!(SIDEBAR_PAGE_AI_RENDER_RUNTIME_JS.contains("data-page-ai-resize-handle"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function pageAiOpenCitationUrl"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("a[data-page-ai-citation-link=\"true\"]"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("data-page-ai-citation-link=\"true\""));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("renderPageAiMarkdownTable"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("wolai-page-ai-markdown-table-wrap"));
assert!(SIDEBAR_PAGE_AI_MARKDOWN_RUNTIME_JS.contains("target=\"_blank\""));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function handlePageAiChange"));
assert!(SIDEBAR_PAGE_AI_RUNTIME_JS.contains("function installPageAiDelegates"));
+167 -1
View File
@@ -1243,6 +1243,14 @@ html[data-mnote-sidebar-resizing="true"] .mnote-sidebar-resizer::before {
.wolai-search-results{overflow:auto;padding:6px}
.wolai-search-result-row{width:100%;min-height:54px;display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border:0;border-radius:4px;background:transparent;color:#37352F;cursor:pointer;text-align:left;font:inherit}
.wolai-search-result-row:hover{background:rgba(55,53,47,.08)}
.wolai-search-source-group{border-radius:4px}
.wolai-search-source-group + .wolai-search-source-group{margin-top:4px}
.wolai-search-source-header{width:100%;min-height:46px;display:flex;align-items:flex-start;gap:10px;padding:8px 10px;border:0;border-radius:4px;background:transparent;color:#37352F;cursor:pointer;text-align:left;font:inherit}
.wolai-search-source-header:hover{background:rgba(55,53,47,.08)}
.wolai-search-source-main{min-width:0;display:flex;flex:1 1 auto;flex-direction:column;gap:3px}
.wolai-search-source-results{padding-left:18px}
.wolai-search-source-results[hidden]{display:none!important}
.wolai-search-source-chevron{flex:0 0 auto;margin-top:2px;color:#8B8780;font-size:11px;line-height:1.35}
.wolai-search-result-icon{width:18px;height:18px;margin-top:2px;color:#8B8780}
.wolai-search-result-main{min-width:0;display:flex;flex-direction:column;gap:3px}
.wolai-search-result-title{color:#2F2D29;font-size:14px;line-height:1.35;word-break:break-word}
@@ -4008,6 +4016,34 @@ body {
white-space: nowrap;
}
.mnote-knowledge-rag-source-progress {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 8px;
align-items: center;
width: 100%;
}
.mnote-knowledge-rag-source-progress div {
height: 5px;
overflow: hidden;
border-radius: 999px;
background: #ECE9E3;
}
.mnote-knowledge-rag-source-progress i {
display: block;
height: 100%;
border-radius: inherit;
background: #2F7D4A;
}
.mnote-knowledge-rag-source-progress span {
color: #5F5A54;
font: 10px/14px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
white-space: nowrap;
}
.mnote-knowledge-rag-source-actions {
display: flex;
gap: 6px;
@@ -4559,6 +4595,7 @@ body {
right: 12px;
bottom: 12px;
z-index: 89;
--mnote-page-ai-width: 440px;
}
.wolai-page-ai-drawer[hidden] {
@@ -4566,7 +4603,7 @@ body {
}
.wolai-page-ai-panel {
width: min(440px, calc(100vw - 24px));
width: min(var(--mnote-page-ai-width), calc(100vw - 24px));
height: 100%;
display: flex;
flex-direction: column;
@@ -4578,6 +4615,37 @@ body {
box-shadow: -18px 0 42px rgba(27, 28, 28, 0.14);
}
.wolai-page-ai-resize-handle {
position: absolute;
top: 10px;
bottom: 10px;
left: -6px;
width: 12px;
cursor: col-resize;
touch-action: none;
}
.wolai-page-ai-resize-handle::before {
position: absolute;
top: 14px;
bottom: 14px;
left: 5px;
width: 2px;
border-radius: 999px;
background: transparent;
content: "";
}
.wolai-page-ai-resize-handle:hover::before,
.wolai-page-ai-drawer[data-page-ai-resizing="true"] .wolai-page-ai-resize-handle::before {
background: rgba(27, 28, 28, 0.18);
}
html[data-mnote-page-ai-resizing="true"] {
cursor: col-resize;
user-select: none;
}
.wolai-page-ai-header-copy {
display: flex;
flex-direction: column;
@@ -5191,6 +5259,96 @@ body {
white-space: pre-wrap;
}
.wolai-page-ai-message-text > * {
white-space: normal;
}
.wolai-page-ai-message-text p,
.wolai-page-ai-message-text ul,
.wolai-page-ai-message-text ol,
.wolai-page-ai-message-text pre,
.wolai-page-ai-message-text table {
margin: 6px 0;
}
.wolai-page-ai-message-text h1,
.wolai-page-ai-message-text h2,
.wolai-page-ai-message-text h3,
.wolai-page-ai-message-text h4,
.wolai-page-ai-message-text h5,
.wolai-page-ai-message-text h6 {
margin: 8px 0 4px;
color: #1B1C1C;
font-size: 14px;
font-weight: 700;
line-height: 20px;
}
.wolai-page-ai-message-text ul,
.wolai-page-ai-message-text ol {
padding-left: 18px;
}
.wolai-page-ai-message-text a {
color: #2563EB;
overflow-wrap: anywhere;
text-decoration: underline;
text-underline-offset: 2px;
}
.wolai-page-ai-message-text pre {
overflow: auto;
padding: 8px 10px;
border-radius: 6px;
background: #F7F6F4;
font: 12px/18px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
}
.wolai-page-ai-message-text code {
border-radius: 4px;
padding: 1px 3px;
background: #F1F0EE;
font: 12px/18px "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
}
.wolai-page-ai-message-text pre code {
padding: 0;
background: transparent;
}
.wolai-page-ai-message-text hr {
height: 1px;
margin: 8px 0;
border: 0;
background: rgba(27, 28, 28, 0.12);
}
.wolai-page-ai-markdown-table-wrap {
max-width: 100%;
overflow-x: auto;
}
.wolai-page-ai-markdown-table-wrap table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
background: #FFFFFF;
}
.wolai-page-ai-markdown-table-wrap th,
.wolai-page-ai-markdown-table-wrap td {
min-width: 72px;
border: 1px solid rgba(27, 28, 28, 0.12);
padding: 5px 7px;
text-align: left;
vertical-align: top;
}
.wolai-page-ai-markdown-table-wrap th {
background: #F7F6F4;
font-weight: 700;
}
button.wolai-page-ai-message-text {
width: 100%;
border: 0;
@@ -5636,6 +5794,10 @@ button.wolai-page-ai-message-text {
width: min(100vw - 24px, 420px);
}
.wolai-page-ai-resize-handle {
display: none;
}
.wolai-page-ai-settings-grid,
.wolai-page-ai-settings-head {
grid-template-columns: minmax(0, 1fr);
@@ -5710,6 +5872,10 @@ button.wolai-page-ai-message-text {
.wolai-page-ai-panel {
width: calc(100vw - 24px);
}
.wolai-page-ai-resize-handle {
display: none;
}
}
"##;