@brainpilot/web 0.0.10 → 0.0.11
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/dist/assets/index-DkoqxJfs.css +1 -0
- package/dist/assets/index-DtLW483q.js +451 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/__tests__/messageGroups.test.ts +150 -0
- package/src/__tests__/newUiEvents.test.ts +32 -0
- package/src/components/chat/MessageStream.tsx +102 -32
- package/src/components/chat/PromptComposer.tsx +1 -0
- package/src/components/demo/DemoView.tsx +1 -1
- package/src/components/session/AgentTraceViews.tsx +5 -9
- package/src/components/settings/KnowledgeBasePanel.tsx +307 -143
- package/src/components/settings/SettingsDialog.tsx +115 -57
- package/src/components/shell/SandboxStatus.tsx +128 -84
- package/src/contexts/messageGroups.ts +110 -4
- package/src/contexts/messageReducer.ts +11 -1
- package/src/i18n/messages/chat.ts +10 -0
- package/src/i18n/messages/sandbox.ts +3 -0
- package/src/i18n/messages/settings.ts +40 -4
- package/src/i18n/messages/trace.ts +0 -2
- package/src/styles/global.css +821 -69
- package/src/utils/api.ts +63 -0
- package/dist/assets/index-D63mUJxx.js +0 -450
- package/dist/assets/index-D8J9Cnup.css +0 -1
|
@@ -54,6 +54,41 @@ function isKnownStage(stage: string): stage is Stage {
|
|
|
54
54
|
return (STAGES as string[]).includes(stage);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
interface SetupState {
|
|
58
|
+
percent: number;
|
|
59
|
+
msg: string;
|
|
60
|
+
status: "pending" | "running" | "done" | "error";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Small progress bar used for the venv + model download rows in the env
|
|
64
|
+
// card. Kept local to this file so we don't grow a shared "ProgressBar"
|
|
65
|
+
// component just for two sites — one file, one style.
|
|
66
|
+
function SetupProgressRow({
|
|
67
|
+
label,
|
|
68
|
+
state,
|
|
69
|
+
}: {
|
|
70
|
+
label: string;
|
|
71
|
+
state: SetupState;
|
|
72
|
+
}) {
|
|
73
|
+
const pct = Math.max(0, Math.min(100, state.percent));
|
|
74
|
+
return (
|
|
75
|
+
<div className="kb-setup-row">
|
|
76
|
+
<div className="kb-setup-row__head">
|
|
77
|
+
<span className="kb-setup-row__label">{label}</span>
|
|
78
|
+
<span className="kb-setup-row__pct">{state.status === "done" ? "✓ 100%" : `${pct}%`}</span>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="kb-setup-row__track" aria-hidden="true">
|
|
81
|
+
<span className={`kb-setup-row__fill kb-setup-row__fill--${state.status}`} style={{ width: `${pct}%` }} />
|
|
82
|
+
</div>
|
|
83
|
+
{state.msg ? (
|
|
84
|
+
<div className={`kb-setup-row__msg ${state.status === "error" ? "kb-setup-row__msg--error" : ""}`}>
|
|
85
|
+
{state.msg}
|
|
86
|
+
</div>
|
|
87
|
+
) : null}
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
export function KnowledgeBasePanel() {
|
|
58
93
|
const t = useT();
|
|
59
94
|
const [ocrApiKey, setOcrApiKey] = useState("");
|
|
@@ -61,6 +96,7 @@ export function KnowledgeBasePanel() {
|
|
|
61
96
|
const [metaBaseUrl, setMetaBaseUrl] = useState("");
|
|
62
97
|
const [metaModel, setMetaModel] = useState("");
|
|
63
98
|
const [reuseAgentKey, setReuseAgentKey] = useState(true);
|
|
99
|
+
const [useHfMirror, setUseHfMirror] = useState(false);
|
|
64
100
|
const [skip, setSkip] = useState<Record<Stage, boolean>>({
|
|
65
101
|
ocr: false,
|
|
66
102
|
extract: false,
|
|
@@ -71,8 +107,10 @@ export function KnowledgeBasePanel() {
|
|
|
71
107
|
const [stages, setStages] = useState<Record<Stage, StageState>>(INITIAL_STAGE_STATE);
|
|
72
108
|
const [active, setActive] = useState(false);
|
|
73
109
|
/** Distinguishes "the build is running" from "env setup is running" so the
|
|
74
|
-
* UI can show the right spinner / disable the right buttons.
|
|
75
|
-
|
|
110
|
+
* UI can show the right spinner / disable the right buttons.
|
|
111
|
+
* "setup-full" covers the one-click orchestration that runs venv + model
|
|
112
|
+
* download back-to-back. */
|
|
113
|
+
const [activeJob, setActiveJob] = useState<"build" | "setup-env" | "setup-full" | null>(null);
|
|
76
114
|
const [error, setError] = useState<string | null>(null);
|
|
77
115
|
const [env, setEnv] = useState<{
|
|
78
116
|
python: string;
|
|
@@ -83,13 +121,39 @@ export function KnowledgeBasePanel() {
|
|
|
83
121
|
kbRoot: string;
|
|
84
122
|
} | null>(null);
|
|
85
123
|
const [envBusy, setEnvBusy] = useState(false);
|
|
124
|
+
// Model download runs in parallel with env setup; it needs its own
|
|
125
|
+
// progress row and busy flag so the UI can show both in flight at once.
|
|
126
|
+
const [modelBusy, setModelBusy] = useState(false);
|
|
127
|
+
const [envProgress, setEnvProgress] = useState<SetupState>(
|
|
128
|
+
{ percent: 0, msg: "", status: "pending" },
|
|
129
|
+
);
|
|
130
|
+
const [modelProgress, setModelProgress] = useState<SetupState>(
|
|
131
|
+
{ percent: 0, msg: "", status: "pending" },
|
|
132
|
+
);
|
|
133
|
+
// Persisted OCR key state: true iff the backend confirms one is on disk.
|
|
134
|
+
// When true, the input shows a masked preview + "Change" button so the
|
|
135
|
+
// user doesn't have to re-type it on every page reload.
|
|
136
|
+
const [ocrKeySaved, setOcrKeySaved] = useState(false);
|
|
137
|
+
const [ocrKeyPreview, setOcrKeyPreview] = useState("");
|
|
138
|
+
const [ocrKeyEditing, setOcrKeyEditing] = useState(false);
|
|
86
139
|
const logRef = useRef<HTMLDivElement | null>(null);
|
|
87
140
|
const sseRef = useRef<EventSource | null>(null);
|
|
88
141
|
|
|
89
142
|
// Hydrate from server: if a build is already running (e.g. user reopened
|
|
90
143
|
// the dialog mid-build), show its current status + replay recent events.
|
|
144
|
+
// Also fetch the persisted OCR key state so the input can show "saved".
|
|
91
145
|
useEffect(() => {
|
|
92
146
|
let cancelled = false;
|
|
147
|
+
void (async () => {
|
|
148
|
+
try {
|
|
149
|
+
const cfg = await api.kb.getApiConfig();
|
|
150
|
+
if (cancelled) return;
|
|
151
|
+
setOcrKeySaved(cfg.hasOcrApiKey);
|
|
152
|
+
setOcrKeyPreview(cfg.ocrApiKeyPreview);
|
|
153
|
+
} catch {
|
|
154
|
+
/* api-config fetch is best-effort */
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
93
157
|
void (async () => {
|
|
94
158
|
try {
|
|
95
159
|
const status = await api.kb.status();
|
|
@@ -98,6 +162,7 @@ export function KnowledgeBasePanel() {
|
|
|
98
162
|
if (status.recentEvents?.length) {
|
|
99
163
|
setEvents(status.recentEvents);
|
|
100
164
|
replayStages(status.recentEvents);
|
|
165
|
+
replaySetupProgress(status.recentEvents);
|
|
101
166
|
}
|
|
102
167
|
if (status.active) {
|
|
103
168
|
// Guess which job is running from the most recent event with a
|
|
@@ -163,6 +228,40 @@ export function KnowledgeBasePanel() {
|
|
|
163
228
|
}
|
|
164
229
|
}
|
|
165
230
|
|
|
231
|
+
// Replay setup-env / setup-models progress from a fresh snapshot (used on
|
|
232
|
+
// page reload when there might be a job already in flight — the SSE stream
|
|
233
|
+
// gives us subsequent events, this fills in whatever happened before).
|
|
234
|
+
function replaySetupProgress(history: BuildEvent[]) {
|
|
235
|
+
let envP: SetupState = { percent: 0, msg: "", status: "pending" };
|
|
236
|
+
let modelP: SetupState = { percent: 0, msg: "", status: "pending" };
|
|
237
|
+
for (const ev of history) {
|
|
238
|
+
if (ev.stage === "setup-env") {
|
|
239
|
+
envP = deriveSetupState(envP, ev);
|
|
240
|
+
} else if (ev.stage === "setup-models") {
|
|
241
|
+
modelP = deriveSetupState(modelP, ev);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
setEnvProgress(envP);
|
|
245
|
+
setModelProgress(modelP);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function deriveSetupState(prev: SetupState, ev: BuildEvent): SetupState {
|
|
249
|
+
if (ev.event === "progress") {
|
|
250
|
+
const pct = typeof ev.percent === "number" ? ev.percent : prev.percent;
|
|
251
|
+
return { status: "running", percent: pct, msg: ev.msg };
|
|
252
|
+
}
|
|
253
|
+
if (ev.event === "info") {
|
|
254
|
+
return { ...prev, status: "running", msg: ev.msg };
|
|
255
|
+
}
|
|
256
|
+
if (ev.event === "done") {
|
|
257
|
+
return { status: "done", percent: 100, msg: ev.msg };
|
|
258
|
+
}
|
|
259
|
+
if (ev.event === "error") {
|
|
260
|
+
return { ...prev, status: "error", msg: ev.msg };
|
|
261
|
+
}
|
|
262
|
+
return prev;
|
|
263
|
+
}
|
|
264
|
+
|
|
166
265
|
async function refreshEnv() {
|
|
167
266
|
try {
|
|
168
267
|
const s = await api.kb.status();
|
|
@@ -188,12 +287,27 @@ export function KnowledgeBasePanel() {
|
|
|
188
287
|
setActive(false);
|
|
189
288
|
setActiveJob(null);
|
|
190
289
|
}
|
|
191
|
-
if (ev.stage === "setup-env"
|
|
192
|
-
|
|
290
|
+
if (ev.stage === "setup-env") {
|
|
291
|
+
setEnvProgress((prev) => deriveSetupState(prev, ev));
|
|
292
|
+
if (ev.event === "done" || ev.event === "error") {
|
|
293
|
+
setEnvBusy(false);
|
|
294
|
+
// Re-fetch environment so the banner flips from yellow to green
|
|
295
|
+
// (or stays yellow with the right error).
|
|
296
|
+
void refreshEnv();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (ev.stage === "setup-models") {
|
|
300
|
+
setModelProgress((prev) => deriveSetupState(prev, ev));
|
|
301
|
+
if (ev.event === "info" && !modelBusy) setModelBusy(true);
|
|
302
|
+
if (ev.event === "done" || ev.event === "error") {
|
|
303
|
+
setModelBusy(false);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// The whole "setup-full" umbrella job clears activeJob only when both
|
|
307
|
+
// constituent jobs are done (or one failed). setup-full emits its own
|
|
308
|
+
// synthetic done/error event that we key off here.
|
|
309
|
+
if (ev.stage === "setup-full" && (ev.event === "done" || ev.event === "error")) {
|
|
193
310
|
setActiveJob(null);
|
|
194
|
-
// Re-fetch environment so the banner flips from yellow to green
|
|
195
|
-
// (or stays yellow with the right error).
|
|
196
|
-
void refreshEnv();
|
|
197
311
|
}
|
|
198
312
|
}
|
|
199
313
|
|
|
@@ -227,7 +341,7 @@ export function KnowledgeBasePanel() {
|
|
|
227
341
|
}
|
|
228
342
|
|
|
229
343
|
const formInvalid = useMemo(() => {
|
|
230
|
-
if (skip.ocr === false && !ocrApiKey.trim()) {
|
|
344
|
+
if (skip.ocr === false && !ocrApiKey.trim() && !ocrKeySaved) {
|
|
231
345
|
return t("settings.kb.error.missingOcrKey");
|
|
232
346
|
}
|
|
233
347
|
if (skip.extract === false) {
|
|
@@ -236,7 +350,7 @@ export function KnowledgeBasePanel() {
|
|
|
236
350
|
}
|
|
237
351
|
}
|
|
238
352
|
return null;
|
|
239
|
-
}, [ocrApiKey, metaApiKey, reuseAgentKey, skip.ocr, skip.extract, t]);
|
|
353
|
+
}, [ocrApiKey, ocrKeySaved, metaApiKey, reuseAgentKey, skip.ocr, skip.extract, t]);
|
|
240
354
|
|
|
241
355
|
async function startBuild() {
|
|
242
356
|
setError(null);
|
|
@@ -260,6 +374,7 @@ export function KnowledgeBasePanel() {
|
|
|
260
374
|
metaBaseUrl: metaBaseUrl.trim() || undefined,
|
|
261
375
|
metaModel: metaModel.trim() || undefined,
|
|
262
376
|
skip: skipList.length ? skipList : undefined,
|
|
377
|
+
hfMirror: useHfMirror ? "https://hf-mirror.com" : undefined,
|
|
263
378
|
});
|
|
264
379
|
if (!r.ok) {
|
|
265
380
|
setError(r.error || "build start failed");
|
|
@@ -288,11 +403,13 @@ export function KnowledgeBasePanel() {
|
|
|
288
403
|
// distinct from prior [build:...] / [ocr:...] lines.
|
|
289
404
|
setEnvBusy(true);
|
|
290
405
|
setActiveJob("setup-env");
|
|
406
|
+
setEnvProgress({ percent: 0, msg: "", status: "running" });
|
|
291
407
|
try {
|
|
292
408
|
const r = await api.kb.setupEnv({ reinstall });
|
|
293
409
|
if (!r.ok) {
|
|
294
410
|
setEnvBusy(false);
|
|
295
411
|
setActiveJob(null);
|
|
412
|
+
setEnvProgress({ percent: 0, msg: r.error || "start failed", status: "error" });
|
|
296
413
|
setError(r.error || "setup-env start failed");
|
|
297
414
|
return;
|
|
298
415
|
}
|
|
@@ -300,6 +417,62 @@ export function KnowledgeBasePanel() {
|
|
|
300
417
|
} catch (err) {
|
|
301
418
|
setEnvBusy(false);
|
|
302
419
|
setActiveJob(null);
|
|
420
|
+
setEnvProgress({ percent: 0, msg: String(err), status: "error" });
|
|
421
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** One-click: create venv, then download bge models. The backend chains
|
|
426
|
+
* the two jobs — venv completes first, models kick off automatically. */
|
|
427
|
+
async function startFullSetup() {
|
|
428
|
+
setError(null);
|
|
429
|
+
setEnvBusy(true);
|
|
430
|
+
setModelBusy(true);
|
|
431
|
+
setActiveJob("setup-full");
|
|
432
|
+
setEnvProgress({ percent: 0, msg: "", status: "running" });
|
|
433
|
+
setModelProgress({ percent: 0, msg: "waiting for venv…", status: "pending" });
|
|
434
|
+
try {
|
|
435
|
+
const r = await api.kb.setupFull({
|
|
436
|
+
hfMirror: useHfMirror ? "https://hf-mirror.com" : undefined,
|
|
437
|
+
});
|
|
438
|
+
if (!r.ok) {
|
|
439
|
+
setEnvBusy(false);
|
|
440
|
+
setModelBusy(false);
|
|
441
|
+
setActiveJob(null);
|
|
442
|
+
setEnvProgress({ percent: 0, msg: r.error || "start failed", status: "error" });
|
|
443
|
+
setError(r.error || "setup-full start failed");
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
openSse();
|
|
447
|
+
} catch (err) {
|
|
448
|
+
setEnvBusy(false);
|
|
449
|
+
setModelBusy(false);
|
|
450
|
+
setActiveJob(null);
|
|
451
|
+
setEnvProgress({ percent: 0, msg: String(err), status: "error" });
|
|
452
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Save the OCR API key to disk (backend → API_config.json). Called on
|
|
457
|
+
* blur when the user typed something new. */
|
|
458
|
+
async function saveOcrKey(value: string) {
|
|
459
|
+
if (!value.trim()) return;
|
|
460
|
+
try {
|
|
461
|
+
const r = await api.kb.saveApiConfig({ ocrApiKey: value.trim() });
|
|
462
|
+
if (!r.ok) {
|
|
463
|
+
setError(r.error || "failed to save OCR key");
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
setOcrKeySaved(true);
|
|
467
|
+
// The backend gives us the masked preview on GET; refresh so the UI
|
|
468
|
+
// shows "...abcd" matching what's actually on disk.
|
|
469
|
+
const cfg = await api.kb.getApiConfig();
|
|
470
|
+
setOcrKeyPreview(cfg.ocrApiKeyPreview);
|
|
471
|
+
setOcrKeyEditing(false);
|
|
472
|
+
// Clear the input value now that it's persisted — subsequent builds
|
|
473
|
+
// pick it up from the backend's saved copy.
|
|
474
|
+
setOcrApiKey("");
|
|
475
|
+
} catch (err) {
|
|
303
476
|
setError(err instanceof Error ? err.message : String(err));
|
|
304
477
|
}
|
|
305
478
|
}
|
|
@@ -308,7 +481,7 @@ export function KnowledgeBasePanel() {
|
|
|
308
481
|
<section className="settings-section">
|
|
309
482
|
<div className="settings-section__header">
|
|
310
483
|
<div>
|
|
311
|
-
<h3
|
|
484
|
+
<h3 className="kb-panel__title">
|
|
312
485
|
<Database size={18} aria-hidden />
|
|
313
486
|
{t("settings.kb.title")}
|
|
314
487
|
</h3>
|
|
@@ -319,94 +492,113 @@ export function KnowledgeBasePanel() {
|
|
|
319
492
|
</div>
|
|
320
493
|
|
|
321
494
|
{env ? (
|
|
322
|
-
<div
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
fontSize: 12,
|
|
330
|
-
color: "#334155",
|
|
331
|
-
}}
|
|
332
|
-
>
|
|
333
|
-
<div style={{ marginBottom: 4 }}>
|
|
334
|
-
<strong>{t("settings.kb.env.title")}</strong>
|
|
335
|
-
</div>
|
|
336
|
-
<div style={{ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }}>
|
|
337
|
-
KB_ROOT: {env.kbRoot}
|
|
338
|
-
<br />
|
|
339
|
-
Python: {env.python}
|
|
340
|
-
{env.pythonIsVenv ? " ✓ venv" : ""}
|
|
495
|
+
<div className={`kb-env kb-env--${env.venvExists ? "ready" : "missing"}`}>
|
|
496
|
+
<div className="kb-env__head">
|
|
497
|
+
<span className={`sandbox-chip sandbox-chip--${env.venvExists ? "ok" : "off"}`}>
|
|
498
|
+
<Wrench size={13} />
|
|
499
|
+
{t("settings.kb.env.title")}
|
|
500
|
+
<i className="sandbox-chip__dot" aria-hidden="true" />
|
|
501
|
+
</span>
|
|
341
502
|
</div>
|
|
503
|
+
<dl className="kb-env__facts">
|
|
504
|
+
<div>
|
|
505
|
+
<dt>KB_ROOT</dt>
|
|
506
|
+
<dd>{env.kbRoot}</dd>
|
|
507
|
+
</div>
|
|
508
|
+
<div>
|
|
509
|
+
<dt>Python</dt>
|
|
510
|
+
<dd>{env.python}{env.pythonIsVenv ? " · venv" : ""}</dd>
|
|
511
|
+
</div>
|
|
512
|
+
</dl>
|
|
342
513
|
{!env.venvExists ? (
|
|
343
|
-
<div
|
|
344
|
-
<
|
|
345
|
-
<div
|
|
514
|
+
<div className="kb-env__action">
|
|
515
|
+
<p className="kb-env__note">{t("settings.kb.env.venvMissing")}</p>
|
|
516
|
+
<div className="kb-env__buttons">
|
|
346
517
|
<button
|
|
347
518
|
type="button"
|
|
348
519
|
className="settings-button"
|
|
349
|
-
onClick={() => void
|
|
350
|
-
disabled={envBusy || activeJob !== null}
|
|
351
|
-
title={t("settings.kb.env.
|
|
520
|
+
onClick={() => void startFullSetup()}
|
|
521
|
+
disabled={envBusy || modelBusy || activeJob !== null}
|
|
522
|
+
title={t("settings.kb.env.setupFullHint")}
|
|
352
523
|
>
|
|
353
|
-
<Wrench size={14}
|
|
354
|
-
{t("settings.kb.env.
|
|
524
|
+
<Wrench size={14} aria-hidden />
|
|
525
|
+
{t("settings.kb.env.setupFullButton")}
|
|
355
526
|
</button>
|
|
356
|
-
{envBusy ? <Loader2 size={14} className="
|
|
527
|
+
{envBusy || modelBusy ? <Loader2 size={14} className="spin" aria-hidden /> : null}
|
|
357
528
|
</div>
|
|
358
|
-
<details
|
|
359
|
-
<summary
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
style={{
|
|
364
|
-
marginTop: 4,
|
|
365
|
-
padding: 8,
|
|
366
|
-
background: "#0f172a",
|
|
367
|
-
color: "#e2e8f0",
|
|
368
|
-
borderRadius: 4,
|
|
369
|
-
overflowX: "auto",
|
|
370
|
-
fontSize: 12,
|
|
371
|
-
}}
|
|
372
|
-
>
|
|
373
|
-
{`bash ${env.kbRoot}/scripts/setup_env.sh`}
|
|
529
|
+
<details className="kb-env__fallback">
|
|
530
|
+
<summary>{t("settings.kb.env.cliFallback")}</summary>
|
|
531
|
+
<pre className="kb-code">
|
|
532
|
+
{`bash ${env.kbRoot}/scripts/setup_env.sh
|
|
533
|
+
${env.kbRoot}/.venv/bin/python ${env.kbRoot}/scripts/setup_models.py`}
|
|
374
534
|
</pre>
|
|
375
|
-
<
|
|
376
|
-
{t("settings.kb.env.venvHint")}
|
|
377
|
-
</div>
|
|
535
|
+
<p className="kb-env__note">{t("settings.kb.env.venvHint")}</p>
|
|
378
536
|
</details>
|
|
379
537
|
</div>
|
|
380
538
|
) : (
|
|
381
|
-
<div
|
|
539
|
+
<div className="kb-env__buttons">
|
|
382
540
|
<button
|
|
383
541
|
type="button"
|
|
384
|
-
className="settings-button"
|
|
542
|
+
className="settings-button settings-button--ghost"
|
|
385
543
|
onClick={() => void startEnvSetup(true)}
|
|
386
|
-
disabled={envBusy || activeJob !== null}
|
|
544
|
+
disabled={envBusy || modelBusy || activeJob !== null}
|
|
387
545
|
title={t("settings.kb.env.reinstallHint")}
|
|
388
|
-
style={{ background: "transparent", border: "1px solid #cbd5e1", color: "#334155" }}
|
|
389
546
|
>
|
|
390
|
-
<RefreshCw size={14}
|
|
547
|
+
<RefreshCw size={14} aria-hidden />
|
|
391
548
|
{t("settings.kb.env.reinstallButton")}
|
|
392
549
|
</button>
|
|
393
|
-
{envBusy ? <Loader2 size={14} className="
|
|
550
|
+
{envBusy ? <Loader2 size={14} className="spin" aria-hidden /> : null}
|
|
394
551
|
</div>
|
|
395
552
|
)}
|
|
553
|
+
|
|
554
|
+
{/* Setup progress rows: shown any time either job is/was active. */}
|
|
555
|
+
{(envProgress.status !== "pending" || modelProgress.status !== "pending") ? (
|
|
556
|
+
<div className="kb-setup-rows">
|
|
557
|
+
<SetupProgressRow
|
|
558
|
+
label={t("settings.kb.env.venvProgressLabel")}
|
|
559
|
+
state={envProgress}
|
|
560
|
+
/>
|
|
561
|
+
<SetupProgressRow
|
|
562
|
+
label={t("settings.kb.env.modelProgressLabel")}
|
|
563
|
+
state={modelProgress}
|
|
564
|
+
/>
|
|
565
|
+
</div>
|
|
566
|
+
) : null}
|
|
396
567
|
</div>
|
|
397
568
|
) : null}
|
|
398
569
|
|
|
399
|
-
<div className="
|
|
570
|
+
<div className="kb-fields">
|
|
400
571
|
<label className="settings-field">
|
|
401
572
|
<span>{t("settings.kb.ocrKey")}</span>
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
573
|
+
{ocrKeySaved && !ocrKeyEditing ? (
|
|
574
|
+
<div className="kb-key-saved-row">
|
|
575
|
+
<div className="kb-key-saved">
|
|
576
|
+
{t("settings.kb.ocrKeySaved")} {ocrKeyPreview}
|
|
577
|
+
</div>
|
|
578
|
+
<button
|
|
579
|
+
type="button"
|
|
580
|
+
className="settings-button settings-button--ghost"
|
|
581
|
+
onClick={() => setOcrKeyEditing(true)}
|
|
582
|
+
disabled={active || skip.ocr}
|
|
583
|
+
>
|
|
584
|
+
{t("settings.kb.ocrKeyChange")}
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
) : (
|
|
588
|
+
<input
|
|
589
|
+
type="password"
|
|
590
|
+
value={ocrApiKey}
|
|
591
|
+
onChange={(e) => setOcrApiKey(e.target.value)}
|
|
592
|
+
onBlur={(e) => {
|
|
593
|
+
// Persist on blur so the user doesn't have to remember to
|
|
594
|
+
// save. Empty input (they cleared it) → treat as no-op.
|
|
595
|
+
if (e.target.value.trim()) void saveOcrKey(e.target.value);
|
|
596
|
+
}}
|
|
597
|
+
placeholder="sk-..."
|
|
598
|
+
autoComplete="off"
|
|
599
|
+
disabled={active || skip.ocr}
|
|
600
|
+
/>
|
|
601
|
+
)}
|
|
410
602
|
</label>
|
|
411
603
|
|
|
412
604
|
<label className="settings-check">
|
|
@@ -457,11 +649,24 @@ export function KnowledgeBasePanel() {
|
|
|
457
649
|
</>
|
|
458
650
|
) : null}
|
|
459
651
|
|
|
460
|
-
<
|
|
652
|
+
<label className="settings-check">
|
|
653
|
+
<input
|
|
654
|
+
type="checkbox"
|
|
655
|
+
checked={useHfMirror}
|
|
656
|
+
onChange={(e) => setUseHfMirror(e.target.checked)}
|
|
657
|
+
disabled={active}
|
|
658
|
+
/>
|
|
659
|
+
<span>{t("settings.kb.useHfMirror")}</span>
|
|
660
|
+
</label>
|
|
661
|
+
<p className="kb-field-hint">
|
|
662
|
+
{t("settings.kb.useHfMirrorHint")}
|
|
663
|
+
</p>
|
|
664
|
+
|
|
665
|
+
<fieldset className="kb-stages">
|
|
461
666
|
<legend>{t("settings.kb.stages")}</legend>
|
|
462
|
-
<div
|
|
667
|
+
<div className="kb-stages__row">
|
|
463
668
|
{STAGES.map((s) => (
|
|
464
|
-
<label key={s} className="settings-check"
|
|
669
|
+
<label key={s} className="settings-check kb-stages__item">
|
|
465
670
|
<input
|
|
466
671
|
type="checkbox"
|
|
467
672
|
checked={!skip[s]}
|
|
@@ -477,7 +682,7 @@ export function KnowledgeBasePanel() {
|
|
|
477
682
|
</fieldset>
|
|
478
683
|
</div>
|
|
479
684
|
|
|
480
|
-
<div
|
|
685
|
+
<div className="kb-actions">
|
|
481
686
|
{!active ? (
|
|
482
687
|
<button
|
|
483
688
|
className="settings-button"
|
|
@@ -493,7 +698,7 @@ export function KnowledgeBasePanel() {
|
|
|
493
698
|
: undefined)
|
|
494
699
|
}
|
|
495
700
|
>
|
|
496
|
-
<Play size={14}
|
|
701
|
+
<Play size={14} aria-hidden />
|
|
497
702
|
{t("settings.kb.start")}
|
|
498
703
|
</button>
|
|
499
704
|
) : (
|
|
@@ -502,91 +707,50 @@ export function KnowledgeBasePanel() {
|
|
|
502
707
|
type="button"
|
|
503
708
|
onClick={() => void cancelBuild()}
|
|
504
709
|
>
|
|
505
|
-
<Square size={14}
|
|
710
|
+
<Square size={14} aria-hidden />
|
|
506
711
|
{t("settings.kb.cancel")}
|
|
507
712
|
</button>
|
|
508
713
|
)}
|
|
509
|
-
{active ? <Loader2 size={16} className="
|
|
714
|
+
{active ? <Loader2 size={16} className="spin" aria-hidden /> : null}
|
|
510
715
|
</div>
|
|
511
716
|
|
|
512
|
-
{error ?
|
|
513
|
-
<p style={{ color: "var(--color-danger, #d92d20)", marginTop: 8 }}>
|
|
514
|
-
{error}
|
|
515
|
-
</p>
|
|
516
|
-
) : null}
|
|
717
|
+
{error ? <p className="settings-note settings-note--error kb-error">{error}</p> : null}
|
|
517
718
|
|
|
518
|
-
<div
|
|
519
|
-
<h4
|
|
520
|
-
<div
|
|
719
|
+
<div className="kb-block">
|
|
720
|
+
<h4 className="kb-block__title">{t("settings.kb.progress")}</h4>
|
|
721
|
+
<div className="kb-progress">
|
|
521
722
|
{STAGES.map((s) => {
|
|
522
723
|
const st = stages[s];
|
|
523
|
-
const
|
|
524
|
-
st.status === "done" ? "#16a34a" :
|
|
525
|
-
st.status === "error" ? "#d92d20" :
|
|
526
|
-
st.status === "running" ? "#2563eb" : "#cbd5e1";
|
|
724
|
+
const pct = Math.min(100, Math.max(0, st.percent));
|
|
527
725
|
return (
|
|
528
|
-
<div key={s}
|
|
529
|
-
<strong>{STAGE_LABELS[s]}</strong>
|
|
530
|
-
<div
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}}>
|
|
536
|
-
<div style={{
|
|
537
|
-
width: `${Math.min(100, Math.max(0, st.percent))}%`,
|
|
538
|
-
background: color,
|
|
539
|
-
height: "100%",
|
|
540
|
-
transition: "width 200ms linear",
|
|
541
|
-
}} />
|
|
726
|
+
<div key={s} className="kb-progress__row">
|
|
727
|
+
<strong className="kb-progress__label">{STAGE_LABELS[s]}</strong>
|
|
728
|
+
<div className="kb-progress__track" aria-hidden="true">
|
|
729
|
+
<span
|
|
730
|
+
className={`kb-progress__fill kb-progress__fill--${st.status}`}
|
|
731
|
+
style={{ width: `${pct}%` }}
|
|
732
|
+
/>
|
|
542
733
|
</div>
|
|
543
|
-
<span
|
|
734
|
+
<span className="kb-progress__pct">
|
|
544
735
|
{st.status === "done" ? "✓" : `${Math.round(st.percent)}%`}
|
|
545
736
|
</span>
|
|
546
|
-
{st.msg ?
|
|
547
|
-
<div style={{ gridColumn: "2 / -1", fontSize: 12, color: "#64748b" }}>
|
|
548
|
-
{st.msg}
|
|
549
|
-
</div>
|
|
550
|
-
) : null}
|
|
737
|
+
{st.msg ? <div className="kb-progress__msg">{st.msg}</div> : null}
|
|
551
738
|
</div>
|
|
552
739
|
);
|
|
553
740
|
})}
|
|
554
741
|
</div>
|
|
555
742
|
</div>
|
|
556
743
|
|
|
557
|
-
<div
|
|
558
|
-
<h4
|
|
559
|
-
<div
|
|
560
|
-
ref={logRef}
|
|
561
|
-
style={{
|
|
562
|
-
background: "#0f172a",
|
|
563
|
-
color: "#e2e8f0",
|
|
564
|
-
fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
565
|
-
fontSize: 12,
|
|
566
|
-
padding: 8,
|
|
567
|
-
borderRadius: 4,
|
|
568
|
-
maxHeight: 220,
|
|
569
|
-
overflowY: "auto",
|
|
570
|
-
whiteSpace: "pre-wrap",
|
|
571
|
-
wordBreak: "break-word",
|
|
572
|
-
}}
|
|
573
|
-
>
|
|
744
|
+
<div className="kb-block">
|
|
745
|
+
<h4 className="kb-block__title">{t("settings.kb.log")}</h4>
|
|
746
|
+
<div ref={logRef} className="kb-log">
|
|
574
747
|
{events.length === 0 ? (
|
|
575
|
-
<span
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
ev.event === "warn" ? "#fbbf24" :
|
|
582
|
-
ev.event === "done" ? "#86efac" :
|
|
583
|
-
ev.event === "progress" ? "#93c5fd" : "#e2e8f0";
|
|
584
|
-
return (
|
|
585
|
-
<div key={i} style={{ color }}>
|
|
586
|
-
[{ev.stage}:{ev.event}] {ev.msg}
|
|
587
|
-
</div>
|
|
588
|
-
);
|
|
589
|
-
})}
|
|
748
|
+
<span className="kb-log__empty">{t("settings.kb.logEmpty")}</span>
|
|
749
|
+
) : events.map((ev, i) => (
|
|
750
|
+
<div key={i} className={`kb-log__line kb-log__line--${ev.event}`}>
|
|
751
|
+
[{ev.stage}:{ev.event}] {ev.msg}
|
|
752
|
+
</div>
|
|
753
|
+
))}
|
|
590
754
|
</div>
|
|
591
755
|
</div>
|
|
592
756
|
</section>
|