@xingjisoft/neutrino 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +312 -464
  2. package/assets/pro/backend_output_model.txt +5 -0
  3. package/assets/pro/claude-module-prompt.txt +65 -0
  4. package/assets/pro/common.txt +9 -0
  5. package/assets/pro/confirm_model.txt +5 -0
  6. package/assets/pro/front_output_model.txt +9 -0
  7. package/assets/pro/genimage_model.txt +1 -0
  8. package/assets/pro/main_model.txt +5 -0
  9. package/assets/pro/model-template.json +376 -0
  10. package/assets/pro/modeling_model.txt +5 -0
  11. package/assets/pro/plan_model.txt +8 -0
  12. package/assets/pro/thinking_model.txt +3 -0
  13. package/dist/app.js +43 -7
  14. package/dist/bin.js +5 -2
  15. package/dist/browser.js +57 -0
  16. package/dist/config.js +170 -26
  17. package/dist/index.js +1 -1
  18. package/dist/llm/approval.js +6 -2
  19. package/dist/llm/client.js +197 -28
  20. package/dist/llm/subagent.js +37 -1
  21. package/dist/pro/agents.js +104 -0
  22. package/dist/pro/config.js +88 -0
  23. package/dist/pro/preflight.js +172 -0
  24. package/dist/screens/chat.js +14 -8
  25. package/dist/screens/modelSelect.js +15 -4
  26. package/dist/screens/settings.js +9 -7
  27. package/dist/screens/start.js +48 -15
  28. package/dist/ui.js +58 -0
  29. package/dist/web/hub.js +22 -4
  30. package/dist/web/server.js +279 -66
  31. package/dist/web/session.js +258 -22
  32. package/dist/web/settings.js +91 -24
  33. package/package.json +37 -37
  34. package/web/README.md +87 -13
  35. package/web/app.js +304 -14
  36. package/web/i18n.js +382 -345
  37. package/web/index.html +90 -14
  38. package/web/neutrino-pro.gif +0 -0
  39. package/web/neutrino.png +0 -0
  40. package/web/pro.js +184 -0
  41. package/web/settings.js +322 -247
  42. package/web/style.css +297 -0
package/web/app.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createSettingsUI } from "./settings.js";
2
+ import { createProUI } from "./pro.js";
2
3
  import { renderMarkdown as renderMarkdownHtml } from "./markdown.js";
3
4
  import { turnAnswerIndex, turnFolds, turnPreview, turnRanges } from "./turns.js";
4
5
  import { applyTranslations, getLanguage as currentLanguage, locale as currentLocale, setLanguage as setI18nLanguage, t as translate } from "./i18n.js";
@@ -24,6 +25,37 @@ const REFRESH_MS = 4000;
24
25
  // Remove credentials left behind by older versions of the UI.
25
26
  try { localStorage.removeItem("neutrino.token"); } catch { /* Storage may be blocked. */ }
26
27
 
28
+ // ------------------------------------------------------------------ pro mode
29
+ //
30
+ // The "Pro" workspace is served at /pro and owns an isolated API under
31
+ // /pro/api, while this page and the login cookie are shared. Detect the mode
32
+ // before the request helpers below so no call can leak between the workspaces.
33
+ const PRO_MODE = (() => {
34
+ try {
35
+ const pathname = String(location?.pathname ?? "");
36
+ return pathname === "/pro" || pathname.startsWith("/pro/");
37
+ } catch {
38
+ return false; // Test sandboxes and unusual embedders have no location.
39
+ }
40
+ })();
41
+
42
+ const API_BASE = PRO_MODE ? "/pro/api" : "/api";
43
+ // Lets Pro-specific chrome (the wider sidebar needed by the longer brand name)
44
+ // apply before the first paint of this module.
45
+ if (PRO_MODE) document.body?.classList.add("pro-mode");
46
+
47
+ /** Point an /api request at the active workspace; other paths are unchanged. */
48
+ function apiPath(path) {
49
+ if (!PRO_MODE) return path;
50
+ const text = String(path);
51
+ return text.startsWith("/api/") ? `${API_BASE}${text.slice(4)}` : path;
52
+ }
53
+
54
+ /** Namespace a remembered preference so both workspaces keep their own copy. */
55
+ function modeStorageKey(key) {
56
+ return PRO_MODE ? `neutrino.pro.${key}` : key;
57
+ }
58
+
27
59
  // Translation lookup. `translate` is an ES-module import; the browser tests
