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
@@ -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");