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