@aryacode/dsh-local-llm 0.0.0-stage → 0.8.2
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/LEGACY-CONTROL-CENTER.md +161 -0
- package/README.md +89 -3
- package/blue.png +0 -0
- package/cordis.patch.yml +4 -0
- package/icon.svg +11 -0
- package/lib/client.js +2272 -0
- package/lib/client.js.map +1 -0
- package/lib/event-stream-BkIJ4cYy.js +80 -0
- package/lib/event-stream-CiG-8VvD.js +80 -0
- package/lib/index.d.ts +2411 -0
- package/lib/index.js +10263 -0
- package/lib/openai-completions-B6T4-oyq.js +11710 -0
- package/lib/openai-completions-C58ZnIdi.js +11710 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +73 -6
- package/scripts/freetoken-control.py +82 -0
- package/scripts/ninfer-control.py +120 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,2272 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@aryacode/dsh-local-llm",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
//#region src/model-output.ts
|
|
10
|
+
const localProviders = [
|
|
11
|
+
"ninfer",
|
|
12
|
+
"llamacpp",
|
|
13
|
+
"strata",
|
|
14
|
+
"freetoken",
|
|
15
|
+
"ollama"
|
|
16
|
+
];
|
|
17
|
+
/** Include a loopback OpenAI-compatible Ollama route without exposing cloud providers. */
|
|
18
|
+
function localRoute(provider, value) {
|
|
19
|
+
if (localProviders.includes(provider)) return true;
|
|
20
|
+
if (provider !== "openai" || !value.baseURL) return false;
|
|
21
|
+
try {
|
|
22
|
+
return [
|
|
23
|
+
"localhost",
|
|
24
|
+
"127.0.0.1",
|
|
25
|
+
"[::1]"
|
|
26
|
+
].includes(new URL(value.baseURL).hostname);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** Read output-capable local chat models even when their engines are stopped. */
|
|
32
|
+
function outputCatalog(namespace) {
|
|
33
|
+
return Object.entries(namespace?.value.providers ?? {}).filter(([provider, value]) => localRoute(provider, value)).flatMap(([provider, value]) => (value.models ?? []).flatMap((model) => {
|
|
34
|
+
if (typeof model.contextWindow !== "number" || !Number.isSafeInteger(model.contextWindow) || model.contextWindow <= 1024) return [];
|
|
35
|
+
return [{
|
|
36
|
+
...model,
|
|
37
|
+
contextWindow: model.contextWindow,
|
|
38
|
+
provider,
|
|
39
|
+
key: provider + "\n" + model.id,
|
|
40
|
+
engine: provider === "openai" ? "ollama" : provider
|
|
41
|
+
}];
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
/** Validate a per-response allowance against the selected model's configured context. */
|
|
45
|
+
function validateOutputLimit(value, contextWindow) {
|
|
46
|
+
if (!Number.isSafeInteger(value) || value < 1024 || value >= contextWindow) throw Error("Choose a whole output limit between 1,024 tokens and below the model context, so input history keeps room.");
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
/** Update only the requested model in freshly read settings, preserving other model fields. */
|
|
50
|
+
function outputModels(namespace, provider, id, value) {
|
|
51
|
+
if (!namespace.value.providers[provider] || !localRoute(provider, namespace.value.providers[provider])) throw Error("Choose a local model provider.");
|
|
52
|
+
const models = namespace.value.providers[provider]?.models;
|
|
53
|
+
const model = models?.find((entry) => entry.id === id);
|
|
54
|
+
if (!model || !model.contextWindow) throw Error("This model is no longer configured. Reload models.");
|
|
55
|
+
const maxTokens = validateOutputLimit(value, model.contextWindow);
|
|
56
|
+
return models.map((entry) => entry.id === id ? {
|
|
57
|
+
...entry,
|
|
58
|
+
maxTokens
|
|
59
|
+
} : entry);
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/client/output-settings.ts
|
|
63
|
+
/** Use the host settings service with optimistic revision checks. */
|
|
64
|
+
async function outputRpc(method, args = {}, signal) {
|
|
65
|
+
const response = await fetch("/api/" + method, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
type: "client-request",
|
|
70
|
+
rpcId: "local-llm-output",
|
|
71
|
+
method,
|
|
72
|
+
payload: { args }
|
|
73
|
+
}),
|
|
74
|
+
...signal ? { signal } : {}
|
|
75
|
+
});
|
|
76
|
+
const data = await response.json();
|
|
77
|
+
if (!response.ok || !data.result?.ok) throw Error(data.result?.error?.message ?? "Model settings could not be updated.");
|
|
78
|
+
return data.result.value;
|
|
79
|
+
}
|
|
80
|
+
/** Read configured chat models without requiring a running inference engine. */
|
|
81
|
+
async function describeOutput(signal) {
|
|
82
|
+
const namespace = (await outputRpc("settings/describe", {}, signal)).namespaces.find((entry) => entry.ns === "llm-pi-ai");
|
|
83
|
+
if (!namespace) throw Error("Chat model settings are unavailable.");
|
|
84
|
+
return namespace;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/client/Connection.tsx
|
|
88
|
+
/** Read errors before JSON decoding so a missing backend never looks like malformed model data. */
|
|
89
|
+
async function responseData(response) {
|
|
90
|
+
if (!response.headers.get("content-type")?.includes("application/json")) throw Error(response.status === 404 ? "This control is unavailable. Check the engine setup or the plugin’s startup status." : `The host returned an unexpected response (${response.status}). Try reconnecting.`);
|
|
91
|
+
const result = await response.json();
|
|
92
|
+
if (!response.ok) throw Error(result.detail ?? `Request failed (${response.status}).`);
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/client/TuningSlider.tsx
|
|
97
|
+
const contextPoints = [
|
|
98
|
+
4096,
|
|
99
|
+
8192,
|
|
100
|
+
16384,
|
|
101
|
+
32768,
|
|
102
|
+
65536,
|
|
103
|
+
98304,
|
|
104
|
+
102400,
|
|
105
|
+
131072,
|
|
106
|
+
196608,
|
|
107
|
+
262144
|
|
108
|
+
];
|
|
109
|
+
const copy$2 = {
|
|
110
|
+
slider: "slider",
|
|
111
|
+
exact: "Exact value",
|
|
112
|
+
tokens: "tokens",
|
|
113
|
+
contextHint: "More context reserves more memory. Filled conversations also take longer to process.",
|
|
114
|
+
low: "Minimum",
|
|
115
|
+
high: "Maximum"
|
|
116
|
+
};
|
|
117
|
+
/** Format the actual setting without rounding away an exact value. */
|
|
118
|
+
function display(value, context) {
|
|
119
|
+
return context ? `${Number((value / 1024).toFixed(2))}K` : Number.isInteger(value) ? value.toLocaleString() : String(value);
|
|
120
|
+
}
|
|
121
|
+
/** Pair keyboard-accessible sliders and quick context choices with an exact numeric input. */
|
|
122
|
+
function TuningSlider({ label, value, min, max, step = 1, onChange, disabled = false, context = false, hint }) {
|
|
123
|
+
const id = (0, react.useId)(), description = hint ?? (context ? copy$2.contextHint : void 0);
|
|
124
|
+
const points = context ? contextPoints.filter((n) => n >= min && n <= max) : [];
|
|
125
|
+
const finite = Number.isFinite(value), safe = finite ? Math.min(max, Math.max(min, value)) : min;
|
|
126
|
+
const index = points.reduce((best, n, i) => Math.abs(n - safe) < Math.abs(points[best] - safe) ? i : best, 0);
|
|
127
|
+
const quick = points.filter((n) => [
|
|
128
|
+
32768,
|
|
129
|
+
65536,
|
|
130
|
+
102400,
|
|
131
|
+
131072,
|
|
132
|
+
262144
|
|
133
|
+
].includes(n));
|
|
134
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
135
|
+
className: "lc-tune-control",
|
|
136
|
+
children: [
|
|
137
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
138
|
+
className: "lc-tune-heading",
|
|
139
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
140
|
+
htmlFor: id + "-range",
|
|
141
|
+
children: label
|
|
142
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("output", {
|
|
143
|
+
htmlFor: id + "-range",
|
|
144
|
+
children: finite ? display(value, context) : "—"
|
|
145
|
+
})]
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
148
|
+
className: "lc-tune-row",
|
|
149
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
150
|
+
id: id + "-range",
|
|
151
|
+
className: "lc-tune-range",
|
|
152
|
+
type: "range",
|
|
153
|
+
"aria-label": label + " " + copy$2.slider,
|
|
154
|
+
"aria-describedby": description ? id + "-hint" : void 0,
|
|
155
|
+
"aria-valuetext": finite ? display(value, context) + (context ? " " + copy$2.tokens : "") : void 0,
|
|
156
|
+
min: context ? 0 : min,
|
|
157
|
+
max: context ? points.length - 1 : max,
|
|
158
|
+
step: context ? 1 : step,
|
|
159
|
+
value: context ? index : safe,
|
|
160
|
+
disabled,
|
|
161
|
+
onChange: (event) => onChange(context ? points[Number(event.target.value)] : Number(event.target.value))
|
|
162
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
163
|
+
className: "lc-input lc-tune-exact",
|
|
164
|
+
"aria-label": label,
|
|
165
|
+
title: copy$2.exact,
|
|
166
|
+
type: "number",
|
|
167
|
+
min,
|
|
168
|
+
max,
|
|
169
|
+
step,
|
|
170
|
+
value: finite ? value : "",
|
|
171
|
+
disabled,
|
|
172
|
+
onChange: (event) => onChange(event.target.value === "" ? NaN : Number(event.target.value))
|
|
173
|
+
})]
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
176
|
+
className: "lc-tune-scale",
|
|
177
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display(min, context) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display(max, context) })]
|
|
178
|
+
}),
|
|
179
|
+
context && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
180
|
+
className: "lc-tune-choices",
|
|
181
|
+
role: "group",
|
|
182
|
+
"aria-label": label + " quick choices",
|
|
183
|
+
children: quick.map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
184
|
+
type: "button",
|
|
185
|
+
className: "lc-button " + (value === n ? "primary" : "ghost"),
|
|
186
|
+
"aria-label": label + " " + display(n, true),
|
|
187
|
+
"aria-pressed": value === n,
|
|
188
|
+
disabled,
|
|
189
|
+
onClick: () => onChange(n),
|
|
190
|
+
children: display(n, true)
|
|
191
|
+
}, n))
|
|
192
|
+
}),
|
|
193
|
+
description && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
194
|
+
id: id + "-hint",
|
|
195
|
+
className: "lc-sub",
|
|
196
|
+
children: description
|
|
197
|
+
})
|
|
198
|
+
]
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/client/OutputTuning.tsx
|
|
203
|
+
const copy$1 = {
|
|
204
|
+
title: "Chat output",
|
|
205
|
+
model: "Chat model",
|
|
206
|
+
limit: "Output tokens per response",
|
|
207
|
+
save: "Save output limit",
|
|
208
|
+
reload: "Reload models",
|
|
209
|
+
empty: "No configured local chat models.",
|
|
210
|
+
note: "Thinking, tool arguments and the answer share this allowance. It applies to each response, not the whole task. Input plus output must fit the context, and the limit must stay below the context window. The active compression gateway reclaims output room after compressing history. The engine context window still limits input plus output.",
|
|
211
|
+
saved: "Output limit saved. It applies to subsequent model requests; an in-progress response keeps its existing limit.",
|
|
212
|
+
remote: "Open Tuning on the local host to change its chat model output limits."
|
|
213
|
+
};
|
|
214
|
+
/** Edit persisted chat output defaults independently of server context and sampling. */
|
|
215
|
+
function OutputTuning({ remote, initialModel = "" }) {
|
|
216
|
+
const [namespace, setNamespace] = (0, react.useState)(null), [appliedInitial, setAppliedInitial] = (0, react.useState)(""), [selected, setSelected] = (0, react.useState)(""), [value, setValue] = (0, react.useState)(65536), [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)(""), [message, setMessage] = (0, react.useState)("");
|
|
217
|
+
const models = outputCatalog(namespace);
|
|
218
|
+
const model = models.find((entry) => entry.key === selected), max = model?.contextWindow ?? 102400, ceiling = Math.max(1024, max - 1024), recommended = Math.max(1024, Math.floor(max / 2));
|
|
219
|
+
async function load() {
|
|
220
|
+
setBusy(true);
|
|
221
|
+
setError("");
|
|
222
|
+
try {
|
|
223
|
+
setNamespace(await describeOutput());
|
|
224
|
+
} catch (e) {
|
|
225
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
226
|
+
} finally {
|
|
227
|
+
setBusy(false);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
(0, react.useEffect)(() => {
|
|
231
|
+
if (!remote) load();
|
|
232
|
+
}, [remote]);
|
|
233
|
+
(0, react.useEffect)(() => {
|
|
234
|
+
if (!initialModel || !namespace || appliedInitial === initialModel) return;
|
|
235
|
+
const next = outputCatalog(namespace).find((model) => model.key === initialModel);
|
|
236
|
+
if (next) {
|
|
237
|
+
setAppliedInitial(initialModel);
|
|
238
|
+
setSelected(initialModel);
|
|
239
|
+
setValue(next.maxTokens ?? Math.min(65536, next.contextWindow - 1024));
|
|
240
|
+
setMessage("");
|
|
241
|
+
setError("");
|
|
242
|
+
}
|
|
243
|
+
}, [
|
|
244
|
+
initialModel,
|
|
245
|
+
namespace,
|
|
246
|
+
appliedInitial
|
|
247
|
+
]);
|
|
248
|
+
function choose(key) {
|
|
249
|
+
setSelected(key);
|
|
250
|
+
const next = models.find((entry) => entry.key === key);
|
|
251
|
+
setValue(next?.maxTokens ?? Math.min(65536, next?.contextWindow ?? 65536));
|
|
252
|
+
setMessage("");
|
|
253
|
+
setError("");
|
|
254
|
+
}
|
|
255
|
+
async function save() {
|
|
256
|
+
if (!model) return;
|
|
257
|
+
setBusy(true);
|
|
258
|
+
setError("");
|
|
259
|
+
setMessage("");
|
|
260
|
+
try {
|
|
261
|
+
const fresh = await describeOutput();
|
|
262
|
+
const updated = outputModels(fresh, model.provider, model.id, value);
|
|
263
|
+
await outputRpc("settings/mutate", {
|
|
264
|
+
ns: fresh.ns,
|
|
265
|
+
expectedRevision: fresh.revision,
|
|
266
|
+
ops: [{
|
|
267
|
+
op: "set",
|
|
268
|
+
path: [
|
|
269
|
+
"providers",
|
|
270
|
+
model.provider,
|
|
271
|
+
"models"
|
|
272
|
+
],
|
|
273
|
+
value: updated
|
|
274
|
+
}]
|
|
275
|
+
});
|
|
276
|
+
setNamespace(await describeOutput());
|
|
277
|
+
setMessage(copy$1.saved);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
280
|
+
} finally {
|
|
281
|
+
setBusy(false);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
let invalid = false;
|
|
285
|
+
try {
|
|
286
|
+
validateOutputLimit(value, max);
|
|
287
|
+
} catch {
|
|
288
|
+
invalid = true;
|
|
289
|
+
}
|
|
290
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
291
|
+
className: "lc-card",
|
|
292
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", { children: copy$1.title }), remote ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
293
|
+
className: "lc-sub",
|
|
294
|
+
children: copy$1.remote
|
|
295
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
297
|
+
className: "lc-field",
|
|
298
|
+
children: [copy$1.model, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
299
|
+
className: "lc-input",
|
|
300
|
+
"aria-label": copy$1.model,
|
|
301
|
+
value: selected,
|
|
302
|
+
disabled: busy,
|
|
303
|
+
onChange: (event) => choose(event.target.value),
|
|
304
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
305
|
+
value: "",
|
|
306
|
+
children: "Choose a model"
|
|
307
|
+
}), models.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
308
|
+
value: entry.key,
|
|
309
|
+
children: [
|
|
310
|
+
entry.name ?? entry.id,
|
|
311
|
+
" · ",
|
|
312
|
+
entry.provider
|
|
313
|
+
]
|
|
314
|
+
}, entry.key))]
|
|
315
|
+
})]
|
|
316
|
+
}),
|
|
317
|
+
model && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
318
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
319
|
+
className: "lc-sub",
|
|
320
|
+
children: [
|
|
321
|
+
"Context: ",
|
|
322
|
+
(max / 1024).toLocaleString(),
|
|
323
|
+
"K tokens · saved output: ",
|
|
324
|
+
model.maxTokens?.toLocaleString() ?? "provider default"
|
|
325
|
+
]
|
|
326
|
+
}),
|
|
327
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TuningSlider, {
|
|
328
|
+
label: copy$1.limit,
|
|
329
|
+
value,
|
|
330
|
+
min: 1024,
|
|
331
|
+
max: ceiling,
|
|
332
|
+
step: 1024,
|
|
333
|
+
disabled: busy,
|
|
334
|
+
hint: copy$1.note,
|
|
335
|
+
onChange: setValue
|
|
336
|
+
}),
|
|
337
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
338
|
+
className: "lc-actions",
|
|
339
|
+
children: [
|
|
340
|
+
8192,
|
|
341
|
+
16384,
|
|
342
|
+
32768,
|
|
343
|
+
65536
|
|
344
|
+
].filter((n) => n <= recommended).map((n) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
345
|
+
className: "lc-button " + (n === value ? "primary" : "ghost"),
|
|
346
|
+
disabled: busy,
|
|
347
|
+
onClick: () => setValue(n),
|
|
348
|
+
children: [n / 1024, "K output"]
|
|
349
|
+
}, n))
|
|
350
|
+
})
|
|
351
|
+
] }),
|
|
352
|
+
!busy && !models.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
353
|
+
className: "lc-sub",
|
|
354
|
+
children: copy$1.empty
|
|
355
|
+
}),
|
|
356
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
357
|
+
className: "lc-actions",
|
|
358
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
359
|
+
className: "lc-button primary",
|
|
360
|
+
disabled: busy || !model || invalid,
|
|
361
|
+
onClick: () => void save(),
|
|
362
|
+
children: copy$1.save
|
|
363
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
364
|
+
className: "lc-button",
|
|
365
|
+
disabled: busy,
|
|
366
|
+
onClick: () => void load(),
|
|
367
|
+
children: copy$1.reload
|
|
368
|
+
})]
|
|
369
|
+
}),
|
|
370
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
371
|
+
className: "lc-notice error",
|
|
372
|
+
role: "alert",
|
|
373
|
+
children: error
|
|
374
|
+
}),
|
|
375
|
+
message && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
376
|
+
className: "lc-notice",
|
|
377
|
+
role: "status",
|
|
378
|
+
children: message
|
|
379
|
+
})
|
|
380
|
+
] })]
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
//#endregion
|
|
384
|
+
//#region src/client/NInferTuning.tsx
|
|
385
|
+
const copy = {
|
|
386
|
+
title: "NInfer tuning",
|
|
387
|
+
context: "Context tokens",
|
|
388
|
+
prefill: "Prompt chunk tokens",
|
|
389
|
+
hostKvMiB: "Host cache MiB",
|
|
390
|
+
draftTokens: "MTP draft tokens",
|
|
391
|
+
outputTokens: "Response output tokens",
|
|
392
|
+
ngram: "N-gram drafts",
|
|
393
|
+
vision: "Enable vision",
|
|
394
|
+
save: "Save NInfer settings",
|
|
395
|
+
note: "Stop NInfer before saving. Context is shared by the prompt and the reply, so the output allowance must stay below it. MTP and vision use GPU memory. Configured context is not a retrieval test. Match the chat model context to this setting. The n-gram chain drafts from the prompt and the reply as it is written: it pays off on repetitive and structured output and costs a little on novel prose."
|
|
396
|
+
};
|
|
397
|
+
/** Preserve unsaved edits across polling and edit only a stopped runtime. */
|
|
398
|
+
function NInferTuning({ state, busy, save }) {
|
|
399
|
+
const [values, setValues] = (0, react.useState)(null);
|
|
400
|
+
(0, react.useEffect)(() => {
|
|
401
|
+
if (state?.context !== void 0 && state.prefill !== void 0 && state.hostKvMiB !== void 0 && state.draftTokens !== void 0 && state.vision !== void 0 && state.ngram !== void 0 && state.outputTokens !== void 0) setValues({
|
|
402
|
+
context: state.context,
|
|
403
|
+
prefill: state.prefill,
|
|
404
|
+
hostKvMiB: state.hostKvMiB,
|
|
405
|
+
draftTokens: state.draftTokens,
|
|
406
|
+
vision: state.vision,
|
|
407
|
+
ngram: state.ngram,
|
|
408
|
+
outputTokens: state.outputTokens
|
|
409
|
+
});
|
|
410
|
+
}, [
|
|
411
|
+
state?.context,
|
|
412
|
+
state?.prefill,
|
|
413
|
+
state?.hostKvMiB,
|
|
414
|
+
state?.draftTokens,
|
|
415
|
+
state?.vision,
|
|
416
|
+
state?.ngram,
|
|
417
|
+
state?.outputTokens
|
|
418
|
+
]);
|
|
419
|
+
if (!values) return null;
|
|
420
|
+
const disabled = busy || state?.phase !== "stopped";
|
|
421
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
422
|
+
style: { marginTop: 18 },
|
|
423
|
+
children: [
|
|
424
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
425
|
+
className: "lc-muted",
|
|
426
|
+
children: copy.title
|
|
427
|
+
}),
|
|
428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
429
|
+
className: "lc-facts",
|
|
430
|
+
children: [[
|
|
431
|
+
[
|
|
432
|
+
"context",
|
|
433
|
+
4096,
|
|
434
|
+
102400
|
|
435
|
+
],
|
|
436
|
+
[
|
|
437
|
+
"prefill",
|
|
438
|
+
128,
|
|
439
|
+
4096
|
|
440
|
+
],
|
|
441
|
+
[
|
|
442
|
+
"hostKvMiB",
|
|
443
|
+
0,
|
|
444
|
+
16384
|
|
445
|
+
],
|
|
446
|
+
[
|
|
447
|
+
"draftTokens",
|
|
448
|
+
1,
|
|
449
|
+
3
|
|
450
|
+
]
|
|
451
|
+
].map(([key, min, max]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TuningSlider, {
|
|
452
|
+
label: "NInfer " + copy[key],
|
|
453
|
+
value: values[key],
|
|
454
|
+
min,
|
|
455
|
+
max,
|
|
456
|
+
disabled,
|
|
457
|
+
context: key === "context",
|
|
458
|
+
onChange: (value) => setValues((current) => {
|
|
459
|
+
if (!current) return current;
|
|
460
|
+
const next = {
|
|
461
|
+
...current,
|
|
462
|
+
[key]: value
|
|
463
|
+
};
|
|
464
|
+
if (next.outputTokens > next.context - 1024) next.outputTokens = Math.max(1024, next.context - 1024);
|
|
465
|
+
return next;
|
|
466
|
+
})
|
|
467
|
+
}, key)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TuningSlider, {
|
|
468
|
+
label: "NInfer " + copy.outputTokens,
|
|
469
|
+
value: values.outputTokens,
|
|
470
|
+
min: 1024,
|
|
471
|
+
max: Math.max(1024, values.context - 1024),
|
|
472
|
+
disabled,
|
|
473
|
+
onChange: (value) => setValues((values) => ({
|
|
474
|
+
...values,
|
|
475
|
+
outputTokens: value
|
|
476
|
+
}))
|
|
477
|
+
})]
|
|
478
|
+
}),
|
|
479
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
480
|
+
className: "lc-field",
|
|
481
|
+
children: [copy.ngram, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
482
|
+
className: "lc-input",
|
|
483
|
+
disabled,
|
|
484
|
+
value: values.ngram,
|
|
485
|
+
onChange: (event) => setValues({
|
|
486
|
+
...values,
|
|
487
|
+
ngram: event.target.value === "off" ? "off" : "chain"
|
|
488
|
+
}),
|
|
489
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
490
|
+
value: "chain",
|
|
491
|
+
children: "Chain"
|
|
492
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
493
|
+
value: "off",
|
|
494
|
+
children: "Off"
|
|
495
|
+
})]
|
|
496
|
+
})]
|
|
497
|
+
}),
|
|
498
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
499
|
+
className: "lc-field",
|
|
500
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
501
|
+
type: "checkbox",
|
|
502
|
+
disabled,
|
|
503
|
+
checked: values.vision,
|
|
504
|
+
onChange: (event) => setValues({
|
|
505
|
+
...values,
|
|
506
|
+
vision: event.target.checked
|
|
507
|
+
})
|
|
508
|
+
}), copy.vision]
|
|
509
|
+
}),
|
|
510
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
511
|
+
className: "lc-button primary",
|
|
512
|
+
disabled,
|
|
513
|
+
onClick: () => save(values),
|
|
514
|
+
children: copy.save
|
|
515
|
+
}),
|
|
516
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
517
|
+
className: "lc-sub",
|
|
518
|
+
children: copy.note
|
|
519
|
+
})
|
|
520
|
+
]
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/client/theme.ts
|
|
525
|
+
const css = ".lc{--bg:var(--dsw-alias-bg-base);--panel:var(--dsw-alias-bg-layer-1);--line:var(--dsw-alias-border-l1);--ink:var(--dsw-alias-label-primary);--muted:var(--dsw-alias-label-secondary);--blue:var(--dsw-alias-label-primary);--teal:var(--dsw-alias-state-success-primary);box-sizing:border-box;height:100%;overflow:auto;background:var(--bg);color:var(--ink);font:14px/1.5 var(--dsw-font-family-sans,system-ui,sans-serif);padding:30px 34px}.lc *{box-sizing:border-box}.lc button,.lc input,.lc select{font:inherit}.lc button{cursor:pointer}.lc button:disabled{opacity:.4;cursor:default}.lc a{color:var(--blue)}.lc h1,.lc h2,.lc h3,.lc p{margin:0}.lc h1{font-size:30px;letter-spacing:-1.1px;font-weight:500}.lc h2{font-size:18px;letter-spacing:-.3px}.lc h3{font-size:15px}.lc-muted{color:var(--muted)}.lc-header{display:flex;justify-content:space-between;align-items:center;gap:18px;margin-bottom:24px}.lc-eyebrow{color:var(--blue);font-size:10px;letter-spacing:2px;font-weight:500;text-transform:uppercase;margin-bottom:7px}.lc-brand{display:flex;align-items:center;gap:15px}.lc-mark{width:49px;height:49px;background:var(--dsw-alias-bg-layer-2);color:var(--ink);border:1px solid var(--line);border-radius:14px;display:grid;place-items:center;box-shadow:none}.lc-pill{display:inline-flex;align-items:center;gap:7px;padding:5px 10px;border:1px solid var(--line);border-radius:20px;font-size:11px;color:var(--muted);white-space:nowrap}.lc-dot{height:6px;width:6px;border-radius:50%;background:currentColor}.lc-pill.good{color:var(--teal);background:color-mix(in srgb,var(--teal) 7%,var(--panel));border-color:color-mix(in srgb,var(--teal) 25%,var(--line))}.lc-pill.warm{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 7%,var(--panel));border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 25%,var(--line))}.lc-tabs{display:flex;gap:6px;padding:5px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--line);border-radius:11px;width:fit-content;margin-bottom:23px}.lc-tab{display:flex;align-items:center;gap:8px;border:0;color:var(--muted);background:transparent;padding:9px 17px;border-radius:7px;font-size:12px}.lc-tab.active{color:var(--ink);background:var(--dsw-alias-interactive-bg-hover);box-shadow:none}.lc-grid{display:grid;gap:16px}.lc-kpis{grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:20px}.lc-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px}.lc-kpi-head{display:flex;justify-content:space-between;align-items:center;font-size:12px;color:var(--muted)}.lc-value{font-size:28px;font-weight:500;letter-spacing:-1px;margin:9px 0 3px}.lc-value small{font-size:13px;font-weight:400;color:var(--muted);letter-spacing:0}.lc-sub{font-size:11px;color:var(--muted)}.lc-bar{height:5px;background:var(--dsw-alias-border-l1);border-radius:8px;overflow:hidden;margin:14px 0 6px}.lc-bar span{height:100%;display:block;background:var(--ink);border-radius:8px}.lc-section{display:flex;align-items:center;justify-content:space-between;margin:23px 0 13px}.lc-section small{color:var(--muted);font-size:11px}.lc-charts{grid-template-columns:1.6fr 1fr}.lc-chart{width:100%;max-width:760px;height:145px;display:block;margin:18px auto 0}.lc-chart-footer{display:flex;justify-content:space-between;color:var(--muted);font-size:10px}.lc-legend{display:flex;gap:16px;font-size:11px;color:var(--muted)}.lc-engine-grid{grid-template-columns:repeat(4,minmax(0,1fr))}.lc-engine{position:relative;transition:transform .15s,border-color .15s;cursor:pointer;text-align:left;color:var(--ink)}.lc-engine:hover{transform:translateY(-2px);border-color:var(--dsw-alias-border-l2)}.lc-engine.selected{border-color:var(--dsw-alias-label-primary);box-shadow:inset 0 0 0 1px var(--dsw-alias-border-l2)}.lc-engine .lc-logo{width:36px;height:36px;display:grid;place-items:center;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--line);border-radius:10px;margin-bottom:16px;font-size:16px;font-weight:500}.lc-engine h3{margin-bottom:4px}.lc-engine-state{position:absolute;top:22px;right:16px}.lc-engine-note{font-size:11px;color:var(--muted);margin-top:17px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.lc-button{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-button-floating-fill);color:var(--dsw-alias-label-primary);padding:9px 14px;border-radius:8px;font-size:12px}.lc-button.primary{background:var(--dsw-alias-button-primary-fill);border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-invert, var(--bg));font-weight:500}.lc-button.danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,var(--line));background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,var(--panel))}.lc-button.ghost{background:transparent}.lc-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:18px 0}.lc-detail-grid{grid-template-columns:1.5fr 1fr;margin-top:16px}.lc-facts{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin:20px 0}.lc-facts span{display:block;color:var(--muted);font-size:11px;margin-bottom:5px}.lc-facts strong{font-size:13px;font-weight:500}.lc-input{background:var(--dsw-alias-bg-base);border:1px solid var(--dsw-alias-border-l2);color:var(--ink);padding:10px 12px;border-radius:8px;min-width:0}.lc-input:focus{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}.lc-search{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin:18px 0}.lc-search input{flex:1;min-width:180px}.lc-model-grid{grid-template-columns:repeat(auto-fill,minmax(270px,1fr))}.lc-model-desc{font-size:12px;color:var(--muted);margin:9px 0 15px!important;min-height:36px}.lc-tags{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0}.lc-tag{padding:3px 7px;background:var(--dsw-alias-bg-layer-2);color:var(--muted);border-radius:5px;font-size:10px}.lc table{width:100%;border-collapse:collapse;font-size:12px}.lc th{text-align:left;color:var(--muted);font-weight:500;font-size:10px;letter-spacing:1px;text-transform:uppercase;padding:14px 12px;border-bottom:1px solid var(--line)}.lc td{padding:14px 12px;border-bottom:1px solid var(--line)}.lc td .lc-button{padding:5px 10px;font-size:11px}.lc-code{font:12px ui-monospace,monospace;color:var(--ink)}.lc-terminal{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--line);padding:18px;border-radius:10px;white-space:pre-wrap;overflow:auto;max-height:440px;font:11px/1.7 ui-monospace,monospace;color:var(--muted);margin-top:18px}.lc-notice{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);padding:12px 16px;border-radius:9px;margin-bottom:16px;font-size:12px}.lc-notice.error{border-color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,var(--panel));color:var(--dsw-alias-state-error-primary)}.lc-empty{padding:45px;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:12px}.lc-select-line{display:flex;gap:10px;align-items:center;margin-top:12px}.lc-select-line input,.lc-select-line select{flex:1;min-width:0}.lc-footer{margin:25px 0 5px;color:var(--muted);font-size:10px;display:flex;justify-content:space-between}.lc-field{display:block;color:var(--muted);font-size:12px;margin-top:14px}.lc-field input{width:100%;margin-top:6px}.lc svg{flex-shrink:0}.lc svg text{font:10px system-ui;fill:var(--muted)}@media(max-width:1150px){.lc{padding:24px}.lc-kpis,.lc-engine-grid{grid-template-columns:repeat(2,1fr)}}@media(max-width:700px){.lc-charts,.lc-detail-grid{grid-template-columns:1fr}.lc-header{align-items:flex-start}.lc h1{font-size:23px}.lc-tabs{flex-wrap:wrap;width:100%}.lc-tab{padding:8px}.lc-facts{grid-template-columns:1fr}.lc table{display:block;overflow:auto}}\n.lb-setup{grid-template-columns:1fr 1fr 1fr}.lb-options{display:flex;flex-direction:column;gap:14px;margin-top:18px;max-height:290px;overflow:auto}.lb-option{display:flex;gap:10px;align-items:flex-start;font-size:13px;cursor:pointer}.lb-option input{margin-top:4px;accent-color:var(--ink);flex-shrink:0}.lb-option small{display:block;color:var(--muted);font-size:11px;margin-top:4px}.lb-settings{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}.lb-settings select{display:block;width:100%;margin-top:6px}.lb-settings .lb-option{grid-column:1/-1;margin-top:16px}.lb-tasks{grid-template-columns:repeat(3,1fr);margin-top:20px;gap:20px}.lb-tasks details{margin-top:8px;margin-left:23px}.lb-summary{grid-template-columns:repeat(auto-fit,minmax(230px,1fr));margin-top:16px}.lb-table{overflow:auto}.lb-table td{vertical-align:top;min-width:90px}.lb-table td:first-child{min-width:210px}.lb-table td:nth-child(6){max-width:300px}.lb-previews{grid-template-columns:repeat(auto-fit,minmax(400px,1fr))}.lb-preview{margin-top:16px;overflow:auto;border:1px solid var(--line);border-radius:9px;background:var(--bg);max-height:650px}.lb-preview img{display:block;width:100%;height:auto}.lb-preview iframe{display:block;width:100%;height:600px;border:0;background:var(--bg)}.lb-preview.mobile{max-width:390px;margin-inline:auto}.lb .lc-field>select{margin-left:10px}.lb-settings .lc-field>select{margin-left:0}@media(max-width:1100px){.lb-setup{grid-template-columns:1fr 1fr}.lb-setup>section:last-child{grid-column:1/-1}}@media(max-width:700px){.lb-setup,.lb-tasks,.lb-previews{grid-template-columns:1fr}.lb-setup>section:last-child{grid-column:auto}.lb-preview iframe{height:500px}}\n.lb-history-actions{gap:12px;flex-wrap:wrap}.lb-comparison{margin-bottom:18px}.lb-compare-picker{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;max-height:220px;overflow:auto;margin:18px 0}.lb-comparison-table table{min-width:760px}.lb-comparison-table th:not(:first-child),.lb-comparison-table td{min-width:230px;max-width:340px}.lb-comparison-table th small{display:block;margin-top:5px;letter-spacing:0;text-transform:none}.lb-comparison-table tbody th{min-width:210px;text-transform:none;letter-spacing:0;font-size:12px}.lb-comparison-table caption{text-align:left;padding:10px 0}.lb-comparison-table thead th{position:sticky;top:0;background:var(--panel)}\n\n.lb-config{grid-template-columns:repeat(3,minmax(0,1fr))}.lb-config .lc-field{min-width:0}.lc-field select{max-width:100%}.lc-engine-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.lc .lb-tasks>div{min-width:0;overflow-wrap:anywhere}.lc button:focus-visible,.lc input:focus-visible,.lc select:focus-visible{outline:2px solid var(--ink);outline-offset:3px}@media(max-width:1100px){.lb-config{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.lb-config,.lc-engine-grid,.lb-settings{grid-template-columns:1fr}.lc{padding:16px}.lc-footer{flex-wrap:wrap;gap:8px}}\n\n.lc h1,.lc h2,.lc h3,.lc strong{font-weight:500}.lb-config .lc-sub{grid-column:1/-1;margin-top:10px}\n.ld-heading{display:flex;justify-content:space-between;align-items:center;gap:24px;margin:8px 0 24px}.ld-heading h2{font-size:26px;margin:8px 0}.ld-hardware{display:flex;gap:20px;flex-wrap:wrap;padding:14px 18px;background:var(--panel);border:1px solid var(--line);border-radius:10px;font-size:12px;color:var(--muted)}.ld-hardware strong{color:var(--ink)}.ld-search{display:grid;grid-template-columns:210px minmax(200px,1fr) auto;gap:10px;padding:14px;background:var(--panel);border:1px solid var(--line);border-radius:12px}.ld-toolbar{display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;margin:16px 0 22px}.ld-suggestions,.ld-filters{display:flex;gap:8px;flex-wrap:wrap}.ld-workspace{display:grid;grid-template-columns:minmax(0,1fr);gap:22px;align-items:start}.ld-workspace.has-selection{grid-template-columns:minmax(0,1fr) minmax(320px,390px)}.ld-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:14px}.ld-model{display:flex;flex-direction:column;gap:10px;padding:20px;min-height:220px;transition:border-color .15s}.ld-model.is-selected{border-color:var(--ink);background:var(--dsw-alias-bg-layer-2)}.ld-model-top{display:flex;gap:6px}.ld-model-title{font-size:15px;font-weight:500;color:var(--ink);overflow-wrap:anywhere;text-decoration:none;line-height:1.45}.ld-model-title:hover,.ld-detail a:hover{text-decoration:underline}.ld-publisher{margin-top:-6px!important}.ld-description{font-size:12px;color:var(--muted);line-height:1.5;overflow-wrap:anywhere}.ld-model-meta{display:flex;gap:12px;flex-wrap:wrap;font-size:11px;color:var(--muted);margin-top:auto}.ld-model>.lc-button{align-self:flex-start;margin-top:4px}.ld-results-label{font-size:12px;color:var(--muted);min-height:22px;margin-bottom:8px}.ld-detail{position:sticky;top:16px;padding:22px;max-height:calc(100vh - 50px);overflow:auto}.ld-detail-header{display:flex;justify-content:space-between;align-items:center;gap:8px}.ld-detail h3{margin:16px 0;overflow-wrap:anywhere;line-height:1.5}.ld-detail a{color:var(--ink)}.ld-budget{display:grid;grid-template-columns:1fr 1fr;gap:12px}.ld-budget small,.ld-version small{display:block;color:var(--muted);font-size:11px;margin-top:4px}.ld-detail select{display:block;width:100%;margin-top:6px}.ld-versions{border:0;padding:0;margin:20px 0}.ld-versions legend{font-size:12px;color:var(--muted);margin-bottom:10px}.ld-version{display:flex;align-items:center;gap:10px;border:1px solid var(--line);border-radius:9px;padding:12px;margin-top:8px;cursor:pointer;font-size:13px}.ld-version.is-selected{border-color:var(--ink);background:var(--dsw-alias-bg-layer-2)}.ld-version>span{flex:1;min-width:0;overflow-wrap:anywhere}.ld-version input{accent-color:var(--ink)}.ld-total{display:flex;justify-content:space-between;gap:12px;padding-top:18px;margin-top:18px;border-top:1px solid var(--line);font-size:14px}.ld-fit{font-size:12px;margin:10px 0;color:var(--ink)}.ld-detail>.lc-button,.ld-detail .lc-button.primary{width:100%;margin:16px 0 12px}.ld-file-list{margin:16px 0;font-size:12px;color:var(--muted)}.ld-file-list summary{cursor:pointer}.ld-file-list div{display:flex;justify-content:space-between;gap:10px;padding:8px 0;border-bottom:1px solid var(--line)}.ld-file-list div span:first-child{overflow-wrap:anywhere;min-width:0}.ld-file-list div span:last-child{white-space:nowrap}.ld-skeleton{height:220px;display:flex;flex-direction:column;gap:20px}.ld-skeleton span{height:16px;background:var(--dsw-alias-bg-layer-2);border-radius:4px}.ld-skeleton span:nth-child(2){width:65%}.ld-skeleton span:last-child{width:40%;margin-top:auto}.ld a:focus-visible{outline:2px solid var(--ink);outline-offset:3px}@media(max-width:1000px){.ld-workspace.has-selection{grid-template-columns:1fr}.ld-detail{position:static;max-height:none;grid-row:1}.ld-heading{align-items:flex-start;flex-direction:column}.ld-search{grid-template-columns:180px 1fr}.ld-search .lc-button{grid-column:1/-1}}@media(max-width:600px){.ld-search{grid-template-columns:1fr}.ld-cards{grid-template-columns:1fr}.ld-budget{grid-template-columns:1fr}.ld-filters{width:100%}.ld-filters select{flex:1;min-width:0}.ld-heading h2{font-size:23px}}\n\n.lc-tune-control{min-width:0;display:grid;gap:8px;color:var(--dsw-alias-label-primary);font-size:13px}.lc-tune-heading{display:flex;align-items:center;justify-content:space-between;gap:12px}.lc-tune-heading output{font-weight:500;font-variant-numeric:tabular-nums}.lc-tune-row{display:flex;align-items:center;gap:12px;min-width:0}.lc-tune-range{flex:1;min-width:0;height:24px;accent-color:var(--dsw-alias-label-primary);cursor:pointer}.lc-tune-exact{width:100px;flex:0 0 100px;min-width:0;text-align:right}.lc-tune-scale{display:flex;justify-content:space-between;padding-right:112px;font-size:11px;color:var(--dsw-alias-label-secondary)}.lc-tune-choices{display:flex;gap:6px;flex-wrap:wrap}.lc-tune-choices .lc-button{padding:5px 9px;font-size:12px}.lc-tune-range:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px;border-radius:6px}.lc-tune-control .lc-sub{margin:0}.lc-tune-range:disabled{cursor:default}\n.lc-tune-control{align-self:start;align-content:start}\n.lb-config .lb-settings{grid-template-columns:1fr}\n\n.lc{padding:24px;max-width:none;margin:0;width:100%;align-self:stretch;min-width:0}.lc-card{background:var(--dsw-alias-bg-base);border-radius:12px}.lc h1{font-size:26px;letter-spacing:-.6px}.lc h2,.lc h3{font-weight:500}.lc-sub{font-size:12px;line-height:1.6}.lc-tabs{max-width:100%;flex-wrap:wrap;background:transparent;padding:0;border:0;gap:4px}.lc-tab{padding:10px 14px;font-size:13px}.lc-tab.active{background:var(--dsw-alias-bg-layer-2)}.lc-header{margin:24px 0 20px}.lc-model-grid{grid-template-columns:repeat(auto-fit,minmax(min(100%,280px),1fr));gap:16px}.lc-model-card{display:flex;flex-direction:column;align-items:flex-start;gap:12px;min-width:0}.lc-model-card h3,.lc-model-card p{overflow-wrap:anywhere}.lc-model-card .lc-actions{margin-top:auto;flex-wrap:wrap}.lc-health-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:20px}.lc-health-meter{margin:18px 0}.lc-health-meter>div{display:flex;justify-content:space-between;gap:12px;font-size:13px}.lc-health-meter strong{font-weight:500}.lc-health-meter progress{display:block;width:100%;height:7px;margin:9px 0;accent-color:var(--dsw-alias-label-primary)}.lc-health-meter small{font-size:12px;color:var(--dsw-alias-label-secondary)}.lc-core-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:8px}.lc-core-grid>div{display:flex;align-items:center;gap:6px;font-size:10px;color:var(--dsw-alias-label-secondary)}.lc-core-grid progress{width:100%;height:5px;accent-color:var(--dsw-alias-state-success-primary)}.lc-network-row{display:grid;gap:4px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px}.lc-network-row strong{font-weight:500}.lc-connection{border-bottom:1px solid var(--dsw-alias-border-l1);padding-bottom:18px}.lc-connection-bar{display:flex;justify-content:space-between;align-items:center;gap:16px;flex-wrap:wrap}.lc-connection-bar strong{display:block;overflow-wrap:anywhere;font-size:13px;font-weight:500}.lc-connection-fields{grid-template-columns:repeat(auto-fit,minmax(min(100%,240px),1fr));gap:16px;margin:18px 0}.lc-connection .lc-actions{flex-wrap:wrap;margin:0}.lc-connection-setup{margin-top:18px}.lc-connection-setup h3{margin-top:20px}.lc-connection input{width:100%;min-width:0}.lc-steps{padding-left:20px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:1.9}.lc-webui-link{display:inline-flex;text-decoration:none;margin:12px 0}.lc-engine-setup{margin-bottom:24px}.lc-engine-setup .lc-card p{margin:12px 0}.lc-engine-setup summary{cursor:pointer}.lc .lc-facts>div{min-width:0}.lc .lc-facts strong{overflow-wrap:anywhere}.lc-empty h3{margin-bottom:8px}.lc-empty .lc-button{margin-top:16px}.lc button:focus-visible,.lc a:focus-visible,.lc input:focus-visible{outline:2px solid var(--dsw-alias-label-primary);outline-offset:3px}\n@media(max-width:800px){.lc-health-grid{grid-template-columns:1fr}.lc{padding:16px}.lc-header{align-items:flex-start}.lc-connection-bar{align-items:flex-start}.lc-tabs{gap:3px}.lc-tab{padding:9px 11px}.lc-section{gap:12px;flex-wrap:wrap}.lc-header h1{font-size:23px}.lc-header .lc-mark{display:none}.lc-connection-bar .lc-actions{width:100%}.lc-network-row{overflow-wrap:anywhere}}\n.lc-ninfer-hero{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px;align-items:start}\n.lc-ninfer-actions{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}\n.lc-ninfer-kpis{grid-template-columns:repeat(4,minmax(0,1fr))}\n.lc-pill.bad{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,var(--panel));border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,var(--line))}\n@media(max-width:1150px){.lc-ninfer-kpis{grid-template-columns:repeat(2,minmax(0,1fr))}}\n@media(max-width:700px){.lc-ninfer-hero{grid-template-columns:1fr}.lc-ninfer-actions{justify-content:flex-start}}\n@media(max-width:520px){.lc-ninfer-kpis{grid-template-columns:1fr}}\n\n/* NInfer: connection details, client examples and engine facts. */\n.lc-ninfer-split{display:grid;grid-template-columns:minmax(0,1.45fr) minmax(0,1fr);gap:16px;margin-top:16px;align-items:start}.lc-ninfer-split>*{min-width:0}\n.lc-api-rows{display:grid;gap:9px}\n.lc-api-row{display:grid;gap:6px;padding:11px 13px;border:1px solid var(--line);border-radius:10px;background:var(--dsw-alias-bg-layer-1);min-width:0}\n.lc-api-row>header{display:flex;align-items:center;justify-content:space-between;gap:10px}\n.lc-api-row>header>span{font-size:10px;letter-spacing:1.6px;text-transform:uppercase;color:var(--muted)}\n.lc-api-row code{font:12.5px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink);overflow-wrap:anywhere}\n.lc-api-note{margin-top:10px}\n.lc-copy{border:1px solid var(--line);background:var(--dsw-alias-bg-base);color:var(--muted);border-radius:7px;padding:4px 10px;font-size:11px;line-height:1.4;white-space:nowrap;min-width:58px}\n.lc-copy:hover{color:var(--ink);border-color:var(--ink)}\n.lc-copy.copied{color:var(--teal);border-color:color-mix(in srgb,var(--teal) 45%,var(--line))}\n.lc-example{margin-top:14px;border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-1);min-width:0}\n.lc-example>header{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 12px;border-bottom:1px solid var(--line);flex-wrap:wrap}\n.lc-example-tabs{display:flex;gap:2px;padding:3px;background:var(--dsw-alias-bg-layer-2);border-radius:9px}\n.lc-example-tab{border:0;background:transparent;color:var(--muted);font-size:11px;padding:5px 11px;border-radius:7px;line-height:1.4}\n.lc-example-tab.active{background:var(--dsw-alias-bg-layer-2);color:var(--ink)}\n.lc-example pre{margin:0;padding:13px 14px;max-height:230px;overflow:auto;font:11.5px/1.65 ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink);white-space:pre}\n.lc-spec-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:9px}\n.lc-spec{padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--dsw-alias-bg-layer-1);min-width:0}\n.lc-spec>span{display:block;font-size:10px;letter-spacing:1.6px;text-transform:uppercase;color:var(--muted)}\n.lc-spec>strong{display:block;margin-top:4px;font-size:13px;font-weight:500;overflow-wrap:anywhere}\n.lc-ninfer-kpis{gap:12px}\n.lc-ninfer-kpis .lc-card{padding:14px 15px}\n.lc-ninfer-kpis .lc-value{font-size:21px;margin:6px 0 2px}\n@media(max-width:1180px){.lc-ninfer-split{grid-template-columns:minmax(0,1fr)}}\n@media(max-width:700px){.lc-spec-grid{grid-template-columns:1fr}.lc-example pre{max-height:180px}}";
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/client/Dashboard.tsx
|
|
528
|
+
function Icon({ kind = "chip", size = 18 }) {
|
|
529
|
+
const paths = {
|
|
530
|
+
chip: "M8 3v3m8-3v3M8 18v3m8-3v3M3 8h3m-3 8h3m12-8h3m-3 8h3M6 6h12v12H6zM9 9h6v6H9z",
|
|
531
|
+
chart: "M3 3v18h18M6 16l4-6 4 3 6-9",
|
|
532
|
+
box: "M3 7l9-4 9 4-9 4zM3 7v10l9 4 9-4V7M12 11v10",
|
|
533
|
+
download: "M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5",
|
|
534
|
+
server: "M3 3h18v7H3zM3 14h18v7H3zM6 6h2m-2 11h2",
|
|
535
|
+
logs: "M5 4h14v16H5zM8 8h8M8 12h8M8 16h5",
|
|
536
|
+
play: "M7 4l13 8-13 8z"
|
|
537
|
+
};
|
|
538
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
539
|
+
width: size,
|
|
540
|
+
height: size,
|
|
541
|
+
viewBox: "0 0 24 24",
|
|
542
|
+
fill: "none",
|
|
543
|
+
stroke: "currentColor",
|
|
544
|
+
strokeWidth: "1.5",
|
|
545
|
+
strokeLinecap: "round",
|
|
546
|
+
strokeLinejoin: "round",
|
|
547
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: paths[kind] ?? paths.chip })
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
const SidebarIcon = Icon;
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/ninfer-log.ts
|
|
553
|
+
const number = (value) => {
|
|
554
|
+
if (value === void 0) return void 0;
|
|
555
|
+
const parsed = Number(value.replace(/,/gu, ""));
|
|
556
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
557
|
+
};
|
|
558
|
+
const rate$1 = (value, suffix) => {
|
|
559
|
+
const parsed = number(value);
|
|
560
|
+
if (parsed === void 0) return void 0;
|
|
561
|
+
return suffix === "k" ? parsed * 1e3 : parsed;
|
|
562
|
+
};
|
|
563
|
+
/**
|
|
564
|
+
* Summarize the part of the log that belongs to the latest start.
|
|
565
|
+
*
|
|
566
|
+
* The log is append-only across restarts, so a new `starting engine` line ends
|
|
567
|
+
* the previous run's figures. A later `listening on` clears an earlier failure;
|
|
568
|
+
* a later `server stopped` clears the throughput that only describes a live run.
|
|
569
|
+
*/
|
|
570
|
+
function parseNInferLog(text) {
|
|
571
|
+
const lines = text.split(/\r?\n/u);
|
|
572
|
+
let start = -1;
|
|
573
|
+
for (let index = 0; index < lines.length; index++) if (lines[index].includes("starting engine")) start = index;
|
|
574
|
+
const run = start >= 0 ? lines.slice(start) : lines;
|
|
575
|
+
const out = {};
|
|
576
|
+
for (const line of run) {
|
|
577
|
+
const request = line.match(/req#(\d+)/u);
|
|
578
|
+
if (request) {
|
|
579
|
+
const seen = number(request[1]);
|
|
580
|
+
if (seen !== void 0) out.requests = Math.max(out.requests ?? 0, seen);
|
|
581
|
+
}
|
|
582
|
+
let match;
|
|
583
|
+
if (match = line.match(/loading weights\s+([\d.]+)%\s*\|\s*([\d.]+)\s*GiB\/([\d.]+)\s*GiB\s*\|\s*([\d.]+)\s*MiB\/s\s*\|\s*ETA\s*([\d.]+)s/u)) {
|
|
584
|
+
out.weightPercent = number(match[1]);
|
|
585
|
+
out.weightGiB = number(match[2]);
|
|
586
|
+
out.weightTotalGiB = number(match[3]);
|
|
587
|
+
out.weightBytesPerSecond = (number(match[4]) ?? 0) * 1024 * 1024;
|
|
588
|
+
out.etaSeconds = number(match[5]);
|
|
589
|
+
} else if (/weights ready/u.test(line)) {
|
|
590
|
+
out.weightPercent = 100;
|
|
591
|
+
out.ready = true;
|
|
592
|
+
out.etaSeconds = 0;
|
|
593
|
+
} else if (/engine ready/u.test(line)) out.ready = true;
|
|
594
|
+
else if (/listening on http/u.test(line)) {
|
|
595
|
+
out.listening = true;
|
|
596
|
+
out.error = void 0;
|
|
597
|
+
} else if (match = line.match(/throughput\s*\|\s*[\d.]+s\s*\|\s*prefill\s+([\d.]+)(k?)\s*tok\/s.*?decode\s+([\d.]+)(k?)\s*tok\/s.*?running\s+(\d+)/u)) {
|
|
598
|
+
out.prefillTokPerSecond = rate$1(match[1], match[2]);
|
|
599
|
+
out.decodeTokPerSecond = rate$1(match[3], match[4]);
|
|
600
|
+
out.running = number(match[5]);
|
|
601
|
+
} else if (match = line.match(/mtp accepted\s+([\d,]+)\/([\d,]+)\s*\(([\d.]+)%\)/u)) {
|
|
602
|
+
out.mtpAccepted = number(match[1]);
|
|
603
|
+
out.mtpOffered = number(match[2]);
|
|
604
|
+
out.mtpPercent = number(match[3]);
|
|
605
|
+
} else if (/(FATAL|ERROR)\s+.*(startup failed|server failed)/u.test(line)) {
|
|
606
|
+
out.error = line.replace(/^.*?(?:FATAL|ERROR)\s+/u, "").trim();
|
|
607
|
+
out.listening = false;
|
|
608
|
+
out.ready = false;
|
|
609
|
+
} else if (/server stopped/u.test(line)) {
|
|
610
|
+
out.running = 0;
|
|
611
|
+
out.decodeTokPerSecond = void 0;
|
|
612
|
+
out.prefillTokPerSecond = void 0;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (out.listening) out.error = void 0;
|
|
616
|
+
return out;
|
|
617
|
+
}
|
|
618
|
+
//#endregion
|
|
619
|
+
//#region src/client/use-ninfer-live.ts
|
|
620
|
+
/**
|
|
621
|
+
* Read the engine's own log and the host telemetry while this view is open.
|
|
622
|
+
*
|
|
623
|
+
* The log is the only place NInfer reports load progress, throughput and the
|
|
624
|
+
* reason a start failed, so the dashboard reads it on the same cadence as the
|
|
625
|
+
* hardware snapshot and keeps a short history for the chart.
|
|
626
|
+
*/
|
|
627
|
+
function useNInferLive({ enabled }) {
|
|
628
|
+
const [telemetry, setTelemetry] = (0, react.useState)();
|
|
629
|
+
const [summary, setSummary] = (0, react.useState)({});
|
|
630
|
+
const [text, setText] = (0, react.useState)("");
|
|
631
|
+
const [history, setHistory] = (0, react.useState)([]);
|
|
632
|
+
const [refreshKey, setRefreshKey] = (0, react.useState)(0);
|
|
633
|
+
(0, react.useEffect)(() => {
|
|
634
|
+
if (!enabled) return;
|
|
635
|
+
let alive = true;
|
|
636
|
+
async function tick() {
|
|
637
|
+
try {
|
|
638
|
+
const response = await fetch("/api/llamacpp/telemetry", { signal: AbortSignal.timeout(8e3) });
|
|
639
|
+
if (response.ok) {
|
|
640
|
+
const value = await response.json();
|
|
641
|
+
if (alive) setTelemetry(value);
|
|
642
|
+
}
|
|
643
|
+
} catch {}
|
|
644
|
+
try {
|
|
645
|
+
const response = await fetch("/api/ninfer/logs", { signal: AbortSignal.timeout(15e3) });
|
|
646
|
+
if (response.ok) {
|
|
647
|
+
const value = await response.json();
|
|
648
|
+
if (alive && typeof value.text === "string") {
|
|
649
|
+
setText(value.text);
|
|
650
|
+
setSummary(parseNInferLog(value.text));
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
} catch {}
|
|
654
|
+
}
|
|
655
|
+
tick();
|
|
656
|
+
const timer = setInterval(() => {
|
|
657
|
+
if (document.visibilityState === "visible") tick();
|
|
658
|
+
}, 3e3);
|
|
659
|
+
return () => {
|
|
660
|
+
alive = false;
|
|
661
|
+
clearInterval(timer);
|
|
662
|
+
};
|
|
663
|
+
}, [enabled, refreshKey]);
|
|
664
|
+
(0, react.useEffect)(() => {
|
|
665
|
+
if (!enabled) return;
|
|
666
|
+
setHistory((current) => [...current, {
|
|
667
|
+
decode: summary.decodeTokPerSecond ?? null,
|
|
668
|
+
prefill: summary.prefillTokPerSecond ?? null,
|
|
669
|
+
vram: telemetry?.gpus?.[0]?.usedMiB ?? null
|
|
670
|
+
}].slice(-60));
|
|
671
|
+
}, [
|
|
672
|
+
enabled,
|
|
673
|
+
summary.decodeTokPerSecond,
|
|
674
|
+
summary.prefillTokPerSecond,
|
|
675
|
+
telemetry
|
|
676
|
+
]);
|
|
677
|
+
return {
|
|
678
|
+
telemetry,
|
|
679
|
+
summary,
|
|
680
|
+
text,
|
|
681
|
+
history,
|
|
682
|
+
refresh: () => setRefreshKey((value) => value + 1)
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/client/NInferApi.tsx
|
|
687
|
+
/** Copy with the async clipboard, falling back to the selection API in a page that denies it. */
|
|
688
|
+
async function copyText(value) {
|
|
689
|
+
try {
|
|
690
|
+
if (navigator.clipboard?.writeText) {
|
|
691
|
+
await navigator.clipboard.writeText(value);
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
} catch (error) {}
|
|
695
|
+
try {
|
|
696
|
+
const area = document.createElement("textarea");
|
|
697
|
+
area.value = value;
|
|
698
|
+
area.readOnly = true;
|
|
699
|
+
area.style.position = "fixed";
|
|
700
|
+
area.style.top = "-1000px";
|
|
701
|
+
area.style.opacity = "0";
|
|
702
|
+
document.body.appendChild(area);
|
|
703
|
+
area.select();
|
|
704
|
+
const copied = document.execCommand("copy");
|
|
705
|
+
area.remove();
|
|
706
|
+
return copied;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** One copy button that says so for a moment, so a copy is never a guess. */
|
|
712
|
+
function Copy({ value, what, label = "" }) {
|
|
713
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
714
|
+
(0, react.useEffect)(() => {
|
|
715
|
+
if (!copied) return;
|
|
716
|
+
const timer = setTimeout(() => setCopied(false), 1600);
|
|
717
|
+
return () => clearTimeout(timer);
|
|
718
|
+
}, [copied]);
|
|
719
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
720
|
+
type: "button",
|
|
721
|
+
className: `lc-copy${copied ? " copied" : ""}`,
|
|
722
|
+
"aria-label": `Copy ${what}`,
|
|
723
|
+
onClick: () => void copyText(value).then((ok) => {
|
|
724
|
+
if (ok) setCopied(true);
|
|
725
|
+
}),
|
|
726
|
+
children: copied ? "Copied" : label || "Copy"
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
/** A labelled address with its own copy button. */
|
|
730
|
+
function Address({ label, value, what, copy = true }) {
|
|
731
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
732
|
+
className: "lc-api-row",
|
|
733
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), copy && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, {
|
|
734
|
+
value,
|
|
735
|
+
what
|
|
736
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: value })]
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/** One labelled engine figure. */
|
|
740
|
+
function Spec({ label, value }) {
|
|
741
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
742
|
+
className: "lc-spec",
|
|
743
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: value })]
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
const examples = [
|
|
747
|
+
"curl",
|
|
748
|
+
"python",
|
|
749
|
+
"node"
|
|
750
|
+
];
|
|
751
|
+
/**
|
|
752
|
+
* What a client needs to talk to this engine: the OpenAI-compatible address,
|
|
753
|
+
* the model name, the auth the engine actually asks for, and a runnable example.
|
|
754
|
+
*/
|
|
755
|
+
function NInferApi({ state, running }) {
|
|
756
|
+
const [example, setExample] = (0, react.useState)("curl");
|
|
757
|
+
const base = (state?.baseUrl ?? "http://127.0.0.1:8083/v1").replace(/\/v1\/?$/u, "");
|
|
758
|
+
const model = state?.modelName ?? "qwen3.8-27b";
|
|
759
|
+
const env = `OPENAI_BASE_URL=${base}/v1\nOPENAI_API_KEY=local\nOPENAI_MODEL=${model}`;
|
|
760
|
+
const code = {
|
|
761
|
+
curl: `curl ${base}/v1/chat/completions \\\n -H 'content-type: application/json' \\\n -d '${JSON.stringify({
|
|
762
|
+
model,
|
|
763
|
+
messages: [{
|
|
764
|
+
role: "user",
|
|
765
|
+
content: "Hello"
|
|
766
|
+
}],
|
|
767
|
+
max_tokens: 128
|
|
768
|
+
})}'`,
|
|
769
|
+
python: `from openai import OpenAI\n\nclient = OpenAI(base_url="${base}/v1", api_key="local")\nreply = client.chat.completions.create(\n model="${model}",\n messages=[{"role": "user", "content": "Hello"}],\n)\nprint(reply.choices[0].message.content)`,
|
|
770
|
+
node: `import OpenAI from 'openai'\n\nconst client = new OpenAI({ baseURL: '${base}/v1', apiKey: 'local' })\nconst reply = await client.chat.completions.create({\n model: '${model}',\n messages: [{ role: 'user', content: 'Hello' }],\n})\nconsole.log(reply.choices[0].message.content)`
|
|
771
|
+
};
|
|
772
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
773
|
+
className: "lc-card",
|
|
774
|
+
"aria-label": "API and clients",
|
|
775
|
+
children: [
|
|
776
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
777
|
+
className: "lc-section",
|
|
778
|
+
style: { marginTop: 0 },
|
|
779
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Connect your tools" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
780
|
+
className: "lc-sub",
|
|
781
|
+
children: "Any OpenAI-compatible client works with these values. The engine answers on this computer only."
|
|
782
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
783
|
+
className: "lc-actions",
|
|
784
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, {
|
|
785
|
+
label: "Copy .env",
|
|
786
|
+
value: env,
|
|
787
|
+
what: "the .env settings"
|
|
788
|
+
})
|
|
789
|
+
})]
|
|
790
|
+
}),
|
|
791
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
792
|
+
className: "lc-api-rows",
|
|
793
|
+
children: [
|
|
794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Address, {
|
|
795
|
+
label: "Base URL",
|
|
796
|
+
value: `${base}/v1`,
|
|
797
|
+
what: "the base URL"
|
|
798
|
+
}),
|
|
799
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Address, {
|
|
800
|
+
label: "Chat completions",
|
|
801
|
+
value: `${base}/v1/chat/completions`,
|
|
802
|
+
what: "the chat completions URL"
|
|
803
|
+
}),
|
|
804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Address, {
|
|
805
|
+
label: "Model name",
|
|
806
|
+
value: model,
|
|
807
|
+
what: "the model name"
|
|
808
|
+
}),
|
|
809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Address, {
|
|
810
|
+
label: "Models endpoint",
|
|
811
|
+
value: `${base}/v1/models`,
|
|
812
|
+
what: "the models URL"
|
|
813
|
+
}),
|
|
814
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Address, {
|
|
815
|
+
label: "API key",
|
|
816
|
+
value: "Not required on this computer",
|
|
817
|
+
what: "the API key",
|
|
818
|
+
copy: false
|
|
819
|
+
})
|
|
820
|
+
]
|
|
821
|
+
}),
|
|
822
|
+
!running && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
823
|
+
className: "lc-sub lc-api-note",
|
|
824
|
+
children: "NInfer is not serving right now, so these addresses answer after Start NInfer."
|
|
825
|
+
}),
|
|
826
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
827
|
+
className: "lc-example",
|
|
828
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
829
|
+
className: "lc-example-tabs",
|
|
830
|
+
role: "tablist",
|
|
831
|
+
"aria-label": "Client example",
|
|
832
|
+
children: examples.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
833
|
+
type: "button",
|
|
834
|
+
role: "tab",
|
|
835
|
+
"aria-selected": example === name,
|
|
836
|
+
className: `lc-example-tab${example === name ? " active" : ""}`,
|
|
837
|
+
onClick: () => setExample(name),
|
|
838
|
+
children: name === "curl" ? "curl" : name === "python" ? "Python" : "Node.js"
|
|
839
|
+
}, name))
|
|
840
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Copy, {
|
|
841
|
+
value: code[example],
|
|
842
|
+
what: "the client example"
|
|
843
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", { children: code[example] })]
|
|
844
|
+
})
|
|
845
|
+
]
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/** The pinned engine this installation serves, as the host reports it. */
|
|
849
|
+
function NInferSpec({ state }) {
|
|
850
|
+
const runtime = state?.runtime === "wsl" ? `WSL · pid ${state.pid ?? "—"}` : state?.containerId ? `Container · ${state.containerId.slice(0, 12)}` : state?.pid ? `Process · pid ${state.pid}` : "Not running";
|
|
851
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
852
|
+
className: "lc-card",
|
|
853
|
+
"aria-label": "Engine configuration",
|
|
854
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
855
|
+
className: "lc-section",
|
|
856
|
+
style: { marginTop: 0 },
|
|
857
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Engine" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
858
|
+
className: "lc-sub",
|
|
859
|
+
children: "The pinned artifact this page starts and serves."
|
|
860
|
+
})] }), state?.complete === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
861
|
+
className: "lc-pill good",
|
|
862
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "lc-dot" }), "verified"]
|
|
863
|
+
})]
|
|
864
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
865
|
+
className: "lc-spec-grid",
|
|
866
|
+
children: [
|
|
867
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
868
|
+
label: "Model",
|
|
869
|
+
value: state?.modelName ?? "qwen3.8-27b"
|
|
870
|
+
}),
|
|
871
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
872
|
+
label: "Runtime",
|
|
873
|
+
value: runtime
|
|
874
|
+
}),
|
|
875
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
876
|
+
label: "Context",
|
|
877
|
+
value: state?.context == null ? "—" : state.context.toLocaleString() + " tokens"
|
|
878
|
+
}),
|
|
879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
880
|
+
label: "Response limit",
|
|
881
|
+
value: state?.outputTokens == null ? "—" : state.outputTokens.toLocaleString() + " tokens"
|
|
882
|
+
}),
|
|
883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
884
|
+
label: "Prefill chunk",
|
|
885
|
+
value: state?.prefill == null ? "—" : state.prefill.toLocaleString()
|
|
886
|
+
}),
|
|
887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
888
|
+
label: "Host KV",
|
|
889
|
+
value: state?.hostKvMiB == null ? "—" : state.hostKvMiB.toLocaleString() + " MiB"
|
|
890
|
+
}),
|
|
891
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
892
|
+
label: "MTP drafts",
|
|
893
|
+
value: state?.draftTokens == null ? "—" : String(state.draftTokens)
|
|
894
|
+
}),
|
|
895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
896
|
+
label: "Vision",
|
|
897
|
+
value: state?.vision ? "Enabled" : "Text only"
|
|
898
|
+
}),
|
|
899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
900
|
+
label: "n-gram drafts",
|
|
901
|
+
value: state?.ngram ?? "chain"
|
|
902
|
+
}),
|
|
903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Spec, {
|
|
904
|
+
label: "Access",
|
|
905
|
+
value: "127.0.0.1 only"
|
|
906
|
+
})
|
|
907
|
+
]
|
|
908
|
+
})]
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
//#endregion
|
|
912
|
+
//#region src/client/NInferPanel.tsx
|
|
913
|
+
const gb = (mib) => mib == null ? "—" : (mib / 1024).toFixed(1);
|
|
914
|
+
const rate = (value) => value == null ? "—" : value >= 1e3 ? (value / 1e3).toFixed(2) + "k" : value.toFixed(1);
|
|
915
|
+
/** One KPI card in the engine's own monitoring vocabulary. */
|
|
916
|
+
function Kpi({ label, value, detail, children }) {
|
|
917
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
918
|
+
className: "lc-card",
|
|
919
|
+
children: [
|
|
920
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
921
|
+
className: "lc-kpi-head",
|
|
922
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })
|
|
923
|
+
}),
|
|
924
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
925
|
+
className: "lc-value",
|
|
926
|
+
children: value
|
|
927
|
+
}),
|
|
928
|
+
children,
|
|
929
|
+
detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
930
|
+
className: "lc-sub",
|
|
931
|
+
children: detail
|
|
932
|
+
})
|
|
933
|
+
]
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
/** Decode throughput from the samples this view has seen since it opened. */
|
|
937
|
+
function Sparkline({ samples }) {
|
|
938
|
+
const values = samples.map((sample) => sample.decode).filter((value) => typeof value === "number" && value > 0);
|
|
939
|
+
if (values.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
940
|
+
className: "lc-empty",
|
|
941
|
+
style: { padding: "26px 0" },
|
|
942
|
+
children: "Throughput appears after the first request."
|
|
943
|
+
});
|
|
944
|
+
const max = Math.max(...values) * 1.15 || 1;
|
|
945
|
+
const points = values.map((value, index) => `${40 + index * 520 / Math.max(1, values.length - 1)},${130 - Math.min(1, value / max) * 110}`).join(" ");
|
|
946
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
947
|
+
className: "lc-chart",
|
|
948
|
+
viewBox: "0 0 580 150",
|
|
949
|
+
role: "img",
|
|
950
|
+
"aria-label": `Decode throughput across ${values.length} samples`,
|
|
951
|
+
children: [
|
|
952
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("linearGradient", {
|
|
953
|
+
id: "ninfer-throughput",
|
|
954
|
+
x1: "0",
|
|
955
|
+
y1: "0",
|
|
956
|
+
x2: "0",
|
|
957
|
+
y2: "1",
|
|
958
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
959
|
+
stopColor: "var(--dsw-alias-state-success-primary)",
|
|
960
|
+
stopOpacity: ".18"
|
|
961
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("stop", {
|
|
962
|
+
offset: "1",
|
|
963
|
+
stopColor: "var(--dsw-alias-state-success-primary)",
|
|
964
|
+
stopOpacity: "0"
|
|
965
|
+
})]
|
|
966
|
+
}) }),
|
|
967
|
+
[
|
|
968
|
+
0,
|
|
969
|
+
1,
|
|
970
|
+
2,
|
|
971
|
+
3
|
|
972
|
+
].map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
973
|
+
d: `M40 ${20 + row * 110 / 3}H560`,
|
|
974
|
+
stroke: "var(--dsw-alias-border-l1)",
|
|
975
|
+
strokeDasharray: "3 4"
|
|
976
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
977
|
+
x: "3",
|
|
978
|
+
y: 24 + row * 110 / 3,
|
|
979
|
+
children: Math.round(max * (1 - row / 3))
|
|
980
|
+
})] }, row)),
|
|
981
|
+
values.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polygon", {
|
|
982
|
+
points: `40,130 ${points} 560,130`,
|
|
983
|
+
fill: "url(#ninfer-throughput)"
|
|
984
|
+
}),
|
|
985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", {
|
|
986
|
+
points,
|
|
987
|
+
stroke: "var(--dsw-alias-state-success-primary)",
|
|
988
|
+
strokeWidth: "2.5",
|
|
989
|
+
strokeLinecap: "round",
|
|
990
|
+
fill: "none"
|
|
991
|
+
}),
|
|
992
|
+
values.length === 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
|
|
993
|
+
cx: 40,
|
|
994
|
+
cy: 130 - Math.min(1, values[0] / max) * 110,
|
|
995
|
+
r: 3,
|
|
996
|
+
fill: "var(--dsw-alias-state-success-primary)"
|
|
997
|
+
})
|
|
998
|
+
]
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* The local NInfer engine as an operations view: state, GPU, throughput,
|
|
1003
|
+
* speculative-decoding acceptance, load progress and the reason a start failed.
|
|
1004
|
+
*/
|
|
1005
|
+
function NInferPanel({ ninfer, strataActive, onShowStrata }) {
|
|
1006
|
+
const { state, error, statusError, notice, busy, act, save, refresh } = ninfer;
|
|
1007
|
+
const live = useNInferLive({ enabled: true });
|
|
1008
|
+
const [stoppingStrata, setStoppingStrata] = (0, react.useState)(false);
|
|
1009
|
+
const phase = state?.phase;
|
|
1010
|
+
const gpu = live.telemetry?.gpus?.[0];
|
|
1011
|
+
const vramUsed = gpu?.usedMiB;
|
|
1012
|
+
const vramTotal = gpu?.memoryMiB;
|
|
1013
|
+
const vramPercent = vramUsed != null && vramTotal ? vramUsed / vramTotal * 100 : null;
|
|
1014
|
+
const failed = typeof live.summary.error === "string" && live.summary.error !== "";
|
|
1015
|
+
const loading = phase === "starting";
|
|
1016
|
+
const running = phase === "running" && state?.healthy !== false;
|
|
1017
|
+
const label = failed ? "Failed" : running ? "Serving" : loading ? "Loading" : phase === "unmanaged" ? "External" : phase === "stopped" ? "Stopped" : "Connecting";
|
|
1018
|
+
const pill = failed ? "bad" : running ? "good" : loading ? "warm" : "";
|
|
1019
|
+
const context = state?.context === void 0 ? "—" : Math.round(state.context / 1024) + "K";
|
|
1020
|
+
const identity = state?.runtime === "wsl" ? "WSL · pid " + (state.pid ?? "—") : state?.containerId ? "Container · " + state.containerId.slice(0, 12) : "local process";
|
|
1021
|
+
const endpoint = state?.baseUrl ?? "http://127.0.0.1:8083/v1";
|
|
1022
|
+
const dashboard = endpoint.replace(/\/v1\/?$/u, "") + "/";
|
|
1023
|
+
const canStart = phase === "stopped" && state?.complete === true;
|
|
1024
|
+
/** Another launcher may hold the engine; when it is provably ours the page can reclaim it. */
|
|
1025
|
+
const external = phase === "unmanaged";
|
|
1026
|
+
const canTakeOver = external && state?.takeover === true;
|
|
1027
|
+
const canStop = state?.owned === true || canTakeOver;
|
|
1028
|
+
const progress = loading ? Math.max(0, Math.min(100, live.summary.weightPercent ?? 0)) : 0;
|
|
1029
|
+
const sampleCount = live.history.filter((sample) => sample.decode != null).length;
|
|
1030
|
+
const vramReading = vramUsed != null && vramTotal != null;
|
|
1031
|
+
async function stopStrata() {
|
|
1032
|
+
if (stoppingStrata) return;
|
|
1033
|
+
setStoppingStrata(true);
|
|
1034
|
+
try {
|
|
1035
|
+
await fetch("/api/aryastrata/stop", {
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
headers: { "x-strata-control": "1" },
|
|
1038
|
+
signal: AbortSignal.timeout(24e4)
|
|
1039
|
+
});
|
|
1040
|
+
refresh();
|
|
1041
|
+
} catch {} finally {
|
|
1042
|
+
setStoppingStrata(false);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [
|
|
1046
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
1047
|
+
className: "lc-card",
|
|
1048
|
+
style: { padding: "22px 24px" },
|
|
1049
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1050
|
+
className: "lc-ninfer-hero",
|
|
1051
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
1052
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1053
|
+
className: "lc-eyebrow",
|
|
1054
|
+
children: "NInfer · local engine"
|
|
1055
|
+
}),
|
|
1056
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1057
|
+
style: {
|
|
1058
|
+
fontSize: 26,
|
|
1059
|
+
letterSpacing: "-.6px"
|
|
1060
|
+
},
|
|
1061
|
+
children: "Qwen3.8-27B"
|
|
1062
|
+
}),
|
|
1063
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1064
|
+
className: "lc-sub",
|
|
1065
|
+
children: [
|
|
1066
|
+
"Dense · GSQ3 · ",
|
|
1067
|
+
context,
|
|
1068
|
+
" context · ",
|
|
1069
|
+
state?.vision ? "vision" : "text only",
|
|
1070
|
+
" · ",
|
|
1071
|
+
state?.draftTokens ?? 3,
|
|
1072
|
+
" MTP drafts · ",
|
|
1073
|
+
identity
|
|
1074
|
+
]
|
|
1075
|
+
}),
|
|
1076
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1077
|
+
className: "lc-actions",
|
|
1078
|
+
style: { marginTop: 14 },
|
|
1079
|
+
children: [
|
|
1080
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1081
|
+
className: `lc-pill ${pill}`,
|
|
1082
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "lc-dot" }), label]
|
|
1083
|
+
}),
|
|
1084
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1085
|
+
className: "lc-pill",
|
|
1086
|
+
children: endpoint
|
|
1087
|
+
}),
|
|
1088
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1089
|
+
className: "lc-pill",
|
|
1090
|
+
children: `MTP ${state?.draftTokens ?? 3}`
|
|
1091
|
+
}),
|
|
1092
|
+
state?.vision === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1093
|
+
className: "lc-pill",
|
|
1094
|
+
children: "vision"
|
|
1095
|
+
})
|
|
1096
|
+
]
|
|
1097
|
+
})
|
|
1098
|
+
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1099
|
+
className: "lc-ninfer-actions",
|
|
1100
|
+
children: [
|
|
1101
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1102
|
+
className: "lc-button primary",
|
|
1103
|
+
disabled: busy || !canStart,
|
|
1104
|
+
onClick: () => void act("start"),
|
|
1105
|
+
children: "Start NInfer"
|
|
1106
|
+
}),
|
|
1107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1108
|
+
className: "lc-button",
|
|
1109
|
+
disabled: busy || !canStop,
|
|
1110
|
+
onClick: () => void act("stop"),
|
|
1111
|
+
children: "Stop NInfer"
|
|
1112
|
+
}),
|
|
1113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1114
|
+
className: "lc-button",
|
|
1115
|
+
disabled: busy || !canStop,
|
|
1116
|
+
onClick: () => void act("restart"),
|
|
1117
|
+
children: "Restart NInfer"
|
|
1118
|
+
}),
|
|
1119
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1120
|
+
className: "lc-button ghost",
|
|
1121
|
+
disabled: busy,
|
|
1122
|
+
onClick: () => {
|
|
1123
|
+
refresh();
|
|
1124
|
+
live.refresh();
|
|
1125
|
+
},
|
|
1126
|
+
children: "Refresh"
|
|
1127
|
+
}),
|
|
1128
|
+
(running || external && state?.healthy === true) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1129
|
+
className: "lc-button",
|
|
1130
|
+
href: dashboard,
|
|
1131
|
+
target: "_blank",
|
|
1132
|
+
rel: "noopener noreferrer",
|
|
1133
|
+
children: "Open NInfer dashboard"
|
|
1134
|
+
})
|
|
1135
|
+
]
|
|
1136
|
+
})]
|
|
1137
|
+
})
|
|
1138
|
+
}),
|
|
1139
|
+
error !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1140
|
+
className: "lc-notice error",
|
|
1141
|
+
role: "alert",
|
|
1142
|
+
children: error
|
|
1143
|
+
}),
|
|
1144
|
+
statusError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1145
|
+
className: "lc-notice error",
|
|
1146
|
+
role: "status",
|
|
1147
|
+
children: statusError
|
|
1148
|
+
}),
|
|
1149
|
+
notice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1150
|
+
className: "lc-notice",
|
|
1151
|
+
role: "status",
|
|
1152
|
+
children: notice
|
|
1153
|
+
}),
|
|
1154
|
+
external && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1155
|
+
className: "lc-notice",
|
|
1156
|
+
role: "status",
|
|
1157
|
+
children: [
|
|
1158
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "NInfer is running outside this Control Center." }),
|
|
1159
|
+
" ",
|
|
1160
|
+
state?.detail,
|
|
1161
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1162
|
+
className: "lc-actions",
|
|
1163
|
+
children: [
|
|
1164
|
+
canTakeOver && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1165
|
+
className: "lc-button primary",
|
|
1166
|
+
disabled: busy,
|
|
1167
|
+
onClick: () => void act("start"),
|
|
1168
|
+
children: "Take over engine"
|
|
1169
|
+
}),
|
|
1170
|
+
canTakeOver && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1171
|
+
className: "lc-button",
|
|
1172
|
+
disabled: busy,
|
|
1173
|
+
onClick: () => void act("stop"),
|
|
1174
|
+
children: "Stop the external engine"
|
|
1175
|
+
}),
|
|
1176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1177
|
+
className: "lc-button ghost",
|
|
1178
|
+
disabled: busy,
|
|
1179
|
+
onClick: () => refresh(),
|
|
1180
|
+
children: "Refresh status"
|
|
1181
|
+
})
|
|
1182
|
+
]
|
|
1183
|
+
})
|
|
1184
|
+
]
|
|
1185
|
+
}),
|
|
1186
|
+
failed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1187
|
+
className: "lc-notice error",
|
|
1188
|
+
role: "alert",
|
|
1189
|
+
children: [
|
|
1190
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "NInfer could not start." }),
|
|
1191
|
+
" ",
|
|
1192
|
+
live.summary.error,
|
|
1193
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1194
|
+
className: "lc-sub",
|
|
1195
|
+
children: [vramReading ? `The GPU shows ${gb(vramUsed)} of ${gb(vramTotal)} GB in use${strataActive ? " — Strata is holding it" : ""}. ` : "", "Starting again stops Strata first."]
|
|
1196
|
+
}),
|
|
1197
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1198
|
+
className: "lc-actions",
|
|
1199
|
+
children: [
|
|
1200
|
+
strataActive && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1201
|
+
className: "lc-button",
|
|
1202
|
+
disabled: stoppingStrata,
|
|
1203
|
+
onClick: () => void stopStrata(),
|
|
1204
|
+
children: stoppingStrata ? "Stopping Strata…" : "Stop Strata"
|
|
1205
|
+
}),
|
|
1206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1207
|
+
className: "lc-button primary",
|
|
1208
|
+
disabled: busy || !canStart,
|
|
1209
|
+
onClick: () => void act("start"),
|
|
1210
|
+
children: "Retry start"
|
|
1211
|
+
}),
|
|
1212
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1213
|
+
className: "lc-button ghost",
|
|
1214
|
+
onClick: onShowStrata,
|
|
1215
|
+
children: "Open Strata controls"
|
|
1216
|
+
})
|
|
1217
|
+
]
|
|
1218
|
+
})
|
|
1219
|
+
]
|
|
1220
|
+
}),
|
|
1221
|
+
!failed && strataActive && phase === "stopped" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1222
|
+
className: "lc-notice",
|
|
1223
|
+
role: "status",
|
|
1224
|
+
children: [
|
|
1225
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Strata has the GPU." }),
|
|
1226
|
+
" ",
|
|
1227
|
+
vramReading ? `It is using ${gb(vramUsed)} of ${gb(vramTotal)} GB. ` : "",
|
|
1228
|
+
"Starting NInfer stops Strata first.",
|
|
1229
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1230
|
+
className: "lc-actions",
|
|
1231
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1232
|
+
className: "lc-button ghost",
|
|
1233
|
+
onClick: onShowStrata,
|
|
1234
|
+
children: "Open Strata controls"
|
|
1235
|
+
})
|
|
1236
|
+
})
|
|
1237
|
+
]
|
|
1238
|
+
}),
|
|
1239
|
+
loading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1240
|
+
className: "lc-card",
|
|
1241
|
+
children: [
|
|
1242
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1243
|
+
className: "lc-section",
|
|
1244
|
+
style: { marginTop: 0 },
|
|
1245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Loading model" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1246
|
+
className: "lc-sub",
|
|
1247
|
+
children: "Weights are read from the pinned artifact before the server listens."
|
|
1248
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1249
|
+
className: "lc-pill warm",
|
|
1250
|
+
children: [
|
|
1251
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "lc-dot" }),
|
|
1252
|
+
progress.toFixed(0),
|
|
1253
|
+
"%"
|
|
1254
|
+
]
|
|
1255
|
+
})]
|
|
1256
|
+
}),
|
|
1257
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1258
|
+
className: "lc-bar",
|
|
1259
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: progress + "%" } })
|
|
1260
|
+
}),
|
|
1261
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1262
|
+
className: "lc-sub",
|
|
1263
|
+
children: [
|
|
1264
|
+
gb(live.summary.weightGiB),
|
|
1265
|
+
" / ",
|
|
1266
|
+
gb(live.summary.weightTotalGiB),
|
|
1267
|
+
" GiB · ",
|
|
1268
|
+
rate(live.summary.weightBytesPerSecond ? live.summary.weightBytesPerSecond / 1024 / 1024 : void 0),
|
|
1269
|
+
" MiB/s · ETA ",
|
|
1270
|
+
live.summary.etaSeconds ?? "—",
|
|
1271
|
+
"s"
|
|
1272
|
+
]
|
|
1273
|
+
})
|
|
1274
|
+
]
|
|
1275
|
+
}),
|
|
1276
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1277
|
+
className: "lc-ninfer-split",
|
|
1278
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NInferApi, {
|
|
1279
|
+
state,
|
|
1280
|
+
running
|
|
1281
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NInferSpec, { state })]
|
|
1282
|
+
}),
|
|
1283
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1284
|
+
className: "lc-card",
|
|
1285
|
+
style: { marginTop: 16 },
|
|
1286
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1287
|
+
className: "lc-section",
|
|
1288
|
+
style: { marginTop: 0 },
|
|
1289
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Live engine" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1290
|
+
className: "lc-sub",
|
|
1291
|
+
children: "From the engine's own log and this computer's sensors."
|
|
1292
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: "3 s refresh" })]
|
|
1293
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1294
|
+
className: "lc-grid lc-ninfer-kpis",
|
|
1295
|
+
children: [
|
|
1296
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1297
|
+
label: "MODEL STATE",
|
|
1298
|
+
value: label,
|
|
1299
|
+
detail: running ? (live.summary.running ?? 0) > 0 ? `${live.summary.running} in flight` : "Waiting for a request" : loading ? "Reading weights" : failed ? "Startup failed" : "Not loaded"
|
|
1300
|
+
}),
|
|
1301
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1302
|
+
label: "DECODE",
|
|
1303
|
+
value: running ? rate(live.summary.decodeTokPerSecond) : "—",
|
|
1304
|
+
detail: "prefill " + rate(live.summary.prefillTokPerSecond) + " tok/s"
|
|
1305
|
+
}),
|
|
1306
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1307
|
+
label: "MTP ACCEPTANCE",
|
|
1308
|
+
value: live.summary.mtpPercent == null ? "—" : live.summary.mtpPercent.toFixed(1) + "%",
|
|
1309
|
+
detail: live.summary.mtpOffered ? `${live.summary.mtpAccepted?.toLocaleString()} / ${live.summary.mtpOffered.toLocaleString()} accepted` : "No speculative request yet"
|
|
1310
|
+
}),
|
|
1311
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1312
|
+
label: "VRAM",
|
|
1313
|
+
value: `${gb(vramUsed)} / ${gb(vramTotal)} GB`,
|
|
1314
|
+
detail: gpu?.name ?? "No GPU reading",
|
|
1315
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1316
|
+
className: "lc-bar",
|
|
1317
|
+
style: { margin: "10px 0 4px" },
|
|
1318
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: { width: (vramPercent ?? 0) + "%" } })
|
|
1319
|
+
})
|
|
1320
|
+
}),
|
|
1321
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1322
|
+
label: "GPU LOAD",
|
|
1323
|
+
value: gpu?.utilization == null ? "—" : Math.round(gpu.utilization) + "%",
|
|
1324
|
+
detail: gpu?.temperature == null ? "No temperature" : `${Math.round(gpu.temperature)} °C${gpu.powerWatts == null ? "" : " · " + Math.round(gpu.powerWatts) + " W"}`
|
|
1325
|
+
}),
|
|
1326
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1327
|
+
label: "CONTEXT",
|
|
1328
|
+
value: context,
|
|
1329
|
+
detail: `${state?.vision ? "vision" : "text only"} · MTP ${state?.draftTokens ?? 3}`
|
|
1330
|
+
}),
|
|
1331
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1332
|
+
label: "ACTIVE REQUESTS",
|
|
1333
|
+
value: running ? String(live.summary.running ?? 0) : "—",
|
|
1334
|
+
detail: `${live.summary.requests?.toLocaleString() ?? 0} requests served`
|
|
1335
|
+
}),
|
|
1336
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Kpi, {
|
|
1337
|
+
label: "HOST",
|
|
1338
|
+
value: live.telemetry?.cpuPct == null ? "—" : Math.round(live.telemetry.cpuPct) + "%",
|
|
1339
|
+
detail: live.telemetry?.ramTotalGiB ? `CPU · RAM ${live.telemetry.ramUsedGiB?.toFixed(1)} / ${live.telemetry.ramTotalGiB.toFixed(1)} GiB` : "CPU · no memory reading"
|
|
1340
|
+
})
|
|
1341
|
+
]
|
|
1342
|
+
})]
|
|
1343
|
+
}),
|
|
1344
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
1345
|
+
className: "lc-card",
|
|
1346
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1347
|
+
className: "lc-section",
|
|
1348
|
+
style: { marginTop: 0 },
|
|
1349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", { children: "Decode throughput" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1350
|
+
className: "lc-sub",
|
|
1351
|
+
children: "Tokens per second reported by the engine's own throughput log."
|
|
1352
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("small", { children: [
|
|
1353
|
+
sampleCount,
|
|
1354
|
+
" ",
|
|
1355
|
+
sampleCount === 1 ? "sample" : "samples",
|
|
1356
|
+
" · 3 s refresh"
|
|
1357
|
+
] })]
|
|
1358
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Sparkline, { samples: live.history })]
|
|
1359
|
+
}),
|
|
1360
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NInferTuning, {
|
|
1361
|
+
state,
|
|
1362
|
+
busy,
|
|
1363
|
+
save
|
|
1364
|
+
}),
|
|
1365
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1366
|
+
style: { marginTop: 18 },
|
|
1367
|
+
children: [
|
|
1368
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", {
|
|
1369
|
+
className: "lc-muted",
|
|
1370
|
+
children: "NInfer logs"
|
|
1371
|
+
}),
|
|
1372
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1373
|
+
className: "lc-actions",
|
|
1374
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1375
|
+
className: "lc-button",
|
|
1376
|
+
onClick: () => live.refresh(),
|
|
1377
|
+
children: "Refresh logs"
|
|
1378
|
+
})
|
|
1379
|
+
}),
|
|
1380
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1381
|
+
className: "lc-terminal",
|
|
1382
|
+
children: live.text === "" ? "No NInfer log output yet." : live.text
|
|
1383
|
+
})
|
|
1384
|
+
]
|
|
1385
|
+
})
|
|
1386
|
+
] });
|
|
1387
|
+
}
|
|
1388
|
+
//#endregion
|
|
1389
|
+
//#region src/client/use-ninfer.ts
|
|
1390
|
+
/**
|
|
1391
|
+
* Poll the owned local engine and drive its existing routes.
|
|
1392
|
+
*
|
|
1393
|
+
* Status reads abort the previous one, so a poll that started before an action
|
|
1394
|
+
* cannot overwrite the state that action produced. Polling pauses while the
|
|
1395
|
+
* page is hidden: in WSL and Docker modes every read is a real process call.
|
|
1396
|
+
*/
|
|
1397
|
+
function useNInfer({ enabled, intervalMs }) {
|
|
1398
|
+
const [state, setState] = (0, react.useState)();
|
|
1399
|
+
const [error, setError] = (0, react.useState)("");
|
|
1400
|
+
const [statusError, setStatusError] = (0, react.useState)("");
|
|
1401
|
+
const [notice, setNotice] = (0, react.useState)("");
|
|
1402
|
+
const [busy, setBusy] = (0, react.useState)(false);
|
|
1403
|
+
const [logs, setLogs] = (0, react.useState)("");
|
|
1404
|
+
const [logsError, setLogsError] = (0, react.useState)("");
|
|
1405
|
+
const inflight = (0, react.useRef)(null);
|
|
1406
|
+
const acting = (0, react.useRef)(false);
|
|
1407
|
+
function readStatus() {
|
|
1408
|
+
inflight.current?.abort();
|
|
1409
|
+
const controller = new AbortController();
|
|
1410
|
+
inflight.current = controller;
|
|
1411
|
+
(async () => {
|
|
1412
|
+
try {
|
|
1413
|
+
const next = await responseData(await fetch("/api/ninfer/status", { signal: AbortSignal.any([controller.signal, AbortSignal.timeout(15e3)]) }));
|
|
1414
|
+
if (controller.signal.aborted) return;
|
|
1415
|
+
setState(next);
|
|
1416
|
+
setStatusError("");
|
|
1417
|
+
} catch (reason) {
|
|
1418
|
+
if (!controller.signal.aborted) setStatusError(reason instanceof Error ? reason.message : String(reason));
|
|
1419
|
+
}
|
|
1420
|
+
})();
|
|
1421
|
+
}
|
|
1422
|
+
async function act(action) {
|
|
1423
|
+
if (!enabled || acting.current) return;
|
|
1424
|
+
acting.current = true;
|
|
1425
|
+
setBusy(true);
|
|
1426
|
+
setError("");
|
|
1427
|
+
setNotice("");
|
|
1428
|
+
try {
|
|
1429
|
+
const result = await responseData(await fetch("/api/ninfer/" + action, {
|
|
1430
|
+
method: "POST",
|
|
1431
|
+
headers: { "x-ninfer-control": "1" },
|
|
1432
|
+
signal: AbortSignal.timeout(24e4)
|
|
1433
|
+
}));
|
|
1434
|
+
setNotice(typeof result.detail === "string" ? result.detail : "NInfer " + action + " requested.");
|
|
1435
|
+
readStatus();
|
|
1436
|
+
} catch (reason) {
|
|
1437
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
1438
|
+
} finally {
|
|
1439
|
+
acting.current = false;
|
|
1440
|
+
setBusy(false);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
async function save(settings) {
|
|
1444
|
+
if (!enabled || acting.current) return;
|
|
1445
|
+
acting.current = true;
|
|
1446
|
+
setBusy(true);
|
|
1447
|
+
setError("");
|
|
1448
|
+
setNotice("");
|
|
1449
|
+
try {
|
|
1450
|
+
const result = await responseData(await fetch("/api/ninfer/configure", {
|
|
1451
|
+
method: "POST",
|
|
1452
|
+
headers: {
|
|
1453
|
+
"x-ninfer-control": "1",
|
|
1454
|
+
"content-type": "application/json"
|
|
1455
|
+
},
|
|
1456
|
+
body: JSON.stringify(settings),
|
|
1457
|
+
signal: AbortSignal.timeout(3e4)
|
|
1458
|
+
}));
|
|
1459
|
+
setNotice(typeof result.detail === "string" ? result.detail : "NInfer settings saved.");
|
|
1460
|
+
readStatus();
|
|
1461
|
+
} catch (reason) {
|
|
1462
|
+
setError(reason instanceof Error ? reason.message : String(reason));
|
|
1463
|
+
} finally {
|
|
1464
|
+
acting.current = false;
|
|
1465
|
+
setBusy(false);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async function loadLogs() {
|
|
1469
|
+
if (!enabled) return;
|
|
1470
|
+
try {
|
|
1471
|
+
const result = await responseData(await fetch("/api/ninfer/logs", { signal: AbortSignal.timeout(15e3) }));
|
|
1472
|
+
setLogs(typeof result.text === "string" ? result.text : "");
|
|
1473
|
+
setLogsError("");
|
|
1474
|
+
} catch (reason) {
|
|
1475
|
+
setLogsError(reason instanceof Error ? reason.message : String(reason));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
(0, react.useEffect)(() => {
|
|
1479
|
+
if (!enabled) return;
|
|
1480
|
+
readStatus();
|
|
1481
|
+
let timer;
|
|
1482
|
+
const schedule = () => {
|
|
1483
|
+
timer = setTimeout(() => {
|
|
1484
|
+
if (document.visibilityState === "visible") readStatus();
|
|
1485
|
+
schedule();
|
|
1486
|
+
}, intervalMs);
|
|
1487
|
+
};
|
|
1488
|
+
schedule();
|
|
1489
|
+
const onVisible = () => {
|
|
1490
|
+
if (document.visibilityState === "visible") readStatus();
|
|
1491
|
+
};
|
|
1492
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
1493
|
+
return () => {
|
|
1494
|
+
clearTimeout(timer);
|
|
1495
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
1496
|
+
inflight.current?.abort();
|
|
1497
|
+
inflight.current = null;
|
|
1498
|
+
};
|
|
1499
|
+
}, [enabled, intervalMs]);
|
|
1500
|
+
return {
|
|
1501
|
+
state,
|
|
1502
|
+
error,
|
|
1503
|
+
statusError,
|
|
1504
|
+
notice,
|
|
1505
|
+
busy,
|
|
1506
|
+
logs,
|
|
1507
|
+
logsError,
|
|
1508
|
+
act,
|
|
1509
|
+
save,
|
|
1510
|
+
loadLogs,
|
|
1511
|
+
refresh: readStatus
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/client/StrataControlCenter.tsx
|
|
1516
|
+
/** Header that routes one call at the configured Host instead of this machine. */
|
|
1517
|
+
const remoteHeader = { "x-strata-target": "remote" };
|
|
1518
|
+
/** Port the paired-computer relay listens on, assumed when a peer is picked from the list. */
|
|
1519
|
+
const PAGE_PORT = "45900";
|
|
1520
|
+
/** Header that marks a write, as every lifecycle route demands. */
|
|
1521
|
+
const controlHeader = { "x-strata-control": "1" };
|
|
1522
|
+
/** Whole minutes until a code expires, floored at zero. */
|
|
1523
|
+
function minutesLeft(expiresAt) {
|
|
1524
|
+
return Math.max(0, Math.ceil((expiresAt - Date.now()) / 6e4));
|
|
1525
|
+
}
|
|
1526
|
+
/** Accept a typed address in the form people actually type it. */
|
|
1527
|
+
function asAddress(value) {
|
|
1528
|
+
const trimmed = value.trim();
|
|
1529
|
+
if (trimmed === "" || trimmed.startsWith("/") || /^[a-z][a-z0-9+.-]*:\/\//iu.test(trimmed)) return trimmed;
|
|
1530
|
+
return "http://" + trimmed;
|
|
1531
|
+
}
|
|
1532
|
+
/** AryaStrata lifecycle controls, for this computer or for the Host it was pointed at. */
|
|
1533
|
+
function StrataControlCenter() {
|
|
1534
|
+
const [state, setState] = (0, react.useState)(), [target, setTarget] = (0, react.useState)(), [error, setError] = (0, react.useState)(""), [loadError, setLoadError] = (0, react.useState)(""), [notice, setNotice] = (0, react.useState)(""), [busy, setBusy] = (0, react.useState)(false), [generation, setGeneration] = (0, react.useState)(0);
|
|
1535
|
+
const [address, setAddress] = (0, react.useState)(""), [token, setToken] = (0, react.useState)(""), [pairing, setPairing] = (0, react.useState)(false), [pairNotice, setPairNotice] = (0, react.useState)(""), [pairError, setPairError] = (0, react.useState)("");
|
|
1536
|
+
const [code, setCode] = (0, react.useState)(""), [joining, setJoining] = (0, react.useState)(false), [joinError, setJoinError] = (0, react.useState)(""), [joinNotice, setJoinNotice] = (0, react.useState)("");
|
|
1537
|
+
const [peers, setPeers] = (0, react.useState)(), [peerNote, setPeerNote] = (0, react.useState)(""), [finding, setFinding] = (0, react.useState)(false);
|
|
1538
|
+
const [offer, setOffer] = (0, react.useState)(), [offerError, setOfferError] = (0, react.useState)(""), [minting, setMinting] = (0, react.useState)(false);
|
|
1539
|
+
const [devices, setDevices] = (0, react.useState)(), [deviceNote, setDeviceNote] = (0, react.useState)(""), [revoking, setRevoking] = (0, react.useState)("");
|
|
1540
|
+
const [webUi, setWebUi] = (0, react.useState)();
|
|
1541
|
+
const remote = !!target && target.address !== "";
|
|
1542
|
+
const localHost = location.protocol === "dsh-app:" || [
|
|
1543
|
+
"localhost",
|
|
1544
|
+
"127.0.0.1",
|
|
1545
|
+
"[::1]"
|
|
1546
|
+
].includes(location.hostname);
|
|
1547
|
+
const enginesHere = localHost && !remote;
|
|
1548
|
+
const [engine, setEngine] = (0, react.useState)("strata");
|
|
1549
|
+
const ninfer = useNInfer({
|
|
1550
|
+
enabled: enginesHere,
|
|
1551
|
+
intervalMs: engine === "ninfer" ? 5e3 : 2e4
|
|
1552
|
+
});
|
|
1553
|
+
const strataActive = ["running", "starting"].includes(state?.phase ?? "");
|
|
1554
|
+
const strataBlocked = ["running", "starting"].includes(ninfer.state?.phase ?? "");
|
|
1555
|
+
(0, react.useEffect)(() => {
|
|
1556
|
+
if (!enginesHere) setEngine("strata");
|
|
1557
|
+
}, [enginesHere]);
|
|
1558
|
+
(0, react.useEffect)(() => {
|
|
1559
|
+
const abort = new AbortController();
|
|
1560
|
+
(async () => {
|
|
1561
|
+
try {
|
|
1562
|
+
setTarget(await responseData(await fetch("/api/aryastrata/connection", { signal: AbortSignal.any([abort.signal, AbortSignal.timeout(15e3)]) })));
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
if (!abort.signal.aborted) setLoadError(e instanceof Error ? e.message : String(e));
|
|
1565
|
+
}
|
|
1566
|
+
})();
|
|
1567
|
+
return () => abort.abort();
|
|
1568
|
+
}, [generation]);
|
|
1569
|
+
(0, react.useEffect)(() => {
|
|
1570
|
+
const abort = new AbortController();
|
|
1571
|
+
let pending = false;
|
|
1572
|
+
async function refresh() {
|
|
1573
|
+
if (pending) return;
|
|
1574
|
+
pending = true;
|
|
1575
|
+
try {
|
|
1576
|
+
const data = await responseData(await fetch("/api/aryastrata/status", {
|
|
1577
|
+
headers: remote ? remoteHeader : {},
|
|
1578
|
+
signal: AbortSignal.any([abort.signal, AbortSignal.timeout(15e3)])
|
|
1579
|
+
}));
|
|
1580
|
+
if (!abort.signal.aborted) {
|
|
1581
|
+
setState(data);
|
|
1582
|
+
setLoadError("");
|
|
1583
|
+
}
|
|
1584
|
+
} catch (e) {
|
|
1585
|
+
if (!abort.signal.aborted) setLoadError(e instanceof Error ? e.message : String(e));
|
|
1586
|
+
} finally {
|
|
1587
|
+
pending = false;
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
refresh();
|
|
1591
|
+
const timer = setInterval(() => void refresh(), 3e3);
|
|
1592
|
+
return () => {
|
|
1593
|
+
abort.abort();
|
|
1594
|
+
clearInterval(timer);
|
|
1595
|
+
};
|
|
1596
|
+
}, [remote]);
|
|
1597
|
+
(0, react.useEffect)(() => {
|
|
1598
|
+
if (!remote || !state?.port) return;
|
|
1599
|
+
const abort = new AbortController();
|
|
1600
|
+
(async () => {
|
|
1601
|
+
try {
|
|
1602
|
+
const data = await responseData(await fetch("/api/aryastrata/webui?port=" + String(state.port), { signal: AbortSignal.any([abort.signal, AbortSignal.timeout(2e4)]) }));
|
|
1603
|
+
if (!abort.signal.aborted) setWebUi(data);
|
|
1604
|
+
} catch (e) {
|
|
1605
|
+
if (!abort.signal.aborted) setWebUi({
|
|
1606
|
+
url: null,
|
|
1607
|
+
detail: e instanceof Error ? e.message : String(e)
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
})();
|
|
1611
|
+
return () => abort.abort();
|
|
1612
|
+
}, [
|
|
1613
|
+
remote,
|
|
1614
|
+
state?.port,
|
|
1615
|
+
generation
|
|
1616
|
+
]);
|
|
1617
|
+
async function action(operation) {
|
|
1618
|
+
if (busy) return;
|
|
1619
|
+
setBusy(true);
|
|
1620
|
+
setError("");
|
|
1621
|
+
setNotice("");
|
|
1622
|
+
try {
|
|
1623
|
+
const result = await responseData(await fetch("/api/aryastrata/" + operation, {
|
|
1624
|
+
method: "POST",
|
|
1625
|
+
headers: remote ? {
|
|
1626
|
+
...remoteHeader,
|
|
1627
|
+
"x-strata-control": "1"
|
|
1628
|
+
} : { "x-strata-control": "1" },
|
|
1629
|
+
signal: AbortSignal.timeout(24e4)
|
|
1630
|
+
}));
|
|
1631
|
+
if (result.state) setState(result.state);
|
|
1632
|
+
if (result.ok === false) throw Error(result.detail);
|
|
1633
|
+
setNotice(result.detail);
|
|
1634
|
+
setGeneration((n) => n + 1);
|
|
1635
|
+
} catch (e) {
|
|
1636
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
1637
|
+
} finally {
|
|
1638
|
+
setBusy(false);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
async function call(path, draft) {
|
|
1642
|
+
if (pairing) return;
|
|
1643
|
+
setPairing(true);
|
|
1644
|
+
setPairError("");
|
|
1645
|
+
setPairNotice("");
|
|
1646
|
+
try {
|
|
1647
|
+
const body = draft ?? {
|
|
1648
|
+
address: asAddress(address),
|
|
1649
|
+
token
|
|
1650
|
+
};
|
|
1651
|
+
const result = await responseData(await fetch("/api/aryastrata" + path, {
|
|
1652
|
+
method: "POST",
|
|
1653
|
+
headers: {
|
|
1654
|
+
...controlHeader,
|
|
1655
|
+
"content-type": "application/json"
|
|
1656
|
+
},
|
|
1657
|
+
body: JSON.stringify(body),
|
|
1658
|
+
signal: AbortSignal.timeout(3e4)
|
|
1659
|
+
}));
|
|
1660
|
+
if (result.ok === false) throw Error(result.detail);
|
|
1661
|
+
setPairNotice(result.detail);
|
|
1662
|
+
if (path === "/connection/save") setGeneration((n) => n + 1);
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
setPairError(e instanceof Error ? e.message : String(e));
|
|
1665
|
+
} finally {
|
|
1666
|
+
setPairing(false);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
async function useThisComputer() {
|
|
1670
|
+
setAddress("");
|
|
1671
|
+
setToken("");
|
|
1672
|
+
await call("/connection/save", {
|
|
1673
|
+
address: "",
|
|
1674
|
+
token: ""
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
/** Ask this Host for a code another computer can redeem. */
|
|
1678
|
+
async function createCode() {
|
|
1679
|
+
if (minting) return;
|
|
1680
|
+
setMinting(true);
|
|
1681
|
+
setOfferError("");
|
|
1682
|
+
try {
|
|
1683
|
+
const result = await responseData(await fetch("/api/aryastrata/pair/offer", { signal: AbortSignal.timeout(15e3) }));
|
|
1684
|
+
if (result.ok === false) throw Error(result.detail);
|
|
1685
|
+
setOffer(result);
|
|
1686
|
+
} catch (e) {
|
|
1687
|
+
setOfferError(e instanceof Error ? e.message : String(e));
|
|
1688
|
+
} finally {
|
|
1689
|
+
setMinting(false);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
/** Redeem the typed code on the typed address, then start controlling that engine. */
|
|
1693
|
+
async function join() {
|
|
1694
|
+
if (joining) return;
|
|
1695
|
+
setJoining(true);
|
|
1696
|
+
setJoinError("");
|
|
1697
|
+
setJoinNotice("");
|
|
1698
|
+
try {
|
|
1699
|
+
const result = await responseData(await fetch("/api/aryastrata/pair", {
|
|
1700
|
+
method: "POST",
|
|
1701
|
+
headers: {
|
|
1702
|
+
...controlHeader,
|
|
1703
|
+
"content-type": "application/json"
|
|
1704
|
+
},
|
|
1705
|
+
body: JSON.stringify({
|
|
1706
|
+
address: asAddress(address),
|
|
1707
|
+
code: code.replace(/[^a-z0-9]/giu, "").toUpperCase()
|
|
1708
|
+
}),
|
|
1709
|
+
signal: AbortSignal.timeout(3e4)
|
|
1710
|
+
}));
|
|
1711
|
+
if (result.ok === false) throw Error(result.detail);
|
|
1712
|
+
setJoinNotice(result.detail);
|
|
1713
|
+
setCode("");
|
|
1714
|
+
setGeneration((n) => n + 1);
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
setJoinError(e instanceof Error ? e.message : String(e));
|
|
1717
|
+
} finally {
|
|
1718
|
+
setJoining(false);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
/** List the machines on this computer's tailnet, so pairing needs no address typed. */
|
|
1722
|
+
async function findComputers() {
|
|
1723
|
+
if (finding) return;
|
|
1724
|
+
setFinding(true);
|
|
1725
|
+
setPeerNote("");
|
|
1726
|
+
try {
|
|
1727
|
+
const result = await responseData(await fetch("/api/aryastrata/peers", { signal: AbortSignal.timeout(2e4) }));
|
|
1728
|
+
setPeers(result.peers ?? []);
|
|
1729
|
+
if (result.detail) setPeerNote(String(result.detail));
|
|
1730
|
+
else if (!(result.peers ?? []).length) setPeerNote("No other machine answered, so type the host address instead.");
|
|
1731
|
+
} catch (e) {
|
|
1732
|
+
setPeerNote(e instanceof Error ? e.message : String(e));
|
|
1733
|
+
} finally {
|
|
1734
|
+
setFinding(false);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function loadDevices() {
|
|
1738
|
+
try {
|
|
1739
|
+
const result = await responseData(await fetch("/api/aryastrata/pair/devices", { signal: AbortSignal.timeout(15e3) }));
|
|
1740
|
+
setDevices(result.devices ?? []);
|
|
1741
|
+
setDeviceNote("");
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
setDeviceNote(e instanceof Error ? e.message : String(e));
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
async function revokeDevice(id) {
|
|
1747
|
+
if (revoking !== "") return;
|
|
1748
|
+
setRevoking(id);
|
|
1749
|
+
setDeviceNote("");
|
|
1750
|
+
try {
|
|
1751
|
+
const result = await responseData(await fetch("/api/aryastrata/pair/revoke", {
|
|
1752
|
+
method: "POST",
|
|
1753
|
+
headers: {
|
|
1754
|
+
...controlHeader,
|
|
1755
|
+
"content-type": "application/json"
|
|
1756
|
+
},
|
|
1757
|
+
body: JSON.stringify({ id }),
|
|
1758
|
+
signal: AbortSignal.timeout(15e3)
|
|
1759
|
+
}));
|
|
1760
|
+
setDevices(result.devices ?? []);
|
|
1761
|
+
setDeviceNote(result.ok === false ? result.detail : "Unpaired. That computer can no longer control this engine.");
|
|
1762
|
+
} catch (e) {
|
|
1763
|
+
setDeviceNote(e instanceof Error ? e.message : String(e));
|
|
1764
|
+
} finally {
|
|
1765
|
+
setRevoking("");
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
let url;
|
|
1769
|
+
try {
|
|
1770
|
+
if (state) {
|
|
1771
|
+
const address = remote && webUi?.url ? webUi.url : state.baseUrl;
|
|
1772
|
+
const candidate = new URL(address);
|
|
1773
|
+
if (["http:", "https:"].includes(candidate.protocol)) {
|
|
1774
|
+
candidate.hash = "monitor";
|
|
1775
|
+
url = candidate;
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
} catch {}
|
|
1779
|
+
const loopback = url && [
|
|
1780
|
+
"localhost",
|
|
1781
|
+
"127.0.0.1",
|
|
1782
|
+
"[::1]"
|
|
1783
|
+
].includes(url.hostname);
|
|
1784
|
+
const accessible = !!url && (remote ? webUi?.url != null : !loopback || localHost);
|
|
1785
|
+
const embedded = accessible && !(location.protocol === "https:" && url?.protocol === "http:");
|
|
1786
|
+
const where = remote ? target.address.replace(/^https?:\/\//u, "") : "This computer";
|
|
1787
|
+
const remaining = offer ? minutesLeft(offer.expiresAt) : 0;
|
|
1788
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
|
|
1789
|
+
className: "lc",
|
|
1790
|
+
style: {
|
|
1791
|
+
height: "100%",
|
|
1792
|
+
display: "flex",
|
|
1793
|
+
flexDirection: "column",
|
|
1794
|
+
minHeight: 0
|
|
1795
|
+
},
|
|
1796
|
+
children: [
|
|
1797
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: css }),
|
|
1798
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
1799
|
+
className: "lc-header",
|
|
1800
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", { children: "AryaStrata Control Center" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1801
|
+
className: "lc-sub",
|
|
1802
|
+
children: "Run AryaStrata, or the local NInfer Qwen3.8 dense model, on this computer."
|
|
1803
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1804
|
+
role: "status",
|
|
1805
|
+
className: "lc-pill",
|
|
1806
|
+
children: engine === "ninfer" ? ninfer.busy ? "Working…" : ninfer.state?.phase ?? "Connecting…" : busy ? "Working…" : state?.phase ?? "Connecting…"
|
|
1807
|
+
})]
|
|
1808
|
+
}),
|
|
1809
|
+
enginesHere && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("nav", {
|
|
1810
|
+
className: "lc-tabs",
|
|
1811
|
+
"aria-label": "Engines",
|
|
1812
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1813
|
+
className: "lc-tab " + (engine === "strata" ? "active" : ""),
|
|
1814
|
+
"aria-pressed": engine === "strata",
|
|
1815
|
+
onClick: () => setEngine("strata"),
|
|
1816
|
+
children: "Strata"
|
|
1817
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1818
|
+
className: "lc-tab " + (engine === "ninfer" ? "active" : ""),
|
|
1819
|
+
"aria-pressed": engine === "ninfer",
|
|
1820
|
+
onClick: () => setEngine("ninfer"),
|
|
1821
|
+
children: "NInfer"
|
|
1822
|
+
})]
|
|
1823
|
+
}),
|
|
1824
|
+
engine === "ninfer" && enginesHere ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NInferPanel, {
|
|
1825
|
+
ninfer,
|
|
1826
|
+
strataActive,
|
|
1827
|
+
onShowStrata: () => setEngine("strata")
|
|
1828
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1830
|
+
className: "lc-actions",
|
|
1831
|
+
style: { flexWrap: "wrap" },
|
|
1832
|
+
children: [
|
|
1833
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1834
|
+
className: "lc-button primary",
|
|
1835
|
+
disabled: busy || !state || state.phase !== "stopped",
|
|
1836
|
+
onClick: () => void action("start"),
|
|
1837
|
+
children: "Start engine"
|
|
1838
|
+
}),
|
|
1839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1840
|
+
className: "lc-button",
|
|
1841
|
+
disabled: busy || !state?.owned,
|
|
1842
|
+
onClick: () => void action("stop"),
|
|
1843
|
+
children: "Stop engine"
|
|
1844
|
+
}),
|
|
1845
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1846
|
+
className: "lc-button",
|
|
1847
|
+
disabled: busy || !state?.owned,
|
|
1848
|
+
onClick: () => void action("restart"),
|
|
1849
|
+
children: "Restart engine"
|
|
1850
|
+
}),
|
|
1851
|
+
accessible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1852
|
+
className: "lc-button",
|
|
1853
|
+
href: url?.href,
|
|
1854
|
+
target: "_blank",
|
|
1855
|
+
rel: "noopener noreferrer",
|
|
1856
|
+
children: "Open AryaStrata web UI"
|
|
1857
|
+
}),
|
|
1858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1859
|
+
className: "lc-button",
|
|
1860
|
+
onClick: () => {
|
|
1861
|
+
setWebUi(void 0);
|
|
1862
|
+
setGeneration((n) => n + 1);
|
|
1863
|
+
},
|
|
1864
|
+
children: "Reload web UI"
|
|
1865
|
+
})
|
|
1866
|
+
]
|
|
1867
|
+
}),
|
|
1868
|
+
strataBlocked && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1869
|
+
className: "lc-sub",
|
|
1870
|
+
children: ["NInfer is running. Starting Strata stops NInfer automatically. ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1871
|
+
className: "lc-button ghost",
|
|
1872
|
+
disabled: busy,
|
|
1873
|
+
onClick: () => setEngine("ninfer"),
|
|
1874
|
+
children: "Open NInfer controls"
|
|
1875
|
+
})]
|
|
1876
|
+
}),
|
|
1877
|
+
(error || loadError) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1878
|
+
className: "lc-notice error",
|
|
1879
|
+
role: "alert",
|
|
1880
|
+
children: error || loadError
|
|
1881
|
+
}),
|
|
1882
|
+
notice && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1883
|
+
className: "lc-notice",
|
|
1884
|
+
role: "status",
|
|
1885
|
+
children: notice
|
|
1886
|
+
}),
|
|
1887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1888
|
+
className: "lc-connection",
|
|
1889
|
+
style: { marginTop: 12 },
|
|
1890
|
+
children: [
|
|
1891
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1892
|
+
className: "lc-muted",
|
|
1893
|
+
children: "Controlling"
|
|
1894
|
+
}),
|
|
1895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: where }),
|
|
1896
|
+
remote && target.storage !== "none" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1897
|
+
className: "lc-tag",
|
|
1898
|
+
children: target.storage === "profile" ? "saved in this profile" : "saved in strata-connection.json"
|
|
1899
|
+
}),
|
|
1900
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
1901
|
+
className: "lc-connection-setup",
|
|
1902
|
+
style: { marginLeft: "auto" },
|
|
1903
|
+
children: [
|
|
1904
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: remote ? "Change computer" : "Control another computer" }),
|
|
1905
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1906
|
+
className: "lc-connection-fields",
|
|
1907
|
+
children: [
|
|
1908
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1909
|
+
className: "lc-field",
|
|
1910
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Computer" }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
1911
|
+
"aria-label": "Computer",
|
|
1912
|
+
className: "lc-input",
|
|
1913
|
+
value: peers?.some((peer) => asAddress(address) === `http://${peer.address}:${PAGE_PORT}`) ? asAddress(address) : "",
|
|
1914
|
+
onChange: (event) => {
|
|
1915
|
+
if (event.target.value !== "") setAddress(event.target.value);
|
|
1916
|
+
},
|
|
1917
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
1918
|
+
value: "",
|
|
1919
|
+
children: "Choose a computer on your tailnet…"
|
|
1920
|
+
}), (peers ?? []).map((peer) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
1921
|
+
value: `http://${peer.address}:${PAGE_PORT}`,
|
|
1922
|
+
children: [
|
|
1923
|
+
peer.name,
|
|
1924
|
+
" · ",
|
|
1925
|
+
peer.address
|
|
1926
|
+
]
|
|
1927
|
+
}, peer.address))]
|
|
1928
|
+
})]
|
|
1929
|
+
}),
|
|
1930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1931
|
+
className: "lc-actions",
|
|
1932
|
+
style: { flexWrap: "wrap" },
|
|
1933
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1934
|
+
className: "lc-button",
|
|
1935
|
+
disabled: finding,
|
|
1936
|
+
onClick: () => void findComputers(),
|
|
1937
|
+
children: finding ? "Looking…" : "Find computers"
|
|
1938
|
+
})
|
|
1939
|
+
}),
|
|
1940
|
+
peerNote !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1941
|
+
className: "lc-muted",
|
|
1942
|
+
children: peerNote
|
|
1943
|
+
}),
|
|
1944
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1945
|
+
className: "lc-field",
|
|
1946
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Host address" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1947
|
+
className: "lc-input",
|
|
1948
|
+
value: address,
|
|
1949
|
+
placeholder: "100.x.y.z:45900 (the address shown beside the code)",
|
|
1950
|
+
onChange: (event) => setAddress(event.target.value)
|
|
1951
|
+
})]
|
|
1952
|
+
}),
|
|
1953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
1954
|
+
className: "lc-field",
|
|
1955
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Pairing code" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1956
|
+
className: "lc-input",
|
|
1957
|
+
value: code,
|
|
1958
|
+
maxLength: 9,
|
|
1959
|
+
autoComplete: "off",
|
|
1960
|
+
spellCheck: false,
|
|
1961
|
+
placeholder: "ABCD-2345",
|
|
1962
|
+
onChange: (event) => setCode(event.target.value.toUpperCase())
|
|
1963
|
+
})]
|
|
1964
|
+
})
|
|
1965
|
+
]
|
|
1966
|
+
}),
|
|
1967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1968
|
+
className: "lc-actions",
|
|
1969
|
+
style: { flexWrap: "wrap" },
|
|
1970
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1971
|
+
className: "lc-button primary",
|
|
1972
|
+
disabled: joining || code.replace(/[^A-Z0-9]/giu, "").length !== 8 || asAddress(address) === "",
|
|
1973
|
+
onClick: () => void join(),
|
|
1974
|
+
children: joining ? "Pairing…" : "Pair and control"
|
|
1975
|
+
}), remote && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1976
|
+
className: "lc-button",
|
|
1977
|
+
disabled: joining,
|
|
1978
|
+
onClick: () => void useThisComputer(),
|
|
1979
|
+
children: "Use this computer"
|
|
1980
|
+
})]
|
|
1981
|
+
}),
|
|
1982
|
+
joinError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1983
|
+
className: "lc-notice error",
|
|
1984
|
+
role: "alert",
|
|
1985
|
+
children: joinError
|
|
1986
|
+
}),
|
|
1987
|
+
joinNotice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1988
|
+
className: "lc-notice",
|
|
1989
|
+
role: "status",
|
|
1990
|
+
children: joinNotice
|
|
1991
|
+
}),
|
|
1992
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
1993
|
+
className: "lc-muted",
|
|
1994
|
+
children: [
|
|
1995
|
+
"On the engine computer open ",
|
|
1996
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "AryaStrata → Let another computer control this engine" }),
|
|
1997
|
+
", create a code, then type it here. The code works once and expires after five minutes."
|
|
1998
|
+
]
|
|
1999
|
+
}),
|
|
2000
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", { children: [
|
|
2001
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "Use a control token instead" }),
|
|
2002
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2003
|
+
className: "lc-connection-fields",
|
|
2004
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2005
|
+
className: "lc-field",
|
|
2006
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Control token" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
2007
|
+
className: "lc-input",
|
|
2008
|
+
value: token,
|
|
2009
|
+
autoComplete: "off",
|
|
2010
|
+
placeholder: "the other computer's strata.controlToken",
|
|
2011
|
+
onChange: (event) => setToken(event.target.value)
|
|
2012
|
+
})]
|
|
2013
|
+
})
|
|
2014
|
+
}),
|
|
2015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2016
|
+
className: "lc-actions",
|
|
2017
|
+
style: { flexWrap: "wrap" },
|
|
2018
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2019
|
+
className: "lc-button",
|
|
2020
|
+
disabled: pairing,
|
|
2021
|
+
onClick: () => void call("/connection/test"),
|
|
2022
|
+
children: "Test connection"
|
|
2023
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2024
|
+
className: "lc-button",
|
|
2025
|
+
disabled: pairing,
|
|
2026
|
+
onClick: () => void call("/connection/save"),
|
|
2027
|
+
children: "Save and use"
|
|
2028
|
+
})]
|
|
2029
|
+
}),
|
|
2030
|
+
pairError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2031
|
+
className: "lc-notice error",
|
|
2032
|
+
role: "alert",
|
|
2033
|
+
children: pairError
|
|
2034
|
+
}),
|
|
2035
|
+
pairNotice !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2036
|
+
className: "lc-notice",
|
|
2037
|
+
role: "status",
|
|
2038
|
+
children: pairNotice
|
|
2039
|
+
}),
|
|
2040
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2041
|
+
className: "lc-muted",
|
|
2042
|
+
children: [
|
|
2043
|
+
"Both computers run this plugin. On the other one, set ",
|
|
2044
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "strata.controlToken" }),
|
|
2045
|
+
" in its plugin configuration and enter that token here."
|
|
2046
|
+
]
|
|
2047
|
+
})
|
|
2048
|
+
] }),
|
|
2049
|
+
!remote && target && !target.acceptsControl && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2050
|
+
className: "lc-muted",
|
|
2051
|
+
children: [
|
|
2052
|
+
"This computer has no ",
|
|
2053
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: "strata.controlToken" }),
|
|
2054
|
+
" and no paired computer yet, so it answers only this computer until one of those is set up."
|
|
2055
|
+
]
|
|
2056
|
+
})
|
|
2057
|
+
]
|
|
2058
|
+
}),
|
|
2059
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
2060
|
+
className: "lc-connection-setup",
|
|
2061
|
+
children: [
|
|
2062
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "Let another computer control this engine" }),
|
|
2063
|
+
offer === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2064
|
+
className: "lc-actions",
|
|
2065
|
+
style: { flexWrap: "wrap" },
|
|
2066
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2067
|
+
className: "lc-button",
|
|
2068
|
+
disabled: minting,
|
|
2069
|
+
onClick: () => void createCode(),
|
|
2070
|
+
children: minting ? "Creating…" : "Create pairing code"
|
|
2071
|
+
})
|
|
2072
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [
|
|
2073
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2074
|
+
style: {
|
|
2075
|
+
display: "flex",
|
|
2076
|
+
alignItems: "baseline",
|
|
2077
|
+
gap: 12,
|
|
2078
|
+
margin: "8px 0"
|
|
2079
|
+
},
|
|
2080
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
2081
|
+
className: "lc-code",
|
|
2082
|
+
style: {
|
|
2083
|
+
fontSize: 22,
|
|
2084
|
+
letterSpacing: "0.12em"
|
|
2085
|
+
},
|
|
2086
|
+
children: offer.display
|
|
2087
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2088
|
+
className: "lc-muted",
|
|
2089
|
+
children: [remaining === 0 ? "expired" : `${remaining} minute${remaining === 1 ? "" : "s"} left`, " · works once"]
|
|
2090
|
+
})]
|
|
2091
|
+
}),
|
|
2092
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2093
|
+
className: "lc-facts",
|
|
2094
|
+
children: offer.targets.map((targetAddress) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2095
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2096
|
+
className: "lc-tag",
|
|
2097
|
+
children: targetAddress.kind
|
|
2098
|
+
}),
|
|
2099
|
+
" ",
|
|
2100
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: targetAddress.url })
|
|
2101
|
+
] }, targetAddress.address))
|
|
2102
|
+
}),
|
|
2103
|
+
offer.targets.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2104
|
+
className: "lc-notice",
|
|
2105
|
+
children: "This computer has no network address another machine can reach, so pair it over Tailscale first."
|
|
2106
|
+
}),
|
|
2107
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2108
|
+
className: "lc-actions",
|
|
2109
|
+
style: { flexWrap: "wrap" },
|
|
2110
|
+
children: [
|
|
2111
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2112
|
+
className: "lc-button",
|
|
2113
|
+
disabled: minting,
|
|
2114
|
+
onClick: () => void createCode(),
|
|
2115
|
+
children: minting ? "Creating…" : "New code"
|
|
2116
|
+
}),
|
|
2117
|
+
offer.targets[0] !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
2118
|
+
label: "Copy address",
|
|
2119
|
+
value: offer.targets[0].url
|
|
2120
|
+
}),
|
|
2121
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CopyButton, {
|
|
2122
|
+
label: "Copy code",
|
|
2123
|
+
value: offer.code
|
|
2124
|
+
})
|
|
2125
|
+
]
|
|
2126
|
+
}),
|
|
2127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2128
|
+
className: "lc-muted",
|
|
2129
|
+
children: [
|
|
2130
|
+
"On the other computer open ",
|
|
2131
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "AryaStrata → Control another computer" }),
|
|
2132
|
+
", pick the address above, type the code, then ",
|
|
2133
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: "Pair and control" }),
|
|
2134
|
+
"."
|
|
2135
|
+
]
|
|
2136
|
+
})
|
|
2137
|
+
] }),
|
|
2138
|
+
offerError !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2139
|
+
className: "lc-notice error",
|
|
2140
|
+
role: "alert",
|
|
2141
|
+
children: offerError
|
|
2142
|
+
}),
|
|
2143
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
2144
|
+
onToggle: (event) => {
|
|
2145
|
+
if (event.target.open && devices === void 0) loadDevices();
|
|
2146
|
+
},
|
|
2147
|
+
children: [
|
|
2148
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("summary", { children: ["Paired computers", devices === void 0 ? "" : ` (${devices.length})`] }),
|
|
2149
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2150
|
+
className: "lc-actions",
|
|
2151
|
+
style: { flexWrap: "wrap" },
|
|
2152
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2153
|
+
className: "lc-button",
|
|
2154
|
+
onClick: () => void loadDevices(),
|
|
2155
|
+
children: "Refresh"
|
|
2156
|
+
})
|
|
2157
|
+
}),
|
|
2158
|
+
devices !== void 0 && devices.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2159
|
+
className: "lc-muted",
|
|
2160
|
+
children: "No computer is paired with this engine yet."
|
|
2161
|
+
}),
|
|
2162
|
+
devices !== void 0 && devices.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
2163
|
+
className: "lc-facts",
|
|
2164
|
+
children: devices.map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [
|
|
2165
|
+
device.label,
|
|
2166
|
+
" ",
|
|
2167
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2168
|
+
className: "lc-muted",
|
|
2169
|
+
children: new Date(device.created).toLocaleDateString()
|
|
2170
|
+
}),
|
|
2171
|
+
" ",
|
|
2172
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2173
|
+
className: "lc-button",
|
|
2174
|
+
disabled: revoking !== "",
|
|
2175
|
+
onClick: () => void revokeDevice(device.id),
|
|
2176
|
+
children: "Unpair"
|
|
2177
|
+
})
|
|
2178
|
+
] }, device.id))
|
|
2179
|
+
}),
|
|
2180
|
+
deviceNote !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2181
|
+
className: "lc-notice",
|
|
2182
|
+
role: "status",
|
|
2183
|
+
children: deviceNote
|
|
2184
|
+
})
|
|
2185
|
+
]
|
|
2186
|
+
})
|
|
2187
|
+
]
|
|
2188
|
+
})
|
|
2189
|
+
]
|
|
2190
|
+
}),
|
|
2191
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("details", {
|
|
2192
|
+
style: { marginTop: 16 },
|
|
2193
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("summary", { children: "Chat output limits" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutputTuning, { remote: !localHost })]
|
|
2194
|
+
}),
|
|
2195
|
+
state && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
2196
|
+
className: "lc-sub",
|
|
2197
|
+
children: [
|
|
2198
|
+
state.modelName,
|
|
2199
|
+
" · ",
|
|
2200
|
+
state.detail
|
|
2201
|
+
]
|
|
2202
|
+
}),
|
|
2203
|
+
remote ? embedded && state?.responding ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
2204
|
+
title: "AryaStrata engine web UI",
|
|
2205
|
+
src: url?.href,
|
|
2206
|
+
style: {
|
|
2207
|
+
flex: 1,
|
|
2208
|
+
width: "100%",
|
|
2209
|
+
minHeight: 560,
|
|
2210
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
2211
|
+
borderRadius: 12,
|
|
2212
|
+
marginTop: 16
|
|
2213
|
+
}
|
|
2214
|
+
}, generation) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2215
|
+
className: "lc-empty",
|
|
2216
|
+
children: !state ? "Connecting to " + where + "…" : !state.responding ? "Start the engine on " + where + " to use its web interface there." : webUi === void 0 ? "Looking for the engine web interface on " + where + "…" : webUi.url === null ? webUi.detail : "Open the web UI in a separate tab to use this HTTP engine from a secure page."
|
|
2217
|
+
}) : embedded && state?.responding ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("iframe", {
|
|
2218
|
+
title: "AryaStrata engine web UI",
|
|
2219
|
+
src: url?.href,
|
|
2220
|
+
style: {
|
|
2221
|
+
flex: 1,
|
|
2222
|
+
width: "100%",
|
|
2223
|
+
minHeight: 560,
|
|
2224
|
+
border: "1px solid var(--dsw-alias-border-l1)",
|
|
2225
|
+
borderRadius: 12,
|
|
2226
|
+
marginTop: 16
|
|
2227
|
+
}
|
|
2228
|
+
}, generation) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2229
|
+
className: "lc-empty",
|
|
2230
|
+
children: !state ? "Connecting to the engine controller…" : !accessible ? "Open AryaStrata on the host computer. Its loopback address is unavailable from this device." : !embedded ? "Open the web UI in a separate tab to use this HTTP engine from a secure page." : "Start the engine to open its web UI."
|
|
2231
|
+
})
|
|
2232
|
+
] })
|
|
2233
|
+
]
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
/** Copy one value to the clipboard, saying so only while it happened. */
|
|
2237
|
+
function CopyButton({ label, value }) {
|
|
2238
|
+
const [done, setDone] = (0, react.useState)(false);
|
|
2239
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2240
|
+
className: "lc-button",
|
|
2241
|
+
onClick: () => {
|
|
2242
|
+
navigator.clipboard.writeText(value).then(() => {
|
|
2243
|
+
setDone(true);
|
|
2244
|
+
setTimeout(() => setDone(false), 1500);
|
|
2245
|
+
}, () => setDone(false));
|
|
2246
|
+
},
|
|
2247
|
+
children: done ? "Copied" : label
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/client/index.ts
|
|
2252
|
+
const inject = ["slots"];
|
|
2253
|
+
function apply(ctx) {
|
|
2254
|
+
ctx.slots.inject("main", () => ctx.slots.register({
|
|
2255
|
+
name: "main",
|
|
2256
|
+
key: "llamacpp"
|
|
2257
|
+
}, StrataControlCenter));
|
|
2258
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
2259
|
+
name: "sidebar.panellist",
|
|
2260
|
+
id: "llamacpp",
|
|
2261
|
+
order: -7,
|
|
2262
|
+
label: "AryaStrata"
|
|
2263
|
+
}, SidebarIcon));
|
|
2264
|
+
}
|
|
2265
|
+
//#endregion
|
|
2266
|
+
exports.apply = apply;
|
|
2267
|
+
exports.inject = inject;
|
|
2268
|
+
return module.exports;
|
|
2269
|
+
}
|
|
2270
|
+
});
|
|
2271
|
+
|
|
2272
|
+
//# sourceMappingURL=client.js.map
|