28
60
  // that evaluate this file in a sandbox strip imports, so the helper degrades to
29
61
  // the English source text instead of throwing. Interpolation is kept here too.
@@ -92,7 +124,7 @@ const initialLogin = (() => {
92
124
 
93
125
  async function apiFetch(path, init = {}) {
94
126
  if (!await initialLogin) throw new Error("Password login required.");
95
- const response = await fetch(path, { ...init, credentials: "same-origin" });
127
+ const response = await fetch(apiPath(path), { ...init, credentials: "same-origin" });
96
128
  if (response.status === 401) {
97
129
  showGate();
98
130
  throw new Error("Password login required.");
@@ -158,6 +190,11 @@ const el = {
158
190
  sessions: document.getElementById("sessions"),
159
191
  status: document.getElementById("status"),
160
192
  refresh: document.getElementById("refresh"),
193
+ brandLogo: document.getElementById("brand-logo"),
194
+ brandName: document.getElementById("brand-name"),
195
+ proTip: document.getElementById("pro-tip"),
196
+ proTipClose: document.getElementById("pro-tip-close"),
197
+ proExit: document.getElementById("pro-exit"),
161
198
  title: document.getElementById("session-title"),
162
199
  sub: document.getElementById("session-sub"),
163
200
  controls: document.getElementById("controls"),
@@ -249,13 +286,16 @@ const state = {
249
286
  };
250
287
 
251
288
  function storedWorkspace() {
252
- const path = readStorage("neutrino.workspace");
289
+ const path = readStorage(modeStorageKey("neutrino.workspace"));
253
290
  if (!path) return null;
254
291
  return { key: pathKey(path), path };
255
292
  }
256
293
 
294
+ let proUI = null;
257
295
  const preferences = createSettingsUI({
258
296
  apiFetch,
297
+ proMode: PRO_MODE,
298
+ onCatalogChange: (catalog) => proUI?.load(catalog),
259
299
  onLanguageChange: (language) => changeInterfaceLanguage(language),
260
300
  async applyModel(selection) {
261
301
  const current = state.selection;
@@ -276,6 +316,37 @@ const preferences = createSettingsUI({
276
316
  },
277
317
  });
278
318
 
319
+ if (PRO_MODE && typeof createProUI === "function") {
320
+ proUI = createProUI({
321
+ async changePreset(preset) {
322
+ const current = state.selection;
323
+ if (current?.kind === "live") {
324
+ const response = await apiFetch(`/api/live/${encodeURIComponent(current.key)}/pro-preset`, {
325
+ method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ preset }),
326
+ });
327
+ const body = await response.json();
328
+ if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
329
+ if (state.selection?.kind === "live" && state.selection.key === current.key) {
330
+ state.snapshot = body.snapshot;
331
+ renderLive();
332
+ }
333
+ } else if (current?.kind !== "file") {
334
+ const response = await apiFetch("/api/settings", {
335
+ method: "POST", headers: { "Content-Type": "application/json" },
336
+ body: JSON.stringify({ settings: { pro_preset: preset } }),
337
+ });
338
+ const body = await response.json();
339
+ if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
340
+ await preferences.load();
341
+ }
342
+ },
343
+ });
344
+ }
345
+
346
+ function currentProPreset() {
347
+ return proUI?.preset() || preferences.catalog?.()?.pro?.preset;
348
+ }
349
+
279
350
  // ------------------------------------------------------------------- sounds
280
351
  //
281
352
  // A turn that ends while the tab is in the background is invisible, so finished
@@ -395,7 +466,7 @@ function workspaceName(value) {
395
466
 
396
467
  function setWorkspace(workspace, { persist = true } = {}) {
397
468
  state.workspace = workspace ? { key: pathKey(workspace.path), path: workspace.path } : null;
398
- if (persist) writeStorage("neutrino.workspace", state.workspace?.path ?? "");
469
+ if (persist) writeStorage(modeStorageKey("neutrino.workspace"), state.workspace?.path ?? "");
399
470
  renderWorkspaceChrome();
400
471
  renderSidebar();
401
472
  }
@@ -649,7 +720,7 @@ async function selectLive(key) {
649
720
 
650
721
  function openStream(key) {
651
722
  closeStream();
652
- const source = new EventSource(`/api/live/${encodeURIComponent(key)}/stream`);
723
+ const source = new EventSource(apiPath(`/api/live/${encodeURIComponent(key)}/stream`));
653
724
  state.eventSource = source;
654
725
 
655
726
  source.addEventListener("snapshot", (event) => {
@@ -728,6 +799,7 @@ function renderFile(data) {
728
799
  el.title.textContent = firstUserMessage(messages) || tr("Conversation");
729
800
  el.sub.textContent = [data.cwd, data.model, data.provider].filter(Boolean).join(" · ");
730
801
  preferences.showConversation(data, `file:${data.id}`);
802
+ proUI?.show(data, `file:${data.id}`);
731
803
  renderMessageList(messages, null, { readOnly: true, context: data.context, active: false });
732
804
  renderFileControls(data);
733
805
  maybeOpenPlan(data.plan);
@@ -779,10 +851,11 @@ function renderFileControls(data) {
779
851
 
780
852
  /** Attach to a resumed conversation, creating it server-side when needed. */
781
853
  async function continueSession(id, selection = preferences.selectionForFile(id)) {
854
+ const payload = PRO_MODE ? { pro_preset: currentProPreset() } : selection ? { selection } : undefined;
782
855
  const response = await apiFetch(`/api/session/${encodeURIComponent(id)}/continue`, {
783
856
  method: "POST",
784
- headers: selection ? { "Content-Type": "application/json" } : undefined,
785
- body: selection ? JSON.stringify({ selection }) : undefined,
857
+ headers: payload ? { "Content-Type": "application/json" } : undefined,
858
+ body: payload ? JSON.stringify(payload) : undefined,
786
859
  });
787
860
  const body = await response.json().catch(() => ({}));
788
861
  if (!response.ok) throw new Error(body.error || `HTTP ${response.status}`);
@@ -959,6 +1032,7 @@ function renderLive() {
959
1032
  el.title.textContent = snapshot.title || tr("New conversation");
960
1033
  el.sub.textContent = [snapshot.cwd, snapshot.model, snapshot.provider].filter(Boolean).join(" · ");
961
1034
  preferences.showConversation(snapshot, `live:${snapshot.key}`);
1035
+ proUI?.show(snapshot, `live:${snapshot.key}`);
962
1036
  renderControls(snapshot);
963
1037
  renderApproval(snapshot);
964
1038
  renderMessageList(snapshot.messages ?? [], snapshot.streamingContent, {
@@ -1324,6 +1398,10 @@ async function sendMessage() {
1324
1398
  if (!selection) return;
1325
1399
  const text = el.input.value;
1326
1400
  if (!text.trim()) return;
1401
+ if (PRO_MODE) {
1402
+ try { await proUI?.flush(); }
1403
+ catch (error) { el.sub.textContent = error.message; return; }
1404
+ }
1327
1405
 
1328
1406
  if (selection.kind === "live") {
1329
1407
  el.input.value = "";
@@ -1467,6 +1545,7 @@ function renderWorkspaceLanding(path) {
1467
1545
  el.title.textContent = workspaceName(path) || tr("Working directory");
1468
1546
  el.sub.textContent = path;
1469
1547
  setComposerMode("new");
1548
+ proUI?.show(null, `new:${path}`);
1470
1549
  el.messages.innerHTML = "";
1471
1550
 
1472
1551
  const empty = document.createElement("div");
@@ -1616,6 +1695,19 @@ function fillNewChatModels(catalog, selectedModel) {
1616
1695
  })), selectedModel);
1617
1696
  }
1618
1697
 
1698
+ /** Offer only the levels the chosen model declares, keeping the draft if valid. */
1699
+ function fillNewChatIntensities(catalog, selected) {
1700
+ const provider = catalog.providers.find((entry) => entry.name === el.newChatProvider.value) ?? catalog.providers[0];
1701
+ const model = provider?.models.find((entry) => entry.name === el.newChatModel.value);
1702
+ const levels = Array.isArray(model?.intensities) && model.intensities.length ? model.intensities : catalog.intensities;
1703
+ let wanted = selected;
1704
+ if (!levels.includes(wanted)) {
1705
+ const fallback = catalog.settings?.default_intensity;
1706
+ wanted = levels.includes(fallback) ? fallback : levels.includes("High") ? "High" : levels[0] ?? "";
1707
+ }
1708
+ fillSelect(el.newChatIntensity, levels.map((value) => ({ value, label: value })), wanted);
1709
+ }
1710
+
1619
1711
  function updateNewChatNote(catalog) {
1620
1712
  const provider = catalog?.providers.find((entry) => entry.name === el.newChatProvider.value);
1621
1713
  const model = provider?.models.find((entry) => entry.name === el.newChatModel.value);
@@ -1626,6 +1718,15 @@ function updateNewChatNote(catalog) {
1626
1718
  }
1627
1719
  }
1628
1720
 
1721
+ function renderProNewChatPresets(catalog, selected = catalog.pro?.preset) {
1722
+ if (!PRO_MODE) return;
1723
+ const select = document.getElementById("new-chat-pro-preset");
1724
+ if (!select) return;
1725
+ fillSelect(select, (catalog.pro?.presets || []).map(preset => ({
1726
+ value: preset.name, label: uiLanguage() === "zh" ? preset.chinese_name || preset.name : preset.name,
1727
+ })), selected);
1728
+ }
1729
+
1629
1730
  /** Relabel the open new-conversation sheet without losing its selections. */
1630
1731
  function refreshNewChatTexts() {
1631
1732
  if (!el.newChatDialog?.open) return;
@@ -1637,9 +1738,10 @@ function refreshNewChatTexts() {
1637
1738
  const approval = el.newChatApproval.value;
1638
1739
  fillSelect(el.newChatProvider, catalog.providers.map((entry) => ({ value: entry.name, label: entry.name })), provider);
1639
1740
  fillNewChatModels(catalog, model);
1640
- fillSelect(el.newChatIntensity, catalog.intensities.map((value) => ({ value, label: value })), intensity);
1741
+ fillNewChatIntensities(catalog, intensity);
1641
1742
  el.newChatApproval.value = approval;
1642
1743
  updateNewChatNote(catalog);
1744
+ renderProNewChatPresets(catalog, document.getElementById("new-chat-pro-preset")?.value);
1643
1745
  renderNewChatRecents();
1644
1746
  }
1645
1747
 
@@ -1659,13 +1761,14 @@ async function openNewChatDialog(options = {}) {
1659
1761
  const defaults = catalog.selection;
1660
1762
  fillSelect(el.newChatProvider, catalog.providers.map((provider) => ({ value: provider.name, label: provider.name })), defaults.provider);
1661
1763
  fillNewChatModels(catalog, defaults.model);
1662
- fillSelect(el.newChatIntensity, catalog.intensities.map((value) => ({ value, label: value })), defaults.intensity);
1764
+ fillNewChatIntensities(catalog, defaults.intensity);
1663
1765
  el.newChatApproval.value = catalog.settings.approval_mode === "auto" ? "auto" : "request";
1664
1766
 
1665
1767
  const start = String(options.cwd || selectionCwd() || state.workspace?.path || state.workspaces?.cwd || "").trim();
1666
1768
  el.newChatCwd.value = start;
1667
1769
  renderNewChatRecents();
1668
1770
  updateNewChatNote(catalog);
1771
+ renderProNewChatPresets(catalog);
1669
1772
  if (typeof el.newChatCwd.focus === "function") el.newChatCwd.focus();
1670
1773
  }
1671
1774
 
@@ -1686,7 +1789,8 @@ async function submitNewChat() {
1686
1789
  el.newChatStart.textContent = tr("Creating…");
1687
1790
  el.newChatFeedback.textContent = "";
1688
1791
  try {
1689
- const created = await createConversation(cwd, selection, el.newChatApproval.value);
1792
+ const created = await createConversation(cwd, selection, el.newChatApproval.value,
1793
+ PRO_MODE ? document.getElementById("new-chat-pro-preset")?.value : undefined);
1690
1794
  el.newChatDialog.close();
1691
1795
  await selectLive(created.key);
1692
1796
  setConversationView(true);
@@ -1700,9 +1804,10 @@ async function submitNewChat() {
1700
1804
  }
1701
1805
 
1702
1806
  /** Create a browser-owned conversation in `cwd` and reveal its folder. */
1703
- async function createConversation(cwd, selection, approvalMode) {
1807
+ async function createConversation(cwd, selection, approvalMode, proPreset = currentProPreset()) {
1704
1808
  const payload = { cwd };
1705
- if (selection && selection.provider && selection.model) payload.selection = selection;
1809
+ if (PRO_MODE) payload.pro_preset = proPreset;
1810
+ else if (selection && selection.provider && selection.model) payload.selection = selection;
1706
1811
  if (approvalMode === "request" || approvalMode === "auto") payload.approval_mode = approvalMode;
1707
1812
  const response = await apiFetch("/api/conversations", {
1708
1813
  method: "POST",
@@ -2177,11 +2282,12 @@ function syncLanguageSelects(language) {
2177
2282
  async function changeInterfaceLanguage(language, { persist = true } = {}) {
2178
2283
  const normalized = String(language ?? "").toLowerCase().startsWith("zh") ? "zh" : "en";
2179
2284
  setUiLanguage(normalized);
2180
- writeStorage("neutrino.language", normalized);
2285
+ writeStorage(modeStorageKey("neutrino.language"), normalized);
2181
2286
  applyStaticTranslations();
2182
2287
  syncLanguageSelects(normalized);
2183
2288
  if (state.status) setStatus(state.status.key, state.status.kind);
2184
2289
  preferences.refreshTexts?.();
2290
+ proUI?.refresh();
2185
2291
  refreshNewChatTexts();
2186
2292
  renderWorkspaceChrome();
2187
2293
  renderSidebar();
@@ -2209,13 +2315,192 @@ async function changeInterfaceLanguage(language, { persist = true } = {}) {
2209
2315
  }
2210
2316
 
2211
2317
  function initializeLanguage() {
2212
- const stored = readStorage("neutrino.language");
2318
+ const stored = readStorage(modeStorageKey("neutrino.language"));
2213
2319
  if (stored) setUiLanguage(stored);
2214
2320
  applyStaticTranslations();
2215
2321
  syncLanguageSelects(uiLanguage());
2216
2322
  if (state.status) setStatus(state.status.key, state.status.kind);
2217
2323
  }
2218
2324
 
2325
+ // ------------------------------------------------------------------ pro mode
2326
+ //
2327
+ // Both workspaces reuse this console. Clicking the orbit badge on the normal
2328
+ // console starts a short wordmark curtain; the recorded start time lets /pro
2329
+ // finish the same animation instead of flashing the freshly loaded page.
2330
+
2331
+ const PRO_TRANSITION_KEY = "neutrino.pro.transition";
2332
+ const PRO_TIP_KEY = "neutrino.pro.tip.dismissed";
2333
+ const PRO_TRANSITION_FADE_MS = 350; // curtain fade in / fade out
2334
+ const PRO_TRANSITION_COVER_MS = 450; // navigate once the curtain is fully opaque
2335
+ const PRO_TRANSITION_HOLD_MS = 3000; // wordmark stays up until 3s after the switch began
2336
+ const PRO_TRANSITION_RESUME_MS = 8000; // older records are stale and never replayed
2337
+ const PRO_LOGO_MARK = '<img class="pro-logo-mark" src="/neutrino.png" alt="Neutrino Pro">';
2338
+
2339
+ let proTransitionStarted = false;
2340
+
2341
+ function proCurtain() {
2342
+ const curtain = document.createElement("div");
2343
+ curtain.className = "pro-curtain";
2344
+ curtain.setAttribute("aria-hidden", "true");
2345
+ const image = document.createElement("img");
2346
+ image.src = "/neutrino-pro.gif";
2347
+ image.alt = "";
2348
+ curtain.appendChild(image);
2349
+ return curtain;
2350
+ }
2351
+
2352
+ /** Timers exist in the browser; the test sandboxes load this file without them. */
2353
+ function proDelay(callback, ms) {
2354
+ if (typeof setTimeout === "function") setTimeout(callback, ms);
2355
+ }
2356
+
2357
+ function startProTransition() {
2358
+ if (PRO_MODE || proTransitionStarted) return;
2359
+ proTransitionStarted = true;
2360
+ const started = Date.now();
2361
+ try {
2362
+ sessionStorage.setItem(PRO_TRANSITION_KEY, String(started));
2363
+ } catch { /* Storage may be blocked; navigation still works. */ }
2364
+
2365
+ const curtain = proCurtain();
2366
+ curtain.style.transition = `opacity ${PRO_TRANSITION_FADE_MS}ms ease`;
2367
+ document.body.appendChild(curtain);
2368
+ // Commit the transparent first frame, then let the style transition fade in.
2369
+ void curtain.offsetWidth;
2370
+ curtain.classList.add("pro-curtain-visible");
2371
+
2372
+ proDelay(() => {
2373
+ try { location.assign("/pro"); } catch { location.href = "/pro"; }
2374
+ }, PRO_TRANSITION_COVER_MS);
2375
+ }
2376
+
2377
+ /** Remove every switch curtain left in this document. */
2378
+ function removeProCurtains() {
2379
+ if (typeof document.querySelectorAll !== "function") return;
2380
+ for (const curtain of document.querySelectorAll(".pro-curtain")) curtain.remove?.();
2381
+ }
2382
+
2383
+ /**
2384
+ * Drop the one-shot switch chrome.
2385
+ *
2386
+ * The curtain lives in the document that navigated away, so the browser's
2387
+ * back/forward cache can restore it fully opaque — a Back or Forward would
2388
+ * otherwise land on that frozen frame. `clearRecord` stays false while this
2389
+ * document is being hidden for /pro: the destination still needs the recorded
2390
+ * start time to finish the same animation.
2391
+ */
2392
+ function resetProTransition({ clearRecord = true } = {}) {
2393
+ removeProCurtains();
2394
+ document.documentElement?.classList?.remove?.("pro-transitioning");
2395
+ proTransitionStarted = false;
2396
+ if (!clearRecord) return;
2397
+ try { sessionStorage.removeItem(PRO_TRANSITION_KEY); } catch { /* Storage may be blocked. */ }
2398
+ }
2399
+
2400
+ /** Undo the switch chrome when the browser restores either page from bfcache. */
2401
+ function watchProNavigation() {
2402
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
2403
+ window.addEventListener("pagehide", (event) => {
2404
+ if (!event?.persisted) return;
2405
+ // The page is entering the back/forward cache: clean the snapshot, but
2406
+ // keep the record for the /pro page that is currently loading.
2407
+ resetProTransition({ clearRecord: false });
2408
+ });
2409
+ window.addEventListener("pageshow", (event) => {
2410
+ if (!event?.persisted) return;
2411
+ // Restored by Back/Forward: any curtain captured in the snapshot is stale.
2412
+ resetProTransition();
2413
+ });
2414
+ }
2415
+
2416
+ /** Consume a just-recorded switch and finish the animation on /pro. */
2417
+ function resumeProTransition() {
2418
+ if (!PRO_MODE) return;
2419
+ let started = 0;
2420
+ try { started = Number(sessionStorage.getItem(PRO_TRANSITION_KEY)) || 0; } catch { /* Storage may be blocked. */ }
2421
+ // Consume the record even when stale: direct visits never show a curtain.
2422
+ if (started) {
2423
+ try { sessionStorage.removeItem(PRO_TRANSITION_KEY); } catch { /* Ignore. */ }
2424
+ }
2425
+ const now = Date.now();
2426
+ const age = now - started;
2427
+ if (!started || age < 0 || age > PRO_TRANSITION_RESUME_MS) {
2428
+ document.documentElement.classList.remove("pro-transitioning");
2429
+ return;
2430
+ }
2431
+
2432
+ const curtain = proCurtain();
2433
+ // Start fully opaque on the first frame so the new page never flashes through.
2434
+ curtain.style.transition = "none";
2435
+ curtain.style.opacity = "1";
2436
+ document.body.appendChild(curtain);
2437
+ // The real curtain is up: retire the pre-paint cover from index.html so it
2438
+ // cannot remain over the page after the fade completes.
2439
+ document.documentElement.classList.remove("pro-transitioning");
2440
+
2441
+ proDelay(() => {
2442
+ curtain.style.transition = `opacity ${PRO_TRANSITION_FADE_MS}ms ease`;
2443
+ curtain.style.opacity = "0";
2444
+ proDelay(() => curtain.remove(), PRO_TRANSITION_FADE_MS);
2445
+ }, Math.max(0, started + PRO_TRANSITION_HOLD_MS - now));
2446
+ }
2447
+
2448
+ /** In Pro mode the badge is a static "you are here" mark, not a control. */
2449
+ function applyProBrand() {
2450
+ document.title = "Neutrino Pro";
2451
+ if (el.brandName) el.brandName.textContent = "Neutrino Pro";
2452
+ const logo = el.brandLogo;
2453
+ if (!logo) return;
2454
+ logo.innerHTML = PRO_LOGO_MARK;
2455
+ logo.classList.add("pro-logo");
2456
+ logo.tabIndex = -1;
2457
+ // Drop the i18n annotations too, or a language switch restores the tooltip.
2458
+ const annotations = ["title", "aria-label", "data-i18n-title", "data-i18n-aria-label"];
2459
+ for (const attribute of annotations) logo.removeAttribute?.(attribute);
2460
+ }
2461
+
2462
+ /** The blue tip explains the badge on the normal console; Pro never shows it. */
2463
+ function initializeProTip() {
2464
+ if (PRO_MODE || !el.proTip) return;
2465
+ if (readStorage(PRO_TIP_KEY) === "1") return;
2466
+ el.proTip.classList.remove("hidden");
2467
+ el.proTipClose?.addEventListener("click", () => {
2468
+ writeStorage(PRO_TIP_KEY, "1");
2469
+ el.proTip.classList.add("hidden");
2470
+ });
2471
+ }
2472
+
2473
+ /** The Pro workspace's top-right escape hatch back to the regular console. */
2474
+ function initializeProExit() {
2475
+ if (!el.proExit) return;
2476
+ el.proExit.classList.remove("hidden");
2477
+ el.proExit.addEventListener("click", () => {
2478
+ el.proExit.disabled = true;
2479
+ try {
2480
+ location.assign("/");
2481
+ } catch {
2482
+ location.href = "/";
2483
+ }
2484
+ });
2485
+ }
2486
+
2487
+ /** Wire the shared brand and either start the switch or resume its animation. */
2488
+ function initializeProWorkspace() {
2489
+ watchProNavigation();
2490
+ if (PRO_MODE) {
2491
+ applyProBrand();
2492
+ initializeProExit();
2493
+ resumeProTransition();
2494
+ return;
2495
+ }
2496
+ // A switch that was interrupted (Stop, Back, a failed navigation) can leave
2497
+ // the record and the pre-paint cover behind. Neither may ever affect the
2498
+ // regular console, so clear them before wiring the badge.
2499
+ resetProTransition();
2500
+ el.brandLogo?.addEventListener("click", startProTransition);
2501
+ initializeProTip();
2502
+ }
2503
+
2219
2504
  // --------------------------------------------------------------------- init
2220
2505
 
2221
2506
  el.refresh.addEventListener("click", () => loadOverview());
@@ -2274,12 +2559,16 @@ el.newChatProvider.addEventListener("change", () => {
2274
2559
  const catalog = preferences.catalog?.();
2275
2560
  if (catalog) {
2276
2561
  fillNewChatModels(catalog, "");
2562
+ fillNewChatIntensities(catalog, el.newChatIntensity.value);
2277
2563
  updateNewChatNote(catalog);
2278
2564
  }
2279
2565
  });
2280
2566
  el.newChatModel.addEventListener("change", () => {
2281
2567
  const catalog = preferences.catalog?.();
2282
- if (catalog) updateNewChatNote(catalog);
2568
+ if (catalog) {
2569
+ fillNewChatIntensities(catalog, el.newChatIntensity.value);
2570
+ updateNewChatNote(catalog);
2571
+ }
2283
2572
  });
2284
2573
 
2285
2574
  el.folderClose.addEventListener("click", () => el.folderDialog.close());
@@ -2318,6 +2607,7 @@ el.folderDialog.addEventListener("cancel", (event) => {
2318
2607
  window.addEventListener("beforeunload", closeStream);
2319
2608
 
2320
2609
  initializeLanguage();
2610
+ initializeProWorkspace();
2321
2611
  preferences.load()
2322
2612
  .then((config) => {
2323
2613
  const language = config?.settings?.language;