297 lines
8.9 KiB
JavaScript
297 lines
8.9 KiB
JavaScript
(() => {
|
|
"use strict";
|
|
|
|
const evidenceData = window.K1412_EVIDENCE;
|
|
const evidenceList = document.querySelector("#evidence-list");
|
|
const evidenceCount = document.querySelector("#evidence-count");
|
|
const emptyEvidence = document.querySelector("#empty-evidence");
|
|
const searchInput = document.querySelector("#evidence-search");
|
|
const expandButton = document.querySelector("#expand-evidence");
|
|
const depthButtons = [...document.querySelectorAll("[data-depth]")];
|
|
const themeButtons = [...document.querySelectorAll("[data-theme]")];
|
|
|
|
const depthLabels = {
|
|
core: "核心证据",
|
|
support: "旁证",
|
|
context: "历史背景",
|
|
};
|
|
|
|
const themeLabels = {
|
|
verification: "完成证明",
|
|
protocol: "协议审计",
|
|
recovery: "失败恢复",
|
|
reliability: "风险可靠性",
|
|
context: "历史背景",
|
|
};
|
|
|
|
const state = {
|
|
query: "",
|
|
depth: "all",
|
|
theme: "all",
|
|
expanded: false,
|
|
};
|
|
|
|
function normalize(value) {
|
|
return String(value || "")
|
|
.toLocaleLowerCase("zh-CN")
|
|
.replace(/\s+/g, " ")
|
|
.trim();
|
|
}
|
|
|
|
function searchableText(record) {
|
|
return normalize(
|
|
[
|
|
record.id,
|
|
record.title,
|
|
record.depth,
|
|
record.theme,
|
|
record.evidence,
|
|
record.supports,
|
|
record.boundary,
|
|
].join(" ")
|
|
);
|
|
}
|
|
|
|
function filteredRecords() {
|
|
if (!evidenceData || !Array.isArray(evidenceData.records)) {
|
|
return [];
|
|
}
|
|
|
|
const terms = normalize(state.query).split(" ").filter(Boolean);
|
|
return evidenceData.records.filter((record) => {
|
|
if (state.depth !== "all" && record.depth !== state.depth) {
|
|
return false;
|
|
}
|
|
if (state.theme !== "all" && record.theme !== state.theme) {
|
|
return false;
|
|
}
|
|
const haystack = searchableText(record);
|
|
return terms.every((term) => haystack.includes(term));
|
|
});
|
|
}
|
|
|
|
function tag(label, className) {
|
|
const element = document.createElement("span");
|
|
element.className = `evidence-tag ${className || ""}`.trim();
|
|
element.textContent = label;
|
|
return element;
|
|
}
|
|
|
|
function detailColumn(title, text, className = "") {
|
|
const wrapper = document.createElement("div");
|
|
wrapper.className = className;
|
|
|
|
const heading = document.createElement("h4");
|
|
heading.textContent = title;
|
|
const paragraph = document.createElement("p");
|
|
paragraph.textContent = text || "本轮未将该论文用于直接论证。";
|
|
|
|
wrapper.append(heading, paragraph);
|
|
return wrapper;
|
|
}
|
|
|
|
function evidenceItem(record) {
|
|
const details = document.createElement("details");
|
|
details.className = "evidence-item";
|
|
details.dataset.depth = record.depth;
|
|
details.open = state.expanded;
|
|
|
|
const summary = document.createElement("summary");
|
|
|
|
const id = document.createElement("span");
|
|
id.className = "evidence-id";
|
|
id.textContent = record.id;
|
|
|
|
const heading = document.createElement("div");
|
|
heading.className = "evidence-heading";
|
|
const title = document.createElement("h3");
|
|
title.textContent = record.title;
|
|
const tags = document.createElement("div");
|
|
tags.className = "evidence-tags";
|
|
tags.append(
|
|
tag(depthLabels[record.depth] || record.depth, record.depth),
|
|
tag(themeLabels[record.theme] || record.theme)
|
|
);
|
|
heading.append(title, tags);
|
|
|
|
const openLabel = document.createElement("span");
|
|
openLabel.className = "evidence-open";
|
|
openLabel.textContent = details.open ? "收起" : "展开";
|
|
|
|
summary.append(id, heading, openLabel);
|
|
details.append(summary);
|
|
|
|
if (record.depth === "context") {
|
|
const detail = document.createElement("div");
|
|
detail.className = "evidence-detail context-detail";
|
|
detail.append(
|
|
detailColumn("本轮位置", record.boundary, "evidence-boundary")
|
|
);
|
|
details.append(detail);
|
|
} else {
|
|
const detail = document.createElement("div");
|
|
detail.className = "evidence-detail";
|
|
detail.append(
|
|
detailColumn("核过的直接证据", record.evidence),
|
|
detailColumn("可支持的判断", record.supports),
|
|
detailColumn("不能外推", record.boundary, "evidence-boundary")
|
|
);
|
|
details.append(detail);
|
|
}
|
|
|
|
const source = document.createElement("div");
|
|
source.className = "evidence-source";
|
|
const sourceLink = document.createElement("a");
|
|
sourceLink.href = record.url;
|
|
sourceLink.target = "_blank";
|
|
sourceLink.rel = "noreferrer";
|
|
sourceLink.textContent = "打开 arXiv 原文";
|
|
source.append(sourceLink);
|
|
details.append(source);
|
|
|
|
details.addEventListener("toggle", () => {
|
|
openLabel.textContent = details.open ? "收起" : "展开";
|
|
});
|
|
|
|
return details;
|
|
}
|
|
|
|
function updateFilterButtons(buttons, value, attribute) {
|
|
buttons.forEach((button) => {
|
|
const active = button.dataset[attribute] === value;
|
|
button.classList.toggle("is-active", active);
|
|
button.setAttribute("aria-pressed", String(active));
|
|
});
|
|
}
|
|
|
|
function renderEvidence() {
|
|
if (!evidenceData) {
|
|
evidenceCount.textContent = "证据数据未载入。";
|
|
emptyEvidence.hidden = false;
|
|
return;
|
|
}
|
|
|
|
const records = filteredRecords();
|
|
const fragment = document.createDocumentFragment();
|
|
records.forEach((record) => fragment.append(evidenceItem(record)));
|
|
evidenceList.replaceChildren(fragment);
|
|
|
|
const counts = evidenceData.counts;
|
|
evidenceCount.textContent =
|
|
`显示 ${records.length} / ${counts.pool} 篇` +
|
|
` · 核心 ${counts.core} · 旁证 ${counts.support} · 背景 ${counts.context}`;
|
|
emptyEvidence.hidden = records.length !== 0;
|
|
expandButton.disabled = records.length === 0;
|
|
expandButton.textContent = state.expanded ? "收起当前结果" : "展开当前结果";
|
|
}
|
|
|
|
depthButtons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
state.depth = button.dataset.depth;
|
|
state.expanded = false;
|
|
updateFilterButtons(depthButtons, state.depth, "depth");
|
|
renderEvidence();
|
|
});
|
|
});
|
|
|
|
themeButtons.forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
state.theme = button.dataset.theme;
|
|
state.expanded = false;
|
|
updateFilterButtons(themeButtons, state.theme, "theme");
|
|
renderEvidence();
|
|
});
|
|
});
|
|
|
|
searchInput?.addEventListener("input", (event) => {
|
|
state.query = event.target.value;
|
|
state.expanded = false;
|
|
renderEvidence();
|
|
});
|
|
|
|
expandButton?.addEventListener("click", () => {
|
|
state.expanded = !state.expanded;
|
|
document.querySelectorAll(".evidence-item").forEach((item) => {
|
|
item.open = state.expanded;
|
|
const label = item.querySelector(".evidence-open");
|
|
if (label) {
|
|
label.textContent = state.expanded ? "收起" : "展开";
|
|
}
|
|
});
|
|
expandButton.textContent = state.expanded ? "收起当前结果" : "展开当前结果";
|
|
});
|
|
|
|
function updateReadingProgress() {
|
|
const documentHeight =
|
|
document.documentElement.scrollHeight - window.innerHeight;
|
|
const progress = documentHeight > 0 ? window.scrollY / documentHeight : 0;
|
|
const bar = document.querySelector("#reading-progress");
|
|
if (bar) {
|
|
bar.style.width = `${Math.min(1, Math.max(0, progress)) * 100}%`;
|
|
}
|
|
}
|
|
|
|
let progressFrame = 0;
|
|
window.addEventListener(
|
|
"scroll",
|
|
() => {
|
|
if (progressFrame) {
|
|
return;
|
|
}
|
|
progressFrame = window.requestAnimationFrame(() => {
|
|
updateReadingProgress();
|
|
progressFrame = 0;
|
|
});
|
|
},
|
|
{ passive: true }
|
|
);
|
|
updateReadingProgress();
|
|
|
|
const navAliases = {
|
|
conclusions: "overview",
|
|
synthesis: "model",
|
|
};
|
|
const navLinks = [...document.querySelectorAll(".primary-nav a")];
|
|
const sections = [...document.querySelectorAll("[data-section]")];
|
|
|
|
const sectionObserver = new IntersectionObserver(
|
|
(entries) => {
|
|
const visible = entries
|
|
.filter((entry) => entry.isIntersecting)
|
|
.sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0];
|
|
if (!visible) {
|
|
return;
|
|
}
|
|
const targetId = navAliases[visible.target.id] || visible.target.id;
|
|
navLinks.forEach((link) => {
|
|
const active = link.getAttribute("href") === `#${targetId}`;
|
|
link.classList.toggle("is-active", active);
|
|
if (active) {
|
|
const nav = link.closest(".primary-nav");
|
|
if (nav && nav.scrollWidth > nav.clientWidth) {
|
|
nav.scrollTo({
|
|
left: Math.max(0, link.offsetLeft - nav.clientWidth / 2),
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{
|
|
rootMargin: "-18% 0px -62% 0px",
|
|
threshold: [0, 0.08, 0.2],
|
|
}
|
|
);
|
|
sections.forEach((section) => sectionObserver.observe(section));
|
|
|
|
renderEvidence();
|
|
|
|
if (window.location.hash) {
|
|
window.requestAnimationFrame(() => {
|
|
window.requestAnimationFrame(() => {
|
|
document.querySelector(window.location.hash)?.scrollIntoView();
|
|
});
|
|
});
|
|
}
|
|
})();
|