@xingjisoft/neutrino 0.2.1 → 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/README.md +35 -7
- package/dist/config.js +9 -0
- package/dist/screens/chat.js +40 -30
- package/dist/web/hub.js +9 -0
- package/dist/web/server.js +353 -5
- package/dist/web/session.js +65 -1
- package/dist/web/settings.js +27 -1
- package/package.json +2 -2
- package/web/README.md +83 -15
- package/web/app.js +1309 -124
- package/web/i18n.js +345 -0
- package/web/index.html +210 -34
- package/web/markdown.js +710 -0
- package/web/settings.js +73 -19
- package/web/style.css +1124 -85
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
|
-
|
|
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",
|
|
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 ? "" :
|
|
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
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
134
|
-
|
|
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 {
|
|
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
|
}
|