@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.
- package/README.md +312 -464
- package/assets/pro/backend_output_model.txt +5 -0
- package/assets/pro/claude-module-prompt.txt +65 -0
- package/assets/pro/common.txt +9 -0
- package/assets/pro/confirm_model.txt +5 -0
- package/assets/pro/front_output_model.txt +9 -0
- package/assets/pro/genimage_model.txt +1 -0
- package/assets/pro/main_model.txt +5 -0
- package/assets/pro/model-template.json +376 -0
- package/assets/pro/modeling_model.txt +5 -0
- package/assets/pro/plan_model.txt +8 -0
- package/assets/pro/thinking_model.txt +3 -0
- package/dist/app.js +43 -7
- package/dist/bin.js +5 -2
- package/dist/browser.js +57 -0
- package/dist/config.js +170 -26
- package/dist/index.js +1 -1
- package/dist/llm/approval.js +6 -2
- package/dist/llm/client.js +197 -28
- package/dist/llm/subagent.js +37 -1
- package/dist/pro/agents.js +104 -0
- package/dist/pro/config.js +88 -0
- package/dist/pro/preflight.js +172 -0
- package/dist/screens/chat.js +14 -8
- package/dist/screens/modelSelect.js +15 -4
- package/dist/screens/settings.js +9 -7
- package/dist/screens/start.js +48 -15
- package/dist/ui.js +58 -0
- package/dist/web/hub.js +22 -4
- package/dist/web/server.js +279 -66
- package/dist/web/session.js +258 -22
- package/dist/web/settings.js +91 -24
- package/package.json +37 -37
- package/web/README.md +87 -13
- package/web/app.js +304 -14
- package/web/i18n.js +382 -345
- package/web/index.html +90 -14
- package/web/neutrino-pro.gif +0 -0
- package/web/neutrino.png +0 -0
- package/web/pro.js +184 -0
- package/web/settings.js +322 -247
- package/web/style.css +297 -0
package/web/settings.js
CHANGED
|
@@ -1,247 +1,322 @@
|
|
|
1
|
-
// Settings and model controls share the same server-side catalog. No API keys
|
|
2
|
-
// are returned to this controller or kept in browser storage.
|
|
3
|
-
import { t } from "./i18n.js";
|
|
4
|
-
|
|
5
|
-
export function createSettingsUI({ apiFetch, applyModel, onLanguageChange }) {
|
|
6
|
-
const ids = ["model-picker", "model-provider", "model-name", "model-intensity", "model-apply", "model-feedback",
|
|
7
|
-
"settings-open", "settings-dialog", "settings-form", "settings-close", "settings-cancel", "settings-save", "settings-feedback",
|
|
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"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
let
|
|
14
|
-
let
|
|
15
|
-
let
|
|
16
|
-
let
|
|
17
|
-
let
|
|
18
|
-
let
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
options(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
el[
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
el["
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
el["settings-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
el[
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
intensity
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
1
|
+
// Settings and model controls share the same server-side catalog. No API keys
|
|
2
|
+
// are returned to this controller or kept in browser storage.
|
|
3
|
+
import { t, getLanguage } from "./i18n.js";
|
|
4
|
+
|
|
5
|
+
export function createSettingsUI({ apiFetch, applyModel, onLanguageChange, proMode = false, onCatalogChange }) {
|
|
6
|
+
const ids = ["model-picker", "model-provider", "model-name", "model-intensity", "model-apply", "model-feedback",
|
|
7
|
+
"settings-open", "settings-dialog", "settings-form", "settings-close", "settings-cancel", "settings-save", "settings-feedback",
|
|
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", "settings-web-lan",
|
|
11
|
+
"settings-pro", "settings-pro-preset", "settings-pro-depth"];
|
|
12
|
+
const el = Object.fromEntries(ids.map(id => [id, document.getElementById(id)]));
|
|
13
|
+
let config = null;
|
|
14
|
+
let target = null;
|
|
15
|
+
let conversation = null;
|
|
16
|
+
let dirty = false;
|
|
17
|
+
let applying = false;
|
|
18
|
+
let feedback = "";
|
|
19
|
+
let feedbackError = false;
|
|
20
|
+
// Value the LAN switch had when the dialog opened. Only an actual flip is
|
|
21
|
+
// saved, so editing another setting never pins an interface that came from
|
|
22
|
+
// --lan/--host on the command line.
|
|
23
|
+
let lanInitial = false;
|
|
24
|
+
|
|
25
|
+
async function json(path, body) {
|
|
26
|
+
const response = await apiFetch(path, body === undefined ? { cache: "no-store" } : {
|
|
27
|
+
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body),
|
|
28
|
+
});
|
|
29
|
+
const data = await response.json();
|
|
30
|
+
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
function message(node, text, error = false) {
|
|
34
|
+
node.textContent = text;
|
|
35
|
+
node.classList.toggle("error", error);
|
|
36
|
+
}
|
|
37
|
+
function options(select, values, selected) {
|
|
38
|
+
select.replaceChildren();
|
|
39
|
+
for (const entry of values) {
|
|
40
|
+
const option = document.createElement("option");
|
|
41
|
+
option.value = entry.value;
|
|
42
|
+
option.textContent = entry.label;
|
|
43
|
+
select.appendChild(option);
|
|
44
|
+
}
|
|
45
|
+
select.value = values.some(entry => entry.value === selected) ? selected : values[0]?.value ?? "";
|
|
46
|
+
}
|
|
47
|
+
function providers(select, selected) {
|
|
48
|
+
options(select, config.providers.map(provider => ({ value: provider.name, label: provider.name })), selected);
|
|
49
|
+
}
|
|
50
|
+
function models(providerSelect, modelSelect, selected) {
|
|
51
|
+
const provider = config.providers.find(entry => entry.name === providerSelect.value);
|
|
52
|
+
options(modelSelect, (provider?.models ?? []).map(model => ({
|
|
53
|
+
value: model.name, label: model.name + (model.configured ? "" : ` ${t("(API key missing)")}`),
|
|
54
|
+
})), selected);
|
|
55
|
+
}
|
|
56
|
+
function intensities(select, selected, providerName, modelName) {
|
|
57
|
+
options(select, intensitiesFor(providerName, modelName).map(value => ({ value, label: value })),
|
|
58
|
+
pickIntensity(providerName, modelName, selected));
|
|
59
|
+
}
|
|
60
|
+
function modelEntry(providerName, modelName) {
|
|
61
|
+
const provider = config?.providers.find(entry => entry.name === providerName);
|
|
62
|
+
return provider?.models.find(entry => entry.name === modelName);
|
|
63
|
+
}
|
|
64
|
+
// Servers that do not know per-model limits omit the list; offer every level.
|
|
65
|
+
function intensitiesFor(providerName, modelName) {
|
|
66
|
+
const model = modelEntry(providerName, modelName);
|
|
67
|
+
return Array.isArray(model?.intensities) && model.intensities.length ? model.intensities : config?.intensities ?? [];
|
|
68
|
+
}
|
|
69
|
+
/** Keep the current level when the model accepts it, else its fallback. */
|
|
70
|
+
function pickIntensity(providerName, modelName, selected) {
|
|
71
|
+
const levels = intensitiesFor(providerName, modelName);
|
|
72
|
+
if (levels.includes(selected)) return selected;
|
|
73
|
+
const fallback = config?.settings?.default_intensity;
|
|
74
|
+
if (levels.includes(fallback)) return fallback;
|
|
75
|
+
if (levels.includes("High")) return "High";
|
|
76
|
+
return levels[0] ?? "";
|
|
77
|
+
}
|
|
78
|
+
function reviewerOptions(selected) {
|
|
79
|
+
const reviewers = [...new Set(config.providers.flatMap(provider => provider.models.map(model => model.name)))];
|
|
80
|
+
options(el["settings-reviewer"], [{ value: "", label: t("Same as conversation") }, ...reviewers.map(value => ({ value, label: value }))], selected);
|
|
81
|
+
}
|
|
82
|
+
function proPresets(selected) {
|
|
83
|
+
options(el["settings-pro-preset"], (config.pro?.presets || []).map(preset => ({
|
|
84
|
+
value: preset.name, label: getLanguage() === "zh" ? preset.chinese_name || preset.name : preset.name,
|
|
85
|
+
})), selected);
|
|
86
|
+
}
|
|
87
|
+
function choice() {
|
|
88
|
+
return { provider: el["model-provider"].value, model: el["model-name"].value, intensity: el["model-intensity"].value };
|
|
89
|
+
}
|
|
90
|
+
function renderPicker() {
|
|
91
|
+
el["model-picker"].classList.toggle("hidden", proMode || !config || !conversation);
|
|
92
|
+
if (proMode || !config || !conversation) return;
|
|
93
|
+
if (!dirty) {
|
|
94
|
+
providers(el["model-provider"], conversation.provider);
|
|
95
|
+
models(el["model-provider"], el["model-name"], conversation.model);
|
|
96
|
+
intensities(el["model-intensity"], conversation.intensity, conversation.provider, conversation.model);
|
|
97
|
+
} else {
|
|
98
|
+
// A provider/model change narrows the intensity list even though the rest
|
|
99
|
+
// of the draft must survive the next server snapshot.
|
|
100
|
+
intensities(el["model-intensity"], el["model-intensity"].value,
|
|
101
|
+
el["model-provider"].value, el["model-name"].value);
|
|
102
|
+
}
|
|
103
|
+
const busy = Boolean(conversation.busy || conversation.approval);
|
|
104
|
+
el["model-apply"].disabled = applying || busy || !choice().provider || !choice().model;
|
|
105
|
+
el["model-apply"].textContent = applying
|
|
106
|
+
? t("Applying…")
|
|
107
|
+
: target?.startsWith("file:") ? t("Continue with model") : t("Apply to conversation");
|
|
108
|
+
message(
|
|
109
|
+
el["model-feedback"],
|
|
110
|
+
feedback || (busy ? t("Pause the current turn to change models.") : t("Applies to the next turn.")),
|
|
111
|
+
feedbackError,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
async function load() {
|
|
115
|
+
config = await json("/api/settings");
|
|
116
|
+
onCatalogChange?.(config);
|
|
117
|
+
renderPicker();
|
|
118
|
+
return config;
|
|
119
|
+
}
|
|
120
|
+
function showConversation(info, key) {
|
|
121
|
+
if (target !== key) { dirty = false; feedback = ""; feedbackError = false; }
|
|
122
|
+
target = key;
|
|
123
|
+
conversation = info;
|
|
124
|
+
renderPicker();
|
|
125
|
+
}
|
|
126
|
+
function clear() {
|
|
127
|
+
target = null;
|
|
128
|
+
conversation = null;
|
|
129
|
+
dirty = false;
|
|
130
|
+
feedback = "";
|
|
131
|
+
renderPicker();
|
|
132
|
+
}
|
|
133
|
+
el["model-provider"].addEventListener("change", () => {
|
|
134
|
+
if (!config) return;
|
|
135
|
+
models(el["model-provider"], el["model-name"]);
|
|
136
|
+
dirty = true; feedback = ""; feedbackError = false;
|
|
137
|
+
renderPicker();
|
|
138
|
+
});
|
|
139
|
+
for (const id of ["model-name", "model-intensity"]) {
|
|
140
|
+
el[id].addEventListener("change", () => { dirty = true; feedback = ""; feedbackError = false; renderPicker(); });
|
|
141
|
+
}
|
|
142
|
+
el["model-picker"].addEventListener("submit", async event => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
if (el["model-apply"].disabled) return;
|
|
145
|
+
const originalTarget = target;
|
|
146
|
+
applying = true;
|
|
147
|
+
feedback = "";
|
|
148
|
+
renderPicker();
|
|
149
|
+
try {
|
|
150
|
+
await applyModel(choice());
|
|
151
|
+
if (target === originalTarget) { dirty = false; feedback = t("Model and intensity updated."); feedbackError = false; }
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (target === originalTarget) { feedback = t(error.message); feedbackError = true; }
|
|
154
|
+
} finally {
|
|
155
|
+
applying = false;
|
|
156
|
+
renderPicker();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
function renderSettings() {
|
|
161
|
+
el["settings-password"].value = "";
|
|
162
|
+
el["settings-password-confirm"].value = "";
|
|
163
|
+
el["settings-language"].value = config.settings.language === "zh" ? "zh" : "en";
|
|
164
|
+
el["settings-plan"].checked = config.settings.plan_first !== false;
|
|
165
|
+
el["settings-approval"].value = config.settings.approval_mode === "auto" ? "auto" : "request";
|
|
166
|
+
providers(el["default-provider"], config.selection.provider);
|
|
167
|
+
models(el["default-provider"], el["default-model"], config.selection.model);
|
|
168
|
+
intensities(el["default-intensity"], config.selection.intensity,
|
|
169
|
+
el["default-provider"].value, el["default-model"].value);
|
|
170
|
+
renderContextWindow();
|
|
171
|
+
reviewerOptions(config.settings.approval_model);
|
|
172
|
+
if (proMode && el["settings-pro"]) {
|
|
173
|
+
el["settings-pro"].classList.remove("hidden");
|
|
174
|
+
proPresets(config.pro?.preset);
|
|
175
|
+
el["settings-pro-depth"].value = String(config.pro?.max_recursion_depth ?? 10);
|
|
176
|
+
}
|
|
177
|
+
el["settings-extreme"].checked = config.settings.auto_approve_extreme_risk;
|
|
178
|
+
el["settings-sound"].checked = config.settings.sound_alerts !== false;
|
|
179
|
+
el["settings-web-console"].checked = config.settings.web_console !== false;
|
|
180
|
+
el["settings-web-lan"].checked = config.settings.web_lan === true;
|
|
181
|
+
lanInitial = el["settings-web-lan"].checked;
|
|
182
|
+
}
|
|
183
|
+
function renderContextWindow() {
|
|
184
|
+
const provider = config.providers.find(entry => entry.name === el["default-provider"].value);
|
|
185
|
+
const model = provider?.models.find(entry => entry.name === el["default-model"].value);
|
|
186
|
+
el["settings-context"].disabled = proMode || !model;
|
|
187
|
+
el["settings-context"].value = String(model?.context_window ?? 1_000_000);
|
|
188
|
+
renderCompressionNote();
|
|
189
|
+
}
|
|
190
|
+
function renderCompressionNote() {
|
|
191
|
+
const tokens = Number(el["settings-context"].value);
|
|
192
|
+
el["settings-compress-note"].textContent = Number.isSafeInteger(tokens) && tokens > 0
|
|
193
|
+
? t("Auto-compress at 80%: {tokens} tokens. Applies to this model in all conversations.", {
|
|
194
|
+
tokens: Math.max(1, Math.floor(tokens * 0.8)).toLocaleString("en-US"),
|
|
195
|
+
})
|
|
196
|
+
: t("Enter a positive whole number of tokens. Default: 1,000,000.");
|
|
197
|
+
}
|
|
198
|
+
/** Repaint option labels after a language switch without losing form edits. */
|
|
199
|
+
function refreshTexts() {
|
|
200
|
+
if (!config) return;
|
|
201
|
+
providers(el["model-provider"], el["model-provider"].value);
|
|
202
|
+
models(el["model-provider"], el["model-name"], el["model-name"].value);
|
|
203
|
+
intensities(el["model-intensity"], el["model-intensity"].value,
|
|
204
|
+
el["model-provider"].value, el["model-name"].value);
|
|
205
|
+
if (el["settings-dialog"].open) {
|
|
206
|
+
providers(el["default-provider"], el["default-provider"].value);
|
|
207
|
+
models(el["default-provider"], el["default-model"], el["default-model"].value);
|
|
208
|
+
intensities(el["default-intensity"], el["default-intensity"].value,
|
|
209
|
+
el["default-provider"].value, el["default-model"].value);
|
|
210
|
+
reviewerOptions(el["settings-reviewer"].value);
|
|
211
|
+
renderCompressionNote();
|
|
212
|
+
if (proMode) proPresets(el["settings-pro-preset"].value);
|
|
213
|
+
}
|
|
214
|
+
renderPicker();
|
|
215
|
+
}
|
|
216
|
+
el["settings-context"].addEventListener("input", renderCompressionNote);
|
|
217
|
+
el["default-model"].addEventListener("change", () => {
|
|
218
|
+
if (!config) return;
|
|
219
|
+
intensities(el["default-intensity"], el["default-intensity"].value,
|
|
220
|
+
el["default-provider"].value, el["default-model"].value);
|
|
221
|
+
renderContextWindow();
|
|
222
|
+
});
|
|
223
|
+
el["settings-language"].addEventListener("change", () => {
|
|
224
|
+
if (typeof onLanguageChange === "function") onLanguageChange(el["settings-language"].value);
|
|
225
|
+
});
|
|
226
|
+
el["settings-open"].addEventListener("click", async () => {
|
|
227
|
+
if (!el["settings-dialog"].open) el["settings-dialog"].showModal();
|
|
228
|
+
el["settings-save"].disabled = true;
|
|
229
|
+
message(el["settings-feedback"], t("Loading…"));
|
|
230
|
+
try {
|
|
231
|
+
await load();
|
|
232
|
+
renderSettings();
|
|
233
|
+
message(el["settings-feedback"], t("Defaults apply to new conversations. Other settings apply to future turns."));
|
|
234
|
+
el["settings-save"].disabled = false;
|
|
235
|
+
} catch (error) { message(el["settings-feedback"], t(error.message), true); }
|
|
236
|
+
});
|
|
237
|
+
for (const id of ["settings-close", "settings-cancel"]) {
|
|
238
|
+
el[id].addEventListener("click", () => el["settings-dialog"].close());
|
|
239
|
+
}
|
|
240
|
+
el["default-provider"].addEventListener("change", () => {
|
|
241
|
+
if (config) {
|
|
242
|
+
models(el["default-provider"], el["default-model"]);
|
|
243
|
+
intensities(el["default-intensity"], el["default-intensity"].value,
|
|
244
|
+
el["default-provider"].value, el["default-model"].value);
|
|
245
|
+
renderContextWindow();
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
el["settings-form"].addEventListener("submit", async event => {
|
|
249
|
+
event.preventDefault();
|
|
250
|
+
if (el["settings-save"].disabled) return;
|
|
251
|
+
const password = el["settings-password"].value;
|
|
252
|
+
if (password !== el["settings-password-confirm"].value) {
|
|
253
|
+
message(el["settings-feedback"], t("Passwords do not match."), true);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (password && !password.trim()) {
|
|
257
|
+
message(el["settings-feedback"], t("Password cannot be blank."), true);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
el["settings-save"].disabled = true;
|
|
261
|
+
const wasLan = config?.settings?.web_lan === true;
|
|
262
|
+
const lanChanged = el["settings-web-lan"].checked !== lanInitial;
|
|
263
|
+
const selection = { provider: el["default-provider"].value, model: el["default-model"].value, intensity: el["default-intensity"].value };
|
|
264
|
+
const settings = {
|
|
265
|
+
...(!proMode ? {
|
|
266
|
+
default_intensity: selection.intensity,
|
|
267
|
+
...(!el["settings-context"].disabled ? { model_context_window: Number(el["settings-context"].value) } : {}),
|
|
268
|
+
approval_model: el["settings-reviewer"].value,
|
|
269
|
+
plan_first: el["settings-plan"].checked,
|
|
270
|
+
} : {
|
|
271
|
+
pro_preset: el["settings-pro-preset"].value,
|
|
272
|
+
pro_max_recursion_depth: Number(el["settings-pro-depth"].value),
|
|
273
|
+
}),
|
|
274
|
+
auto_approve_extreme_risk: el["settings-extreme"].checked,
|
|
275
|
+
sound_alerts: el["settings-sound"].checked,
|
|
276
|
+
web_console: el["settings-web-console"].checked,
|
|
277
|
+
...(lanChanged ? { web_lan: el["settings-web-lan"].checked } : {}),
|
|
278
|
+
language: el["settings-language"].value,
|
|
279
|
+
approval_mode: el["settings-approval"].value,
|
|
280
|
+
};
|
|
281
|
+
if (password) settings.web_password = password;
|
|
282
|
+
try {
|
|
283
|
+
config = await json("/api/settings", { ...(!proMode && selection.provider && selection.model ? { selection } : {}), settings });
|
|
284
|
+
onCatalogChange?.(config);
|
|
285
|
+
renderPicker();
|
|
286
|
+
el["settings-password"].value = "";
|
|
287
|
+
el["settings-password-confirm"].value = "";
|
|
288
|
+
// The switch takes effect on the live socket, so report exactly what the
|
|
289
|
+
// server bound and (when available) the address to open elsewhere.
|
|
290
|
+
el["settings-web-lan"].checked = config.settings.web_lan === true;
|
|
291
|
+
lanInitial = el["settings-web-lan"].checked;
|
|
292
|
+
const urls = Array.isArray(config.settings.web_urls) ? config.settings.web_urls : [];
|
|
293
|
+
const lanNote = config.settings.web_lan === wasLan ? "" : config.settings.web_lan
|
|
294
|
+
? urls.length
|
|
295
|
+
? t("LAN access is on. Open {urls} from another device.", { urls: urls.join(" · ") })
|
|
296
|
+
: t("LAN access is on, but no network address was found. Only this machine can connect.")
|
|
297
|
+
: t("LAN access is off. Only this machine can connect.");
|
|
298
|
+
const savedText = password
|
|
299
|
+
? t("Settings saved. Web password updated permanently.")
|
|
300
|
+
: t("Settings saved.");
|
|
301
|
+
message(el["settings-feedback"], lanNote ? `${savedText} ${lanNote}` : savedText);
|
|
302
|
+
} catch (error) { message(el["settings-feedback"], t(error.message), true); }
|
|
303
|
+
finally { el["settings-save"].disabled = false; }
|
|
304
|
+
});
|
|
305
|
+
// Read by the sound cues in app.js. Until the settings land, the feature is
|
|
306
|
+
// on, matching the server-side default.
|
|
307
|
+
const soundAlerts = () => config?.settings?.sound_alerts !== false;
|
|
308
|
+
return {
|
|
309
|
+
load,
|
|
310
|
+
showConversation,
|
|
311
|
+
clear,
|
|
312
|
+
soundAlerts,
|
|
313
|
+
refreshTexts,
|
|
314
|
+
catalog: () => config,
|
|
315
|
+
defaultSelection: () => (config ? {
|
|
316
|
+
provider: config.selection.provider,
|
|
317
|
+
model: config.selection.model,
|
|
318
|
+
intensity: config.selection.intensity,
|
|
319
|
+
} : undefined),
|
|
320
|
+
selectionForFile: id => target === `file:${id}` && dirty ? choice() : undefined,
|
|
321
|
+
};
|
|
322
|
+
}
|