@brainpilot/web 0.0.9 → 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__/api.test.ts +49 -1
- package/src/__tests__/messageGroups.test.ts +150 -0
- package/src/__tests__/newUiEvents.test.ts +32 -0
- package/src/__tests__/runningScripts.test.ts +139 -0
- package/src/components/chat/MessageStream.tsx +103 -43
- package/src/components/chat/PromptComposer.tsx +28 -10
- package/src/components/chat/RunningScriptsPanel.tsx +118 -0
- package/src/components/chat/runningScripts.ts +88 -0
- package/src/components/demo/DemoView.tsx +1 -1
- package/src/components/session/AgentTraceViews.tsx +5 -9
- package/src/components/settings/KnowledgeBasePanel.tsx +758 -0
- package/src/components/settings/SettingsDialog.tsx +127 -61
- 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 +14 -0
- package/src/i18n/messages/sandbox.ts +3 -0
- package/src/i18n/messages/settings.ts +93 -0
- package/src/i18n/messages/trace.ts +0 -2
- package/src/styles/global.css +970 -80
- package/src/utils/api.ts +188 -3
- package/src/utils/format.ts +9 -0
- package/dist/assets/index-CJNvdeGz.js +0 -445
- package/dist/assets/index-DWOsU22G.css +0 -1
package/src/utils/api.ts
CHANGED
|
@@ -66,13 +66,44 @@ function authHeaders(json = true): Record<string, string> {
|
|
|
66
66
|
return json ? { "Content-Type": "application/json" } : {};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* #206: build a readable message from a Zod issue list. The backend returns
|
|
71
|
+
* `details: parsed.error.issues` — each issue has a `path` (field) and a
|
|
72
|
+
* `message`. We render `field: message` per issue so a validation 400 tells the
|
|
73
|
+
* user *which* field is wrong (empty name, invalid url, …) instead of degrading
|
|
74
|
+
* to a generic "Request failed (400)".
|
|
75
|
+
*/
|
|
76
|
+
function formatIssues(details: unknown): string | null {
|
|
77
|
+
if (!Array.isArray(details) || details.length === 0) return null;
|
|
78
|
+
const parts: string[] = [];
|
|
79
|
+
for (const issue of details) {
|
|
80
|
+
if (!issue || typeof issue !== "object") continue;
|
|
81
|
+
const { path, message } = issue as { path?: unknown; message?: unknown };
|
|
82
|
+
if (typeof message !== "string" || message.length === 0) continue;
|
|
83
|
+
const field = Array.isArray(path) ? path.filter((p) => p !== "" && p != null).join(".") : "";
|
|
84
|
+
parts.push(field ? `${field}: ${message}` : message);
|
|
85
|
+
}
|
|
86
|
+
return parts.length > 0 ? parts.join("; ") : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
69
89
|
async function parseError(res: Response): Promise<string> {
|
|
70
90
|
const contentType = res.headers.get("content-type") || "";
|
|
71
91
|
if (contentType.includes("application/json")) {
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
// #206: the backend uses two shapes — `{ detail }` (single string) and the
|
|
93
|
+
// Zod validation shape `{ error, details }`. parseError previously read only
|
|
94
|
+
// `detail`, so every `{ error, details }` 400/409 fell through to the generic
|
|
95
|
+
// text fallback. Read all three: detail → error(+formatted details) → error.
|
|
96
|
+
const body = (await res.json().catch(() => null)) as
|
|
97
|
+
| { detail?: unknown; error?: unknown; details?: unknown }
|
|
98
|
+
| null;
|
|
99
|
+
if (typeof body?.detail === "string" && body.detail.length > 0) {
|
|
74
100
|
return body.detail;
|
|
75
101
|
}
|
|
102
|
+
const issues = formatIssues(body?.details);
|
|
103
|
+
if (typeof body?.error === "string" && body.error.length > 0) {
|
|
104
|
+
return issues ? `${body.error} (${issues})` : body.error;
|
|
105
|
+
}
|
|
106
|
+
if (issues) return issues;
|
|
76
107
|
}
|
|
77
108
|
const text = await res.text().catch(() => "");
|
|
78
109
|
return text || `Request failed (${res.status})`;
|
|
@@ -519,7 +550,14 @@ export const api = {
|
|
|
519
550
|
`${API_BASE}/sessions/${sessionId}/history${qs}`,
|
|
520
551
|
{ headers: authHeaders() },
|
|
521
552
|
);
|
|
522
|
-
|
|
553
|
+
// A 404 means the session has no transcript on disk (genuinely empty) —
|
|
554
|
+
// return an empty history. Any OTHER non-OK status is a real failure
|
|
555
|
+
// (routing / storage / auth); surface it instead of silently rendering an
|
|
556
|
+
// empty transcript, which historically masked broken rehydrates (#223).
|
|
557
|
+
if (res.status === 404) return { events: [], total: 0, truncated: false };
|
|
558
|
+
if (!res.ok) {
|
|
559
|
+
throw new Error(`history fetch failed: ${res.status} ${res.statusText}`);
|
|
560
|
+
}
|
|
523
561
|
const raw = (await res.json().catch(() => null)) as
|
|
524
562
|
| { events?: unknown[]; total?: number; truncated?: boolean }
|
|
525
563
|
| null;
|
|
@@ -735,4 +773,151 @@ export const api = {
|
|
|
735
773
|
return normalizeProviderProfile(raw as Parameters<typeof normalizeProviderProfile>[0]);
|
|
736
774
|
},
|
|
737
775
|
},
|
|
776
|
+
|
|
777
|
+
// Knowledge Base — local pipeline build orchestration.
|
|
778
|
+
// Mirrors the backend /api/kb/* routes. The SSE event stream is consumed
|
|
779
|
+
// directly via `new EventSource()` in the panel component (so it can stay
|
|
780
|
+
// attached for the lifetime of the dialog), so we don't expose a helper
|
|
781
|
+
// here for it.
|
|
782
|
+
kb: {
|
|
783
|
+
async build(opts: {
|
|
784
|
+
ocrApiKey?: string;
|
|
785
|
+
metaApiKey?: string;
|
|
786
|
+
metaBaseUrl?: string;
|
|
787
|
+
metaModel?: string;
|
|
788
|
+
kbRoot?: string;
|
|
789
|
+
ocrConcurrency?: number;
|
|
790
|
+
ocrLimit?: number;
|
|
791
|
+
skip?: Array<"ocr" | "extract" | "chunk" | "vectorize">;
|
|
792
|
+
only?: Array<"ocr" | "extract" | "chunk" | "vectorize">;
|
|
793
|
+
hfMirror?: string;
|
|
794
|
+
}): Promise<{ ok: boolean; startedAt?: number; error?: string }> {
|
|
795
|
+
const res = await apiFetch(`${API_BASE}/kb/build`, {
|
|
796
|
+
method: "POST",
|
|
797
|
+
headers: { "content-type": "application/json" },
|
|
798
|
+
body: JSON.stringify(opts),
|
|
799
|
+
});
|
|
800
|
+
if (!res.ok) {
|
|
801
|
+
const body = await res.json().catch(() => ({}));
|
|
802
|
+
return { ok: false, error: body.error || `kb build failed (${res.status})` };
|
|
803
|
+
}
|
|
804
|
+
return handleJson(res);
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
async status(): Promise<{
|
|
808
|
+
active: boolean;
|
|
809
|
+
startedAt: number | null;
|
|
810
|
+
finishedAt: number | null;
|
|
811
|
+
exitCode: number | null | undefined;
|
|
812
|
+
error?: string;
|
|
813
|
+
recentEvents: Array<{
|
|
814
|
+
ts: string;
|
|
815
|
+
stage: string;
|
|
816
|
+
event: string;
|
|
817
|
+
msg: string;
|
|
818
|
+
[k: string]: unknown;
|
|
819
|
+
}>;
|
|
820
|
+
environment: {
|
|
821
|
+
python: string;
|
|
822
|
+
pythonIsVenv: boolean;
|
|
823
|
+
venvExists: boolean;
|
|
824
|
+
expectedVenvPath: string;
|
|
825
|
+
scriptsPresent: boolean;
|
|
826
|
+
kbRoot: string;
|
|
827
|
+
};
|
|
828
|
+
}> {
|
|
829
|
+
return handleJson(await apiFetch(`${API_BASE}/kb/status`));
|
|
830
|
+
},
|
|
831
|
+
|
|
832
|
+
async cancel(): Promise<{ ok: boolean; message?: string }> {
|
|
833
|
+
const res = await apiFetch(`${API_BASE}/kb/cancel`, { method: "POST" });
|
|
834
|
+
return res.json().catch(() => ({ ok: false }));
|
|
835
|
+
},
|
|
836
|
+
|
|
837
|
+
// Bootstrap the Python venv (KnowledgeBase/.venv) before the first
|
|
838
|
+
// build can run. Streams progress on the same SSE channel as `build`,
|
|
839
|
+
// so the panel only needs one EventSource subscription.
|
|
840
|
+
async setupEnv(opts: {
|
|
841
|
+
python?: string;
|
|
842
|
+
reinstall?: boolean;
|
|
843
|
+
kbRoot?: string;
|
|
844
|
+
} = {}): Promise<{ ok: boolean; startedAt?: number; error?: string }> {
|
|
845
|
+
const res = await apiFetch(`${API_BASE}/kb/setup-env`, {
|
|
846
|
+
method: "POST",
|
|
847
|
+
headers: { "content-type": "application/json" },
|
|
848
|
+
body: JSON.stringify(opts),
|
|
849
|
+
});
|
|
850
|
+
if (!res.ok) {
|
|
851
|
+
const body = await res.json().catch(() => ({}));
|
|
852
|
+
return { ok: false, error: body.error || `setup-env failed (${res.status})` };
|
|
853
|
+
}
|
|
854
|
+
return handleJson(res);
|
|
855
|
+
},
|
|
856
|
+
|
|
857
|
+
// Download bge-m3 + bge-reranker-v2-m3 model weights (~2.5 GB) via
|
|
858
|
+
// `scripts/setup_models.py`. Independent slot from setupEnv — the two
|
|
859
|
+
// can run concurrently, and setupFull() chains them.
|
|
860
|
+
async setupModels(opts: {
|
|
861
|
+
hfMirror?: string;
|
|
862
|
+
kbRoot?: string;
|
|
863
|
+
} = {}): Promise<{ ok: boolean; startedAt?: number; error?: string }> {
|
|
864
|
+
const res = await apiFetch(`${API_BASE}/kb/setup-models`, {
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers: { "content-type": "application/json" },
|
|
867
|
+
body: JSON.stringify(opts),
|
|
868
|
+
});
|
|
869
|
+
if (!res.ok) {
|
|
870
|
+
const body = await res.json().catch(() => ({}));
|
|
871
|
+
return { ok: false, error: body.error || `setup-models failed (${res.status})` };
|
|
872
|
+
}
|
|
873
|
+
return handleJson(res);
|
|
874
|
+
},
|
|
875
|
+
|
|
876
|
+
// One-click orchestration: create venv, then download models when venv
|
|
877
|
+
// exits 0. Preferred entry point from the KB panel — one button, two
|
|
878
|
+
// progress rows in the UI.
|
|
879
|
+
async setupFull(opts: {
|
|
880
|
+
python?: string;
|
|
881
|
+
reinstall?: boolean;
|
|
882
|
+
hfMirror?: string;
|
|
883
|
+
kbRoot?: string;
|
|
884
|
+
} = {}): Promise<{ ok: boolean; startedAt?: number; error?: string }> {
|
|
885
|
+
const res = await apiFetch(`${API_BASE}/kb/setup-full`, {
|
|
886
|
+
method: "POST",
|
|
887
|
+
headers: { "content-type": "application/json" },
|
|
888
|
+
body: JSON.stringify(opts),
|
|
889
|
+
});
|
|
890
|
+
if (!res.ok) {
|
|
891
|
+
const body = await res.json().catch(() => ({}));
|
|
892
|
+
return { ok: false, error: body.error || `setup-full failed (${res.status})` };
|
|
893
|
+
}
|
|
894
|
+
return handleJson(res);
|
|
895
|
+
},
|
|
896
|
+
|
|
897
|
+
// Persisted KB API config (SiliconFlow OCR key today). Backend never
|
|
898
|
+
// returns the plaintext — only a masked preview + boolean — so the
|
|
899
|
+
// browser can indicate "already saved" without ever holding the secret.
|
|
900
|
+
async getApiConfig(): Promise<{ hasOcrApiKey: boolean; ocrApiKeyPreview: string }> {
|
|
901
|
+
const res = await apiFetch(`${API_BASE}/kb/api-config`);
|
|
902
|
+
if (!res.ok) return { hasOcrApiKey: false, ocrApiKeyPreview: "" };
|
|
903
|
+
return handleJson(res);
|
|
904
|
+
},
|
|
905
|
+
|
|
906
|
+
async saveApiConfig(patch: { ocrApiKey?: string }): Promise<{ ok: boolean; error?: string }> {
|
|
907
|
+
const res = await apiFetch(`${API_BASE}/kb/api-config`, {
|
|
908
|
+
method: "PUT",
|
|
909
|
+
headers: { "content-type": "application/json" },
|
|
910
|
+
body: JSON.stringify(patch),
|
|
911
|
+
});
|
|
912
|
+
if (!res.ok) {
|
|
913
|
+
const body = await res.json().catch(() => ({}));
|
|
914
|
+
return { ok: false, error: body.error || `save failed (${res.status})` };
|
|
915
|
+
}
|
|
916
|
+
return handleJson(res);
|
|
917
|
+
},
|
|
918
|
+
|
|
919
|
+
eventsUrl(): string {
|
|
920
|
+
return `${API_BASE}/kb/events`;
|
|
921
|
+
},
|
|
922
|
+
},
|
|
738
923
|
};
|
package/src/utils/format.ts
CHANGED
|
@@ -5,3 +5,12 @@ export function formatBytes(bytes: number) {
|
|
|
5
5
|
const value = bytes / 1024 ** index;
|
|
6
6
|
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** Compact elapsed formatter: "3.2s" under a minute, "1m 05s" above. */
|
|
10
|
+
export function formatElapsed(ms: number): string {
|
|
11
|
+
const totalSeconds = Math.max(0, ms) / 1000;
|
|
12
|
+
if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
|
|
13
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
14
|
+
const seconds = Math.floor(totalSeconds % 60);
|
|
15
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
16
|
+
}
|