// AI summaries per article section, running fully on-device via WebGPU. // Model runtime vendored from huggingface.co/spaces/webml-community/lfm2-webgpu-kernels // (Lfm2Mobile: GGUF loader + custom WGSL kernels + generation loop, all in lfm2_5.js). import { Lfm2Mobile, DEFAULT_MODEL_ID } from "./lfm2_5.js"; // A "section" is an H1 or H2 plus every direct-child element after it up to the next // H1/H2 (mirrors the boundaries the .article-sidebar TOC already uses — H3s are // sub-headings within their parent section, not their own summarizable unit). const HEADING_SELECTOR = ":scope > h1, :scope > h2"; const MIN_CHARS = 60; // skip empty/near-empty sections (e.g. a heading right before the next one) const MAX_NEW_TOKENS = 512; const CIRC = 2 * Math.PI * 13; // matches the r=13 in renderRing() // Model + its download/warmup are shared across every section on the page: only the // first click anywhere pays the load cost. `progressListeners` fans load progress out to // every ring currently waiting on it (a section clicked mid-download gets live updates, // not just the one that triggered the load). `genTail` serializes generate() calls, since // the model can only run one generation at a time. let model = null; let loadPromise = null; const progressListeners = new Set(); let genTail = Promise.resolve(); // Confirm WebGPU is actually usable BEFORE we ever touch the model. `navigator.gpu` // merely existing isn't enough — a browser can expose the API yet hand back no // adapter (blocklisted GPU, headless, disabled flag), so we request an adapter and // only resolve once one comes back. The result is cached so repeated summary clicks // don't re-probe; a failure clears the cache so a later attempt can retry. let webgpuReady = null; function ensureWebGPU() { if (!navigator.gpu) { return Promise.reject(new Error("Summaries need a WebGPU browser (recent Chrome or Edge).")); } if (!webgpuReady) { webgpuReady = (async () => { let adapter = null; try { adapter = await navigator.gpu.requestAdapter(); } catch { adapter = null; } if (!adapter) { throw new Error("WebGPU is present but no compatible GPU adapter is available on this device."); } return adapter; })().catch((err) => { webgpuReady = null; // let a future click re-probe throw err; }); } return webgpuReady; } document.addEventListener("DOMContentLoaded", init); function init() { const article = document.querySelector(".article"); if (!article) return; // Read the section boundaries from the ORIGINAL tree before any wrapping happens below // (wrapping a heading only reparents the heading itself, not its following siblings, but // computing every section's content list up front keeps this a clean read-then-write pass). // Also: must run before script.js wraps
 blocks into .code-collapsible — relies on
  // this file's