209 lines
6.2 KiB
JavaScript
209 lines
6.2 KiB
JavaScript
const DEFAULT_POST_SELECTOR = ".post";
|
|
const DEFAULT_STYLE = "tldr";
|
|
|
|
function $(root, selector) {
|
|
const el = root.querySelector(selector);
|
|
if (!el) throw new Error(`Missing element: ${selector}`);
|
|
return el;
|
|
}
|
|
|
|
function compactText(text) {
|
|
return text.replace(/\s+/g, " ").trim();
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return s
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">");
|
|
}
|
|
|
|
async function fetchJson(url, init) {
|
|
const res = await fetch(url, init);
|
|
const data = await res.json().catch(() => ({}));
|
|
return { res, data };
|
|
}
|
|
|
|
function renderMarkdownish(container, style, summary) {
|
|
if (style === "bullets") {
|
|
const lines = summary
|
|
.split("\n")
|
|
.map((l) => l.trim())
|
|
.filter(Boolean);
|
|
const items = lines
|
|
.map((l) => (l.startsWith("-") ? l.replace(/^-+\s*/, "") : l))
|
|
.filter(Boolean);
|
|
container.innerHTML = `<ul>${items
|
|
.map((i) => `<li>${escapeHtml(i)}</li>`)
|
|
.join("")}</ul>`;
|
|
return;
|
|
}
|
|
|
|
if (style === "outline") {
|
|
const blocks = summary.split("\n");
|
|
const html = [];
|
|
for (const line of blocks) {
|
|
const l = line.trimEnd();
|
|
if (!l) continue;
|
|
if (l.startsWith("###")) {
|
|
html.push(`<h4>${escapeHtml(l.replace(/^###\s*/, ""))}</h4>`);
|
|
} else {
|
|
html.push(`<p>${escapeHtml(l)}</p>`);
|
|
}
|
|
}
|
|
container.innerHTML = html.join("");
|
|
return;
|
|
}
|
|
|
|
container.textContent = summary;
|
|
}
|
|
|
|
async function ensureSummary(root) {
|
|
const widget = root;
|
|
const title = widget.dataset.title || "摘要";
|
|
const autoload =
|
|
widget.dataset.autoload !== "false" && widget.dataset.autoload !== "0";
|
|
if (widget.dataset.expanded == null) widget.dataset.expanded = "false";
|
|
const needsScaffold =
|
|
!widget.querySelector(".post-summary__status") ||
|
|
!widget.querySelector(".post-summary__content") ||
|
|
!widget.querySelector(".post-summary__styles");
|
|
if (needsScaffold) {
|
|
widget.innerHTML = `
|
|
<div class="post-summary__header">
|
|
<div class="post-summary__title"></div>
|
|
<div class="post-summary__styles" aria-label="Summary styles"></div>
|
|
</div>
|
|
<div class="post-summary__body">
|
|
<div class="post-summary__status post-summary__muted">等待生成…</div>
|
|
<div class="post-summary__content"></div>
|
|
</div>
|
|
`.trim();
|
|
const titleEl = widget.querySelector(".post-summary__title");
|
|
if (titleEl) titleEl.textContent = title;
|
|
} else {
|
|
const titleEl = widget.querySelector(".post-summary__title");
|
|
if (titleEl && !titleEl.textContent) titleEl.textContent = title;
|
|
}
|
|
|
|
const postSelector = widget.dataset.postSelector || DEFAULT_POST_SELECTOR;
|
|
const inferredBase = (() => {
|
|
try {
|
|
return new URL(import.meta.url).origin;
|
|
} catch {
|
|
return "";
|
|
}
|
|
})();
|
|
const apiBase = (widget.dataset.apiBase || inferredBase).replace(/\/$/, "");
|
|
const urlKey = widget.dataset.url || location.pathname + location.search;
|
|
|
|
const statusEl = $(widget, ".post-summary__status");
|
|
const contentEl = $(widget, ".post-summary__content");
|
|
const stylesEl = $(widget, ".post-summary__styles");
|
|
|
|
const updatePressed = (style) => {
|
|
for (const btn of stylesEl.querySelectorAll("button[data-style]")) {
|
|
btn.setAttribute(
|
|
"aria-pressed",
|
|
btn.dataset.style === style ? "true" : "false"
|
|
);
|
|
}
|
|
};
|
|
|
|
const setStyle = async (style) => {
|
|
widget.dataset.expanded = "true";
|
|
widget.dataset.style = style;
|
|
updatePressed(style);
|
|
|
|
statusEl.textContent = "读取缓存…";
|
|
contentEl.textContent = "";
|
|
|
|
const { res, data } = await fetchJson(
|
|
`${apiBase}/api/summary?url=${encodeURIComponent(urlKey)}&style=${encodeURIComponent(
|
|
style
|
|
)}`
|
|
);
|
|
|
|
if (res.ok && typeof data?.summary === "string" && data.summary) {
|
|
statusEl.textContent = data.cached ? "来自 KV 缓存" : "已生成";
|
|
renderMarkdownish(contentEl, style, data.summary || "");
|
|
return;
|
|
}
|
|
|
|
const isCacheMiss = res.status === 404 || data?.error === "Not found";
|
|
if (!isCacheMiss) {
|
|
statusEl.textContent = `请求失败:${res.status}`;
|
|
contentEl.textContent = data?.error || "Unknown error";
|
|
return;
|
|
}
|
|
|
|
const postEl = document.querySelector(postSelector);
|
|
if (!postEl) {
|
|
statusEl.textContent = `未找到文章元素:${postSelector}`;
|
|
return;
|
|
}
|
|
|
|
const rawText = compactText(postEl.innerText || postEl.textContent || "");
|
|
if (!rawText) {
|
|
statusEl.textContent = "文章内容为空";
|
|
return;
|
|
}
|
|
|
|
const MAX_CHARS = 20000;
|
|
const text = rawText.slice(0, MAX_CHARS);
|
|
|
|
statusEl.textContent = "AI 生成中…";
|
|
const created = await fetchJson(`${apiBase}/api/summary`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ url: urlKey, style, text }),
|
|
});
|
|
|
|
if (!created.res.ok) {
|
|
statusEl.textContent = `生成失败:${created.res.status}`;
|
|
contentEl.textContent = created.data?.error || "Unknown error";
|
|
return;
|
|
}
|
|
|
|
statusEl.textContent = created.data.cached ? "来自 KV 缓存" : "已生成";
|
|
renderMarkdownish(contentEl, style, created.data.summary || "");
|
|
};
|
|
|
|
stylesEl.innerHTML = "";
|
|
const styleList = [
|
|
{ key: "tldr", label: "TL;DR" },
|
|
{ key: "bullets", label: "要点" },
|
|
{ key: "outline", label: "结构化" },
|
|
{ key: "keywords", label: "关键词" },
|
|
];
|
|
|
|
for (const { key, label } of styleList) {
|
|
const btn = document.createElement("button");
|
|
btn.type = "button";
|
|
btn.dataset.style = key;
|
|
btn.textContent = label;
|
|
btn.addEventListener("click", () => setStyle(key));
|
|
stylesEl.appendChild(btn);
|
|
}
|
|
|
|
const initialStyle = widget.dataset.style || DEFAULT_STYLE;
|
|
widget.dataset.style = initialStyle;
|
|
updatePressed(initialStyle);
|
|
|
|
if (autoload) {
|
|
widget.dataset.expanded = "true";
|
|
await setStyle(initialStyle);
|
|
} else {
|
|
widget.dataset.expanded = "false";
|
|
statusEl.textContent = "点击上方风格生成摘要…";
|
|
contentEl.textContent = "";
|
|
}
|
|
}
|
|
|
|
for (const widget of document.querySelectorAll(".post-summary")) {
|
|
ensureSummary(widget).catch((err) => {
|
|
const statusEl = widget.querySelector(".post-summary__status");
|
|
if (statusEl) statusEl.textContent = `初始化失败:${err.message || err}`;
|
|
});
|
|
}
|