Improve local evidence search and AI capabilities
This commit is contained in:
@@ -418,6 +418,21 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
}
|
||||
};
|
||||
|
||||
const officeOpenModeForEntry = (entry) => {
|
||||
if (!entry || normalizeResourceTabKind(entry) !== 'office') return '';
|
||||
const href = String(entry.passiveFrameSrc || entry.officeUrl || entry.href || '').trim()
|
||||
|| String(entry.panel?.querySelector?.('iframe.mnote-resource-tab-frame')?.getAttribute?.('src') || '').trim();
|
||||
if (href) {
|
||||
try {
|
||||
const url = new URL(href, window.location.origin);
|
||||
if (url.pathname === '/office-preview' || url.pathname === '/office-n') return 'preview';
|
||||
if (url.pathname === '/onlyoffice' || url.pathname.startsWith('/office/')) return 'onlyoffice_live';
|
||||
} catch (_) {}
|
||||
}
|
||||
if (entry.onlyofficeSessionId || entry.bridgeSessionId) return 'onlyoffice_live';
|
||||
return 'preview';
|
||||
};
|
||||
|
||||
const openEditorsSnapshotEntry = (entry, key, generatedAt = Date.now()) => {
|
||||
const active = entry?.tab instanceof HTMLElement
|
||||
? entry.tab.getAttribute('aria-selected') === 'true'
|
||||
@@ -432,15 +447,23 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const kind = normalizeResourceTabKind(entry);
|
||||
const dirtyState = resourceTabCloseGuardReason(entry?.session);
|
||||
const officeBridgeDebug = kind === 'office' ? officeBridgeDebugForEntry(entry) : null;
|
||||
const officeOpenMode = kind === 'office' ? officeOpenModeForEntry(entry) : '';
|
||||
const onlyofficeSessionId = String(
|
||||
officeBridgeDebug?.bridgeSessionId
|
||||
|| entry?.onlyofficeSessionId
|
||||
|| entry?.bridgeSessionId
|
||||
|| '',
|
||||
).trim();
|
||||
const snapshotResourceKind = kind === 'office'
|
||||
? (officeOpenMode === 'onlyoffice_live' || onlyofficeSessionId ? 'only_office' : 'attachment')
|
||||
: kind;
|
||||
const workspacePathSeed = entry?.workspacePath && typeof entry.workspacePath === 'object'
|
||||
? { ...entry.workspacePath, resourceKind: snapshotResourceKind }
|
||||
: entry?.workspacePath;
|
||||
return {
|
||||
objectIdentity,
|
||||
workspacePath: buildWorkspacePath({
|
||||
workspacePath: workspacePathSeed,
|
||||
workspaceId,
|
||||
sourceKind,
|
||||
rootUri,
|
||||
@@ -448,8 +471,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
documentId,
|
||||
objectIdentity,
|
||||
assetId,
|
||||
resourceKind: kind,
|
||||
workspacePath: entry?.workspacePath,
|
||||
resourceKind: snapshotResourceKind,
|
||||
}),
|
||||
paneRole: normalizePaneRole(entry?.paneRole),
|
||||
documentId,
|
||||
@@ -465,13 +487,14 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
dirtyGuard: dirtyState,
|
||||
assetId,
|
||||
path: relativePath,
|
||||
officeOpenMode,
|
||||
onlyofficeSessionId,
|
||||
bridgeSessionId: onlyofficeSessionId,
|
||||
bridgeSessionReady: Boolean(onlyofficeSessionId),
|
||||
bridgeDocumentId: String(officeBridgeDebug?.documentId || '').trim(),
|
||||
bridgeAssetId: String(officeBridgeDebug?.assetId || '').trim(),
|
||||
lastActiveAt: Number(entry?.session?.lastActiveAt || entry?.lastActiveAt || 0) || (active ? generatedAt : 0),
|
||||
preview: false,
|
||||
preview: officeOpenMode === 'preview',
|
||||
pinned: false,
|
||||
};
|
||||
};
|
||||
@@ -848,6 +871,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
panel.setAttribute('data-mnote-object-identity', objectIdentity);
|
||||
panel.setAttribute('data-pane-role', paneRole);
|
||||
panel.setAttribute('data-resource-kind', kind);
|
||||
panel.setAttribute('data-resource-path', String(input.path || '').trim());
|
||||
panel.hidden = true;
|
||||
nodes.strip.append(tab);
|
||||
nodes.panelRoot.append(panel);
|
||||
@@ -989,6 +1013,16 @@ 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 (url.pathname === '/office-preview' && frame.contentWindow) {
|
||||
frame.contentWindow.postMessage({
|
||||
type: 'mnote:office-evidence-locator',
|
||||
page: locator.page,
|
||||
bbox,
|
||||
sourceMapPath: locator.sourceMapPath || '',
|
||||
blockId: locator.blockId || '',
|
||||
}, window.location.origin);
|
||||
return;
|
||||
}
|
||||
frame.src = url.toString();
|
||||
} catch (_) {}
|
||||
};
|
||||
@@ -1015,11 +1049,22 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
};
|
||||
|
||||
const applyEvidenceLocatorToTextPanel = (entry, locator) => {
|
||||
if (!(entry?.panel instanceof HTMLElement) || !locator?.blockId) return;
|
||||
if (!(entry?.panel instanceof HTMLElement) || !locator) return;
|
||||
const root = entry.panel.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
const selector = `[data-block-id="${cssSafe(locator.blockId)}"]`;
|
||||
const target = root.querySelector(selector);
|
||||
const selector = locator.blockId ? `[data-block-id="${cssSafe(locator.blockId)}"]` : '';
|
||||
let target = selector ? root.querySelector(selector) : null;
|
||||
const query = String(locator?.openAction?.params?.query || locator?.query || '').trim();
|
||||
if (!(target instanceof HTMLElement) && query) {
|
||||
target = Array.from(root.querySelectorAll('.ProseMirror [data-block-id]'))
|
||||
.find((node) => node instanceof HTMLElement && String(node.textContent || '').includes(query)) || null;
|
||||
}
|
||||
const lineStart = Number(locator?.lineRange?.start ?? locator?.line_range?.start ?? 0);
|
||||
if (!(target instanceof HTMLElement) && Number.isFinite(lineStart) && lineStart > 0) {
|
||||
const blocks = Array.from(root.querySelectorAll('.ProseMirror [data-block-id]'))
|
||||
.filter((node) => node instanceof HTMLElement);
|
||||
target = blocks[Math.max(0, Math.min(blocks.length - 1, lineStart - 1))] || null;
|
||||
}
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
root.querySelectorAll('[data-mnote-evidence-text-highlight="true"]').forEach((node) => {
|
||||
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-text-highlight');
|
||||
@@ -1364,6 +1409,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
rootUri: '',
|
||||
jobsBySource: new Map(),
|
||||
drawerOpen: false,
|
||||
taskFilter: 'active',
|
||||
eventSource: null,
|
||||
fileTreeRefreshKeys: new Set(),
|
||||
};
|
||||
@@ -1467,13 +1513,36 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
: status || '未知';
|
||||
};
|
||||
|
||||
const localOcrTaskCategory = (job) => {
|
||||
const status = String(job?.status || '').trim();
|
||||
if (['failed', 'stale', 'retry_scheduled'].includes(status)) return 'attention';
|
||||
if (['done', 'succeeded', 'success'].includes(status)) return 'completed';
|
||||
return 'active';
|
||||
};
|
||||
|
||||
const localOcrTaskProgress = (job) => {
|
||||
const current = Number(job?.progressCurrent ?? job?.currentProgress);
|
||||
const total = Number(job?.progressTotal ?? job?.maxProgress);
|
||||
if (Number.isFinite(current) && Number.isFinite(total) && total > 0) {
|
||||
return Math.max(0, Math.min(100, Math.round((current / total) * 100)));
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const localOcrTaskFilterLabel = (filter) => {
|
||||
return filter === 'active' ? '进行中'
|
||||
: filter === 'completed' ? '已完成'
|
||||
: filter === 'attention' ? '需处理'
|
||||
: '全部';
|
||||
};
|
||||
|
||||
const ensureLocalOcrTaskDock = () => {
|
||||
let dock = document.querySelector('[data-testid="mnote-local-ocr-task-dock"]');
|
||||
if (!(dock instanceof HTMLElement)) {
|
||||
dock = document.createElement('section');
|
||||
dock.className = 'mnote-local-ocr-task-dock';
|
||||
dock.setAttribute('data-testid', 'mnote-local-ocr-task-dock');
|
||||
dock.innerHTML = '<div class="mnote-local-ocr-task-drawer" data-testid="mnote-local-ocr-task-drawer" hidden><div class="mnote-local-ocr-task-head"><strong>OCR 任务</strong><button type="button" class="mnote-local-ocr-task-close" data-mnote-local-ocr-task-close aria-label="收起 OCR 任务">×</button></div><div class="mnote-local-ocr-task-list" data-testid="mnote-local-ocr-task-list"></div></div>';
|
||||
dock.innerHTML = '<div class="mnote-local-ocr-task-drawer" data-testid="mnote-local-ocr-task-drawer" hidden><div class="mnote-local-ocr-task-panel"><div class="mnote-local-ocr-task-head"><div><strong>后台任务</strong><span data-mnote-local-ocr-task-summary>暂无任务</span></div><button type="button" class="mnote-local-ocr-task-close" data-mnote-local-ocr-task-close aria-label="收起后台任务">×</button></div><div class="mnote-local-ocr-task-tabs" data-mnote-local-ocr-task-tabs></div><div class="mnote-local-ocr-task-toolbar"><span data-mnote-local-ocr-task-filter-label>进行中</span><button type="button" data-mnote-local-ocr-task-clear-completed>清除已完成</button></div><div class="mnote-local-ocr-task-list" data-testid="mnote-local-ocr-task-list"></div></div></div>';
|
||||
document.body.appendChild(dock);
|
||||
}
|
||||
let toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
@@ -1509,6 +1578,20 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
}
|
||||
return;
|
||||
}
|
||||
const tabButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-tab]') : null;
|
||||
if (tabButton instanceof HTMLElement) {
|
||||
localOcrTaskState.taskFilter = tabButton.getAttribute('data-mnote-local-ocr-task-tab') || 'active';
|
||||
renderLocalOcrTaskDock();
|
||||
return;
|
||||
}
|
||||
const clearCompleted = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-clear-completed]') : null;
|
||||
if (clearCompleted instanceof HTMLElement) {
|
||||
Array.from(localOcrTaskState.jobsBySource.entries()).forEach(([key, job]) => {
|
||||
if (localOcrTaskCategory(job) === 'completed') localOcrTaskState.jobsBySource.delete(key);
|
||||
});
|
||||
renderLocalOcrTaskDock();
|
||||
return;
|
||||
}
|
||||
const deleteButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-delete]') : null;
|
||||
if (deleteButton instanceof HTMLElement) {
|
||||
const sourcePath = deleteButton.getAttribute('data-mnote-local-ocr-task-delete') || '';
|
||||
@@ -1568,12 +1651,18 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const dock = ensureLocalOcrTaskDock();
|
||||
const jobs = Array.from(localOcrTaskState.jobsBySource.values())
|
||||
.sort((left, right) => Number(right.updatedAtMs || 0) - Number(left.updatedAtMs || 0));
|
||||
const runningCount = jobs.filter((job) => !['done', 'failed', 'stale'].includes(String(job?.status || ''))).length;
|
||||
const toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
if (toggle instanceof HTMLButtonElement) {
|
||||
const activeJobs = jobs.filter((job) => localOcrTaskCategory(job) === 'active');
|
||||
const attentionJobs = jobs.filter((job) => localOcrTaskCategory(job) === 'attention');
|
||||
const completedJobs = jobs.filter((job) => localOcrTaskCategory(job) === 'completed');
|
||||
const runningCount = activeJobs.length;
|
||||
const taskToggles = Array.from(document.querySelectorAll('[data-testid="mnote-local-ocr-task-toggle"], [data-testid="mnote-floating-task-toggle"]'))
|
||||
.filter((node) => node instanceof HTMLButtonElement);
|
||||
taskToggles.forEach((toggle) => {
|
||||
const opensSettings = toggle.getAttribute('data-mnote-action') === 'open-ocr-settings';
|
||||
const label = runningCount > 0 ? `${runningCount} 个 OCR 处理中,打开 OCR 设置` : (jobs.length > 0 ? `${jobs.length} 个 OCR 任务,打开 OCR 设置` : 'OCR 设置');
|
||||
toggle.setAttribute('title', label);
|
||||
toggle.setAttribute('aria-label', label);
|
||||
const taskLabel = runningCount > 0 ? `${runningCount} 个后台任务正在运行` : (jobs.length > 0 ? `${jobs.length} 个后台任务` : '后台任务');
|
||||
toggle.setAttribute('title', opensSettings ? label : taskLabel);
|
||||
toggle.setAttribute('aria-label', opensSettings ? label : taskLabel);
|
||||
toggle.setAttribute('aria-expanded', localOcrTaskState.drawerOpen ? 'true' : 'false');
|
||||
toggle.classList.toggle('has-mnote-local-ocr-tasks', jobs.length > 0);
|
||||
const badge = toggle.querySelector('[data-mnote-local-ocr-task-count]');
|
||||
@@ -1583,28 +1672,85 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
} else {
|
||||
toggle.textContent = runningCount > 0 ? `OCR ${runningCount} 处理中` : `OCR ${jobs.length}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const drawer = dock.querySelector('[data-testid="mnote-local-ocr-task-drawer"]');
|
||||
if (drawer instanceof HTMLElement) drawer.hidden = !localOcrTaskState.drawerOpen;
|
||||
const summary = dock.querySelector('[data-mnote-local-ocr-task-summary]');
|
||||
if (summary instanceof HTMLElement) {
|
||||
summary.textContent = `${runningCount} 进行中 · ${attentionJobs.length} 需处理 · ${completedJobs.length} 已完成`;
|
||||
}
|
||||
const tabs = dock.querySelector('[data-mnote-local-ocr-task-tabs]');
|
||||
if (tabs instanceof HTMLElement) {
|
||||
const tabItems = [
|
||||
['active', '进行中', activeJobs.length],
|
||||
['attention', '需处理', attentionJobs.length],
|
||||
['completed', '已完成', completedJobs.length],
|
||||
['all', '全部', jobs.length],
|
||||
];
|
||||
tabs.replaceChildren();
|
||||
tabItems.forEach(([key, label, count]) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-mnote-local-ocr-task-tab', key);
|
||||
button.setAttribute('aria-selected', localOcrTaskState.taskFilter === key ? 'true' : 'false');
|
||||
button.textContent = `${label} ${count}`;
|
||||
tabs.appendChild(button);
|
||||
});
|
||||
}
|
||||
const filterLabel = dock.querySelector('[data-mnote-local-ocr-task-filter-label]');
|
||||
if (filterLabel instanceof HTMLElement) filterLabel.textContent = localOcrTaskFilterLabel(localOcrTaskState.taskFilter);
|
||||
const clearCompleted = dock.querySelector('[data-mnote-local-ocr-task-clear-completed]');
|
||||
if (clearCompleted instanceof HTMLButtonElement) clearCompleted.disabled = completedJobs.length === 0;
|
||||
const list = dock.querySelector('[data-testid="mnote-local-ocr-task-list"]');
|
||||
if (!(list instanceof HTMLElement)) return;
|
||||
list.replaceChildren();
|
||||
if (!jobs.length) {
|
||||
const visibleJobs = jobs.filter((job) => {
|
||||
return localOcrTaskState.taskFilter === 'all' || localOcrTaskCategory(job) === localOcrTaskState.taskFilter;
|
||||
});
|
||||
if (!visibleJobs.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'mnote-local-ocr-task-empty';
|
||||
empty.textContent = '暂无 OCR 任务';
|
||||
empty.textContent = jobs.length ? `暂无${localOcrTaskFilterLabel(localOcrTaskState.taskFilter)}任务` : '暂无后台任务';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
jobs.forEach((job) => {
|
||||
visibleJobs.forEach((job) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'mnote-local-ocr-task-row';
|
||||
row.setAttribute('data-mnote-local-ocr-task-row', String(job.sourceRootRelativePath || ''));
|
||||
row.setAttribute('data-mnote-local-ocr-task-status', String(job.status || ''));
|
||||
row.setAttribute('data-mnote-local-ocr-task-category', localOcrTaskCategory(job));
|
||||
const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || 'OCR';
|
||||
row.innerHTML = '<div class="mnote-local-ocr-task-main"><strong></strong><span></span></div><div class="mnote-local-ocr-task-actions"><button type="button" data-mnote-local-ocr-task-open>打开</button><button type="button" data-mnote-local-ocr-task-retry>重试</button><button type="button" data-mnote-local-ocr-task-clear>清除</button><button type="button" data-mnote-local-ocr-task-delete>删除</button></div>';
|
||||
row.querySelector('strong').textContent = title;
|
||||
row.querySelector('span').textContent = statusTextForLocalOcrJob(job);
|
||||
const category = localOcrTaskCategory(job);
|
||||
const progress = localOcrTaskProgress(job);
|
||||
row.innerHTML = '<div class="mnote-local-ocr-task-main"><div class="mnote-local-ocr-task-title-line"><strong></strong><em></em></div><span></span><div class="mnote-local-ocr-task-progress" role="progressbar"><i></i></div></div><div class="mnote-local-ocr-task-actions"><button type="button" data-mnote-local-ocr-task-open>打开</button><button type="button" data-mnote-local-ocr-task-retry>重试</button><button type="button" data-mnote-local-ocr-task-clear>清除</button><button type="button" data-mnote-local-ocr-task-delete>删除</button></div>';
|
||||
const titleNode = row.querySelector('strong');
|
||||
if (titleNode instanceof HTMLElement) {
|
||||
titleNode.textContent = title;
|
||||
titleNode.setAttribute('title', String(job.sourceRootRelativePath || title));
|
||||
}
|
||||
const categoryNode = row.querySelector('em');
|
||||
if (categoryNode instanceof HTMLElement) {
|
||||
categoryNode.textContent = category === 'active' ? '进行中' : category === 'attention' ? '需处理' : '已完成';
|
||||
}
|
||||
const statusNode = row.querySelector('span');
|
||||
if (statusNode instanceof HTMLElement) statusNode.textContent = statusTextForLocalOcrJob(job);
|
||||
const progressBar = row.querySelector('.mnote-local-ocr-task-progress');
|
||||
const progressValue = row.querySelector('.mnote-local-ocr-task-progress i');
|
||||
if (progressBar instanceof HTMLElement && progressValue instanceof HTMLElement) {
|
||||
progressBar.hidden = category !== 'active' && progress === null;
|
||||
progressBar.setAttribute('aria-valuemin', '0');
|
||||
progressBar.setAttribute('aria-valuemax', '100');
|
||||
if (progress === null) {
|
||||
progressBar.setAttribute('data-progress-mode', 'indeterminate');
|
||||
progressBar.removeAttribute('aria-valuenow');
|
||||
progressValue.style.width = '';
|
||||
} else {
|
||||
progressBar.setAttribute('data-progress-mode', 'determinate');
|
||||
progressBar.setAttribute('aria-valuenow', String(progress));
|
||||
progressValue.style.width = `${progress}%`;
|
||||
}
|
||||
}
|
||||
const open = row.querySelector('[data-mnote-local-ocr-task-open]');
|
||||
if (open instanceof HTMLButtonElement) {
|
||||
open.setAttribute('data-mnote-local-ocr-task-open', String(job.sourceRootRelativePath || ''));
|
||||
@@ -2178,6 +2324,20 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const officePreviewBaseHref = (href) => {
|
||||
const raw = String(href || '').trim();
|
||||
if (!raw) return '';
|
||||
try {
|
||||
const url = new URL(raw, window.location.origin);
|
||||
['page', 'bbox', 'sourceMapPath', 'blockId', 'lineRange', 'charRange', 'mnoteResourceReload'].forEach((key) => {
|
||||
url.searchParams.delete(key);
|
||||
});
|
||||
return url.pathname + '?' + url.searchParams.toString();
|
||||
} catch (_) {
|
||||
return raw;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshExistingOfficeResourceTab = (entry, input) => {
|
||||
if (!entry || entry.kind !== 'office') return false;
|
||||
const nextHref = String(input.officeUrl || input.href || '').trim();
|
||||
@@ -2186,7 +2346,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const currentHref = frame instanceof HTMLIFrameElement
|
||||
? String(frame.getAttribute('src') || frame.src || '').trim()
|
||||
: '';
|
||||
if (currentHref !== nextHref) void openPassiveResourceTab(entry, input);
|
||||
if (officePreviewBaseHref(currentHref) !== officePreviewBaseHref(nextHref)) void openPassiveResourceTab(entry, input);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user