import { queueSteps, architectures, benchmarks } from "./model.js"; const chunkSlider = document.getElementById("chunk-slider"); const computeSlider = document.getElementById("compute-slider"); const queueChart = document.getElementById("queue-chart"); function renderQueue() { const chunkMs = Number(chunkSlider.value); const computeMs = Number(computeSlider.value); const steps = queueSteps(chunkMs, computeMs); const totalMs = Math.max(steps.at(-1).end, chunkMs * 9); const percentage = (value) => `${(value / totalMs) * 100}%`; document.getElementById("chunk-value").textContent = `${chunkMs} ms`; document.getElementById("compute-value").textContent = `${computeMs} ms`; const ratio = document.getElementById("rtf-value"); ratio.textContent = `${(computeMs / chunkMs).toFixed(2)}×`; ratio.classList.toggle("overloaded", computeMs >= chunkMs); document.getElementById("backlog-value").textContent = `${steps.at(-1).wait} ms`; document.getElementById("growth-value").textContent = `${Math.max(0, computeMs - chunkMs)} ms`; queueChart.replaceChildren(); const ticks = document.createElement("div"); ticks.className = "queue-ticks"; for (const fraction of [0, 0.25, 0.5, 0.75, 1]) { const tick = document.createElement("span"); tick.textContent = `${Math.round(totalMs * fraction)}${fraction === 1 ? " ms" : ""}`; ticks.append(tick); } queueChart.append(ticks); for (const step of steps) { const row = document.createElement("div"); row.className = "queue-row"; row.setAttribute( "aria-label", `第${step.index}块在${step.arrival}毫秒就绪,等待${step.wait}毫秒,在${step.end}毫秒计算完成`, ); const label = document.createElement("span"); label.className = "queue-label"; label.textContent = `块 ${step.index}`; const arrival = document.createElement("span"); arrival.className = "queue-arrival"; arrival.style.left = percentage(step.arrival); const wait = document.createElement("span"); wait.className = "queue-wait"; wait.style.left = percentage(step.arrival); wait.style.width = percentage(step.wait); const compute = document.createElement("span"); compute.className = "queue-compute"; compute.style.left = percentage(step.start); compute.style.width = percentage(computeMs); row.append(label, wait, compute, arrival); queueChart.append(row); } const explanation = document.getElementById("queue-explanation"); if (computeMs < chunkMs) { explanation.textContent = `每块留有 ${chunkMs - computeMs} ms 计算余量。此理想条件下等待为零,单块 ${computeMs} ms 的计算延迟仍然存在。`; } else if (computeMs === chunkMs) { explanation.textContent = "理想条件下刚好赶上输入节奏。出现额外计算或调度抖动时,队列就会开始积压。"; } else { explanation.textContent = `每块增加 ${computeMs - chunkMs} ms 等待,第 8 块已等待 ${steps.at(-1).wait} ms。需要减少计算、增加处理能力或调整处理策略。`; } } function selectArchitecture(key) { const selected = architectures[key]; const flow = document.getElementById("architecture-flow"); flow.replaceChildren(); selected.nodes.forEach(([title, subtitle, type], index) => { if (index) { const arrow = document.createElement("span"); arrow.className = "flow-arrow"; arrow.textContent = "→"; arrow.setAttribute("aria-hidden", "true"); flow.append(arrow); } const node = document.createElement("div"); node.className = `flow-node ${type || ""}`; const strong = document.createElement("strong"); strong.textContent = title; const small = document.createElement("small"); small.textContent = subtitle; node.append(strong, small); flow.append(node); }); document.getElementById("architecture-detail").innerHTML = selected.detail; document.querySelectorAll("[data-architecture]").forEach((button) => { button.setAttribute( "aria-pressed", String(button.dataset.architecture === key), ); }); } function renderBenchmarks() { const chart = document.getElementById("benchmark-chart"); for (const model of benchmarks) { const row = document.createElement("div"); row.className = "benchmark-row"; const label = document.createElement("span"); label.className = "benchmark-label"; label.textContent = model.name; const tracks = document.createElement("div"); tracks.className = "benchmark-tracks"; for (const [key, className] of [ ["first", "first"], ["answer", "answer"], ]) { const bar = document.createElement("div"); bar.className = `benchmark-bar ${className}`; bar.style.width = `${(model[key] / 12) * 100}%`; const value = document.createElement("span"); value.className = "benchmark-value"; value.textContent = `${model[key].toFixed(2)} s`; bar.append(value); tracks.append(bar); } row.append(label, tracks); chart.append(row); } const axis = document.createElement("div"); axis.className = "benchmark-axis"; for (const value of ["0", "3", "6", "9", "12 s"]) { const tick = document.createElement("span"); tick.textContent = value; axis.append(tick); } chart.append(axis); } chunkSlider.addEventListener("input", renderQueue); computeSlider.addEventListener("input", renderQueue); document.querySelectorAll("[data-preset]").forEach((button) => { button.addEventListener("click", () => { const [chunk, compute] = button.dataset.preset.split(","); chunkSlider.value = chunk; computeSlider.value = compute; renderQueue(); }); }); document.querySelectorAll("[data-architecture]").forEach((button) => { button.addEventListener("click", () => selectArchitecture(button.dataset.architecture), ); }); const themeToggle = document.getElementById("theme-toggle"); try { const preference = localStorage.getItem("voice-duplex-theme"); if (preference === "dark" || preference === "light") document.documentElement.dataset.theme = preference; } catch { /* The page remains usable when storage is unavailable. */ } themeToggle.addEventListener("click", () => { const theme = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = theme; themeToggle.setAttribute( "aria-label", theme === "dark" ? "切换浅色主题" : "切换深色主题", ); try { localStorage.setItem("voice-duplex-theme", theme); } catch { /* Theme changes still apply for this visit. */ } }); const navigationLinks = [...document.querySelectorAll(".contents nav a")]; const observer = new IntersectionObserver( (entries) => { const visible = entries .filter((entry) => entry.isIntersecting) .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top); if (!visible.length) return; const currentId = visible[0].target.id; navigationLinks.forEach((link) => { const active = link.hash === `#${currentId}`; link.classList.toggle("active", active); if (active) link.setAttribute("aria-current", "location"); else link.removeAttribute("aria-current"); }); }, { rootMargin: "-110px 0px -55% 0px", threshold: 0 }, ); document .querySelectorAll(".article-section") .forEach((section) => observer.observe(section)); selectArchitecture("cascade"); renderQueue(); renderBenchmarks();