@xingjisoft/neutrino 0.2.0 → 0.3.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.
package/web/settings.js CHANGED
@@ -1,9 +1,13 @@
1
1
  // Settings and model controls share the same server-side catalog. No API keys
2
2
  // are returned to this controller or kept in browser storage.
3
- export function createSettingsUI({ apiFetch, applyModel }) {
3
+ import { t } from "./i18n.js";
4
+
5
+ export function createSettingsUI({ apiFetch, applyModel, onLanguageChange }) {
4
6
  const ids = ["model-picker", "model-provider", "model-name", "model-intensity", "model-apply", "model-feedback",
5
7
  "settings-open", "settings-dialog", "settings-form", "settings-close", "settings-cancel", "settings-save", "settings-feedback",
6
- "default-provider", "default-model", "default-intensity", "settings-context", "settings-compress-note", "settings-reviewer", "settings-extreme", "settings-sound", "settings-password", "settings-password-confirm"];
8
+ "default-provider", "default-model", "default-intensity", "settings-context", "settings-compress-note", "settings-reviewer",
9
+ "settings-extreme", "settings-sound", "settings-password", "settings-password-confirm",
10
+ "settings-language", "settings-plan", "settings-approval", "settings-web-console"];
7
11
  const el = Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
8
12
  let config = null;
9
13
  let target = null;
@@ -41,12 +45,16 @@ export function createSettingsUI({ apiFetch, applyModel }) {
41
45
  function models(providerSelect, modelSelect, selected) {
42
46
  const provider = config.providers.find(entry => entry.name === providerSelect.value);
43
47
  options(modelSelect, (provider?.models ?? []).map(model => ({
44
- value: model.name, label: model.name + (model.configured ? "" : " (API key missing)"),
48
+ value: model.name, label: model.name + (model.configured ? "" : ` ${t("(API key missing)")}`),
45
49
  })), selected);
46
50
  }
47
51
  function intensities(select, selected) {
48
52
  options(select, config.intensities.map(value => ({ value, label: value })), selected);
49
53
  }
54
+ function reviewerOptions(selected) {
55
+ const reviewers = [...new Set(config.providers.flatMap(provider => provider.models.map(model => model.name)))];
56
+ options(el["settings-reviewer"], [{ value: "", label: t("Same as conversation") }, ...reviewers.map(value => ({ value, label: value }))], selected);
57
+ }
50
58
  function choice() {
51
59
  return { provider: el["model-provider"].value, model: el["model-name"].value, intensity: el["model-intensity"].value };
52
60
  }
@@ -60,8 +68,14 @@ export function createSettingsUI({ apiFetch, applyModel }) {
60
68
  }
61
69
  const busy = Boolean(conversation.busy || conversation.approval);
62
70
  el["model-apply"].disabled = applying || busy || !choice().provider || !choice().model;
63
- el["model-apply"].textContent = applying ? "Applying…" : target?.startsWith("file:") ? "Continue with model" : "Apply to conversation";
64
- message(el["model-feedback"], feedback || (busy ? "Pause the current turn to change models." : "Applies to the next turn."), feedbackError);
71
+ el["model-apply"].textContent = applying
72
+ ? t("Applying…")
73
+ : target?.startsWith("file:") ? t("Continue with model") : t("Apply to conversation");
74
+ message(
75
+ el["model-feedback"],
76
+ feedback || (busy ? t("Pause the current turn to change models.") : t("Applies to the next turn.")),
77
+ feedbackError,
78
+ );
65
79
  }
66
80
  async function load() {
67
81
  config = await json("/api/settings");
@@ -99,9 +113,9 @@ export function createSettingsUI({ apiFetch, applyModel }) {
99
113
  renderPicker();
100
114
  try {
101
115
  await applyModel(choice());
102
- if (target === originalTarget) { dirty = false; feedback = "Model and intensity updated."; feedbackError = false; }
116
+ if (target === originalTarget) { dirty = false; feedback = t("Model and intensity updated."); feedbackError = false; }
103
117
  } catch (error) {
104
- if (target === originalTarget) { feedback = error.message; feedbackError = true; }
118
+ if (target === originalTarget) { feedback = t(error.message); feedbackError = true; }
105
119
  } finally {
106
120
  applying = false;
107
121
  renderPicker();
@@ -111,14 +125,17 @@ export function createSettingsUI({ apiFetch, applyModel }) {
111
125
  function renderSettings() {
112
126
  el["settings-password"].value = "";
113
127
  el["settings-password-confirm"].value = "";
128
+ el["settings-language"].value = config.settings.language === "zh" ? "zh" : "en";
129
+ el["settings-plan"].checked = config.settings.plan_first !== false;
130
+ el["settings-approval"].value = config.settings.approval_mode === "auto" ? "auto" : "request";
114
131
  providers(el["default-provider"], config.selection.provider);
115
132
  models(el["default-provider"], el["default-model"], config.selection.model);
116
133
  intensities(el["default-intensity"], config.selection.intensity);
117
134
  renderContextWindow();
118
- const reviewers = [...new Set(config.providers.flatMap(provider => provider.models.map(model => model.name)))];
119
- options(el["settings-reviewer"], [{ value: "", label: "Same as conversation" }, ...reviewers.map(value => ({ value, label: value }))], config.settings.approval_model);
135
+ reviewerOptions(config.settings.approval_model);
120
136
  el["settings-extreme"].checked = config.settings.auto_approve_extreme_risk;
121
137
  el["settings-sound"].checked = config.settings.sound_alerts !== false;
138
+ el["settings-web-console"].checked = config.settings.web_console !== false;
122
139
  }
123
140
  function renderContextWindow() {
124
141
  const provider = config.providers.find(entry => entry.name === el["default-provider"].value);
@@ -130,21 +147,41 @@ export function createSettingsUI({ apiFetch, applyModel }) {
130
147
  function renderCompressionNote() {
131
148
  const tokens = Number(el["settings-context"].value);
132
149
  el["settings-compress-note"].textContent = Number.isSafeInteger(tokens) && tokens > 0
133
- ? `Auto-compress at 80%: ${Math.max(1, Math.floor(tokens * 0.8)).toLocaleString("en-US")} tokens. Applies to this model in all conversations.`
134
- : "Enter a positive whole number of tokens. Default: 1,000,000.";
150
+ ? t("Auto-compress at 80%: {tokens} tokens. Applies to this model in all conversations.", {
151
+ tokens: Math.max(1, Math.floor(tokens * 0.8)).toLocaleString("en-US"),
152
+ })
153
+ : t("Enter a positive whole number of tokens. Default: 1,000,000.");
154
+ }
155
+ /** Repaint option labels after a language switch without losing form edits. */
156
+ function refreshTexts() {
157
+ if (!config) return;
158
+ providers(el["model-provider"], el["model-provider"].value);
159
+ models(el["model-provider"], el["model-name"], el["model-name"].value);
160
+ intensities(el["model-intensity"], el["model-intensity"].value);
161
+ if (el["settings-dialog"].open) {
162
+ providers(el["default-provider"], el["default-provider"].value);
163
+ models(el["default-provider"], el["default-model"], el["default-model"].value);
164
+ intensities(el["default-intensity"], el["default-intensity"].value);
165
+ reviewerOptions(el["settings-reviewer"].value);
166
+ renderCompressionNote();
167
+ }
168
+ renderPicker();
135
169
  }
136
170
  el["settings-context"].addEventListener("input", renderCompressionNote);
137
171
  el["default-model"].addEventListener("change", () => { if (config) renderContextWindow(); });
172
+ el["settings-language"].addEventListener("change", () => {
173
+ if (typeof onLanguageChange === "function") onLanguageChange(el["settings-language"].value);
174
+ });
138
175
  el["settings-open"].addEventListener("click", async () => {
139
176
  if (!el["settings-dialog"].open) el["settings-dialog"].showModal();
140
177
  el["settings-save"].disabled = true;
141
- message(el["settings-feedback"], "Loading…");
178
+ message(el["settings-feedback"], t("Loading…"));
142
179
  try {
143
180
  await load();
144
181
  renderSettings();
145
- message(el["settings-feedback"], "Defaults apply to new conversations. Other settings apply to future turns.");
182
+ message(el["settings-feedback"], t("Defaults apply to new conversations. Other settings apply to future turns."));
146
183
  el["settings-save"].disabled = false;
147
- } catch (error) { message(el["settings-feedback"], error.message, true); }
184
+ } catch (error) { message(el["settings-feedback"], t(error.message), true); }
148
185
  });
149
186
  for (const id of ["settings-close", "settings-cancel"]) {
150
187
  el[id].addEventListener("click", () => el["settings-dialog"].close());
@@ -160,11 +197,11 @@ export function createSettingsUI({ apiFetch, applyModel }) {
160
197
  if (el["settings-save"].disabled) return;
161
198
  const password = el["settings-password"].value;
162
199
  if (password !== el["settings-password-confirm"].value) {
163
- message(el["settings-feedback"], "Passwords do not match.", true);
200
+ message(el["settings-feedback"], t("Passwords do not match."), true);
164
201
  return;
165
202
  }
166
203
  if (password && !password.trim()) {
167
- message(el["settings-feedback"], "Password cannot be blank.", true);
204
+ message(el["settings-feedback"], t("Password cannot be blank."), true);
168
205
  return;
169
206
  }
170
207
  el["settings-save"].disabled = true;
@@ -175,6 +212,10 @@ export function createSettingsUI({ apiFetch, applyModel }) {
175
212
  approval_model: el["settings-reviewer"].value,
176
213
  auto_approve_extreme_risk: el["settings-extreme"].checked,
177
214
  sound_alerts: el["settings-sound"].checked,
215
+ web_console: el["settings-web-console"].checked,
216
+ language: el["settings-language"].value,
217
+ plan_first: el["settings-plan"].checked,
218
+ approval_mode: el["settings-approval"].value,
178
219
  };
179
220
  if (password) settings.web_password = password;
180
221
  try {
@@ -182,12 +223,25 @@ export function createSettingsUI({ apiFetch, applyModel }) {
182
223
  renderPicker();
183
224
  el["settings-password"].value = "";
184
225
  el["settings-password-confirm"].value = "";
185
- message(el["settings-feedback"], password ? "Settings saved. Web password updated permanently." : "Settings saved.");
186
- } catch (error) { message(el["settings-feedback"], error.message, true); }
226
+ message(el["settings-feedback"], password ? t("Settings saved. Web password updated permanently.") : t("Settings saved."));
227
+ } catch (error) { message(el["settings-feedback"], t(error.message), true); }
187
228
  finally { el["settings-save"].disabled = false; }
188
229
  });
189
230
  // Read by the sound cues in app.js. Until the settings land, the feature is
190
231
  // on, matching the server-side default.
191
232
  const soundAlerts = () => config?.settings?.sound_alerts !== false;
192
- return { load, showConversation, clear, soundAlerts, selectionForFile: id => target === `file:${id}` && dirty ? choice() : undefined };
233
+ return {
234
+ load,
235
+ showConversation,
236
+ clear,
237
+ soundAlerts,
238
+ refreshTexts,
239
+ catalog: () => config,
240
+ defaultSelection: () => (config ? {
241
+ provider: config.selection.provider,
242
+ model: config.selection.model,
243
+ intensity: config.selection.intensity,
244
+ } : undefined),
245
+ selectionForFile: id => target === `file:${id}` && dirty ? choice() : undefined,
246
+ };
193
247
  }