返回 DeepSeek-Reasonix
site.js
根目录 / site / src / scripts / site.js
1 import { downloadPaneFromURL, downloadURLForPane } from "./download-link.js";
2 import { desktopDownloadVersion } from "../data/desktop-download.js";
3 import {
4 cliReleaseModel,
5 cliUpgradeCommand,
6 fetchDesktopDownloadModel,
7 fetchFirstJSON,
8 releaseVersionLabel,
9 } from "./release-channels.js";
10 import { initTheme } from "./theme.js";
11 import { initMobileNav } from "./mobile-nav.js";
12
13 // Reasonix site — vanilla interactions
14 (function () {
15 initTheme();
16 initMobileNav();
17 const motionOK = () =>
18 document.body.dataset.motion === "rich" &&
19 !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
20
21 const nav = document.querySelector(".nav");
22 if (nav) {
23 const onScroll = () => nav.classList.toggle("scrolled", window.scrollY > 12);
24 window.addEventListener("scroll", onScroll, { passive: true });
25 onScroll();
26 }
27
28 const revealEls = Array.from(document.querySelectorAll(".reveal"));
29 const inView = (el, factor) =>
30 el.getBoundingClientRect().top < window.innerHeight * (factor || 0.95);
31
32 const term = document.querySelector(".term");
33 const lines = Array.from(document.querySelectorAll(".term-body .tl"));
34 let played = false;
35 const playTerm = () => {
36 if (played) return;
37 played = true;
38 const fire = () => document.dispatchEvent(new CustomEvent("rx:term-played"));
39 if (!motionOK()) {
40 lines.forEach((l) => l.classList.add("on"));
41 fire();
42 return;
43 }
44 lines.forEach((l, i) => setTimeout(() => l.classList.add("on"), 350 + i * 520));
45 setTimeout(fire, 350 + Math.max(0, lines.length - 2) * 520);
46 };
47
48 let sweepQueued = false;
49 const sweep = () => {
50 sweepQueued = false;
51 revealEls.forEach((el) => {
52 if (!el.classList.contains("in") && inView(el, 0.95)) el.classList.add("in");
53 });
54 if (term && !played && inView(term, 0.85)) playTerm();
55 };
56 const queueSweep = () => {
57 if (sweepQueued) return;
58 sweepQueued = true;
59 requestAnimationFrame(sweep);
60 };
61 window.addEventListener("scroll", queueSweep, { passive: true });
62 window.addEventListener("resize", queueSweep, { passive: true });
63 window.addEventListener("load", queueSweep);
64 sweep();
65 setTimeout(sweep, 400);
66
67 /* contributors marquee — duplicate the server-rendered set for a seamless loop */
68 document.querySelectorAll(".crew-row").forEach((row) => {
69 const set = row.querySelector(".crew-set");
70 if (set) row.appendChild(set.cloneNode(true));
71 });
72
73 /* download / channel tabs */
74 const tabs = Array.from(document.querySelectorAll(".dl-tab"));
75 const panes = Array.from(document.querySelectorAll(".dl-pane"));
76 const activatePane = (name) => {
77 tabs.forEach((b) => {
78 const active = b.dataset.pane === name;
79 b.classList.toggle("active", active);
80 b.setAttribute("aria-selected", active ? "true" : "false");
81 b.tabIndex = active ? 0 : -1;
82 });
83 panes.forEach((p) => {
84 const active = p.dataset.pane === name;
85 p.classList.toggle("active", active);
86 p.hidden = !active;
87 });
88 };
89 tabs.forEach((tab) => {
90 tab.addEventListener("click", () => {
91 activatePane(tab.dataset.pane);
92 reflectPaneURL(tab.dataset.pane);
93 });
94 tab.addEventListener("keydown", (event) => {
95 const current = tabs.indexOf(tab);
96 let next = -1;
97 if (event.key === "ArrowRight" || event.key === "ArrowDown") next = (current + 1) % tabs.length;
98 else if (event.key === "ArrowLeft" || event.key === "ArrowUp") next = (current - 1 + tabs.length) % tabs.length;
99 else if (event.key === "Home") next = 0;
100 else if (event.key === "End") next = tabs.length - 1;
101 if (next < 0) return;
102 event.preventDefault();
103 const nextTab = tabs[next];
104 activatePane(nextTab.dataset.pane);
105 reflectPaneURL(nextTab.dataset.pane);
106 nextTab.focus();
107 });
108 });
109
110 /* OS detection — hero download button + card badge + highlight */
111 const ua = navigator.userAgent;
112 const os = /Windows/i.test(ua) ? "win" : /Mac|iPhone|iPad/i.test(ua) ? "mac" : /Linux|X11/i.test(ua) ? "linux" : "mac";
113 const osNames = { mac: "macOS", win: "Windows", linux: "Linux" };
114 document.querySelectorAll("[data-os-dl] .os-name").forEach((s) => (s.textContent = osNames[os]));
115 const osCard = document.querySelector('.os-card[data-os="' + os + '"]');
116 if (osCard) {
117 osCard.classList.add("detected");
118 const chip = document.createElement("span");
119 chip.className = "os-chip";
120 chip.innerHTML = '<span class="l-en">your OS</span><span class="l-zh">当前系统</span>';
121 osCard.appendChild(chip);
122 }
123
124 const flashOSCard = () => {
125 if (!osCard) return;
126 osCard.classList.remove("flash");
127 void osCard.offsetWidth;
128 setTimeout(() => osCard.classList.add("flash"), 450);
129 setTimeout(() => osCard.classList.remove("flash"), 2600);
130 };
131
132 const requestedPane = downloadPaneFromURL(window.location.href);
133 const legacyChannelURL = new URL(window.location.href);
134 if (legacyChannelURL.searchParams.has("channel")) {
135 legacyChannelURL.searchParams.delete("channel");
136 window.history.replaceState(null, "", legacyChannelURL.href);
137 }
138 if (requestedPane) {
139 activatePane(requestedPane);
140 if (requestedPane === "desktop") flashOSCard();
141 requestAnimationFrame(() => {
142 document.getElementById("start")?.scrollIntoView({ block: "start" });
143 queueSweep();
144 });
145 }
146
147 /* links that deep-link into a specific download tab */
148 document.querySelectorAll("[data-goto]").forEach((a) => {
149 a.addEventListener("click", (event) => {
150 event.preventDefault();
151 activatePane(a.dataset.goto);
152 reflectPaneURL(a.dataset.goto);
153 if (a.hasAttribute("data-os-dl")) flashOSCard();
154 document.getElementById("start")?.scrollIntoView({ block: "start" });
155 setTimeout(queueSweep, 500);
156 });
157 });
158
159 /* language switch */
160 const LANG_KEY = "reasonix-lang";
161 const langBtns = Array.from(document.querySelectorAll(".lang-switch button"));
162 const setLang = (l, alignHash) => {
163 document.body.dataset.lang = l;
164 document.documentElement.lang = l === "zh" ? "zh-CN" : "en";
165 const t = document.body.dataset[l === "zh" ? "titleZh" : "titleEn"];
166 if (t) document.title = t;
167 langBtns.forEach((b) => b.classList.toggle("active", b.dataset.lang === l));
168 try { localStorage.setItem(LANG_KEY, l); } catch (e) {}
169 if (alignHash && window.location.hash) {
170 const target = document.getElementById(window.location.hash.slice(1));
171 if (target) requestAnimationFrame(() => target.scrollIntoView({ block: "start" }));
172 }
173 };
174 langBtns.forEach((b) => b.addEventListener("click", () => setLang(b.dataset.lang)));
175 let savedLang = "";
176 try { savedLang = localStorage.getItem(LANG_KEY) || ""; } catch (e) {}
177 const requestedLang = new URLSearchParams(window.location.search).get("lang");
178 const initialLang = requestedLang === "zh" || requestedLang === "en"
179 ? requestedLang
180 : savedLang || ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");
181 setLang(initialLang, true);
182
183 /* docs scrollspy */
184 const sideLinks = Array.from(document.querySelectorAll(".docs-side a[href^='#']"));
185 if (sideLinks.length) {
186 const targets = sideLinks
187 .map((a) => document.getElementById(a.getAttribute("href").slice(1)))
188 .filter(Boolean)
189 // Sidebar links are grouped editorially, so their order differs from the
190 // page order. The spy below picks the last section past the 140px line,
191 // which is only correct when targets are sorted in document order.
192 .sort((a, b) =>
193 a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
194 const setActive = (id) =>
195 sideLinks.forEach((a) => {
196 const on = a.getAttribute("href") === "#" + id;
197 a.classList.toggle("active", on);
198 if (on) a.setAttribute("aria-current", "true");
199 else a.removeAttribute("aria-current");
200 });
201 // While a click smooth-scrolls to a section, pin the highlight to it so it
202 // doesn't sweep through every section scrolled past on the way. scrollend
203 // releases the pin when the scroll settles — on arrival or when the user
204 // takes over (wheel, touch, keyboard, scrollbar). Browsers without
205 // scrollend just skip pinning: correct destination, no sweep suppression.
206 let pinned = null;
207 const spy = () => {
208 if (pinned) return;
209 let current = targets[0];
210 for (const t of targets) if (t.getBoundingClientRect().top < 140) current = t;
211 if (current) setActive(current.id);
212 };
213 if ("onscrollend" in window) {
214 const ids = new Set(targets.map((t) => t.id));
215 document.querySelectorAll("a[href^='#']").forEach((a) => {
216 const id = a.getAttribute("href").slice(1);
217 if (ids.has(id)) a.addEventListener("click", () => { pinned = id; setActive(id); });
218 });
219 window.addEventListener("scrollend", () => { pinned = null; spy(); }, { passive: true });
220 }
221 window.addEventListener("scroll", spy, { passive: true });
222 spy();
223 }
224
225 /* copy-to-clipboard */
226 document.querySelectorAll("[data-copy]").forEach((btn) => {
227 btn.addEventListener("click", () => {
228 const text = btn.getAttribute("data-copy");
229 const done = () => {
230 btn.classList.add("copied");
231 const prev = btn.textContent;
232 btn.textContent = "Copied";
233 setTimeout(() => { btn.classList.remove("copied"); btn.textContent = prev; }, 1600);
234 };
235 if (navigator.clipboard && navigator.clipboard.writeText) {
236 navigator.clipboard.writeText(text).then(done).catch(done);
237 } else done();
238 });
239 });
240
241 /* public official releases */
242 const releaseModels = { desktop: null, cli: null };
243 const releasesPage = "https://github.com/esengine/DeepSeek-Reasonix/releases";
244 const reflectPaneURL = (surface) => {
245 const nextURL = downloadURLForPane(window.location.href, surface, "");
246 if (nextURL) window.history.replaceState(null, "", nextURL);
247 };
248
249 // npm / Homebrew / generic product chips track the CLI stable line only.
250 // Desktop and CLI download panes render their own versions via
251 // [data-release-version="<surface>"]; never let Desktop and CLI race on .rxv.
252 const updateCLIPackageVersion = (model) => {
253 if (!model) return;
254 document.querySelectorAll(".rxv").forEach((element) => { element.textContent = releaseVersionLabel(model); });
255 document.querySelectorAll("a.rxnotes").forEach((link) => {
256 link.href = new URL("changelog/v" + model.displayVersion + "/", window.location.origin + "/").href;
257 });
258 };
259
260 // Never synthesize public artifact URLs. If every required asset is not
261 // attested by live release data, fall back to the release list instead of a
262 // plausible-looking URL that may 404.
263 const fallbackReleaseURL = (surface) => surface === "desktop" && desktopDownloadVersion
264 ? releasesPage + "/tag/desktop-" + desktopDownloadVersion
265 : releasesPage;
266
267 const renderReleaseSurface = (surface) => {
268 const model = releaseModels[surface];
269 document.querySelectorAll('[data-release-version="' + surface + '"]').forEach((element) => {
270 element.textContent = releaseVersionLabel(model || (surface === "desktop" && desktopDownloadVersion
271 ? { version: desktopDownloadVersion } : null));
272 });
273 document.querySelectorAll('[data-release-notes="' + surface + '"]').forEach((link) => {
274 const path = model?.changelogURL ? new URL(model.changelogURL).pathname
275 : surface === "desktop" && desktopDownloadVersion ? "changelog/" + desktopDownloadVersion + "/" : "changelog/";
276 link.href = new URL(path, window.location.origin + "/").href;
277 });
278
279 const assetAttribute = "data-" + surface + "-asset";
280 document.querySelectorAll("[" + assetAttribute + "]").forEach((link) => {
281 const asset = link.getAttribute(assetAttribute);
282 const target = model?.assets?.[asset] || fallbackReleaseURL(surface);
283 link.href = target;
284 if (!model?.assets?.[asset]) link.removeAttribute("download");
285 else link.setAttribute("download", "");
286 });
287
288 if (surface === "cli") {
289 const command = cliUpgradeCommand();
290 document.querySelectorAll('[data-release-command="cli"]').forEach((element) => { element.textContent = command; });
291 document.querySelectorAll('.release-upgrade-command [data-copy]').forEach((button) => { button.dataset.copy = command; });
292 }
293 };
294
295 renderReleaseSurface("desktop");
296 renderReleaseSurface("cli");
297 if (requestedPane) reflectPaneURL(requestedPane);
298
299 fetchDesktopDownloadModel(fetch, desktopDownloadVersion)
300 .then((model) => {
301 if (!model) return;
302 releaseModels.desktop = model;
303 renderReleaseSurface("desktop");
304 })
305 .catch(() => {});
306
307 let githubCLIReleases;
308 const fallbackCLIReleases = () => {
309 githubCLIReleases ??= fetchFirstJSON([
310 "https://api.github.com/repos/esengine/DeepSeek-Reasonix/releases?per_page=100",
311 ]).catch(() => null);
312 return githubCLIReleases;
313 };
314 fetchFirstJSON(
315 ["https://crash.reasonix.io/v1/cli/releases/stable/latest.json"],
316 fetch,
317 (payload) => Boolean(cliReleaseModel(Array.isArray(payload) ? payload : [payload])),
318 )
319 .catch(() => fallbackCLIReleases())
320 .then((payload) => {
321 const releases = Array.isArray(payload) ? payload : payload ? [payload] : [];
322 const model = cliReleaseModel(releases);
323 if (!model) return;
324 releaseModels.cli = model;
325 updateCLIPackageVersion(model);
326 renderReleaseSurface("cli");
327 })
328 .catch(() => {});
329 })();
330
330 lines JAVASCRIPT