@schlessera/brain-ui-react 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +25 -0
  2. package/dist/components/chat/brain-markdown.js +2 -2
  3. package/dist/components/chat/brain-markdown.js.map +1 -1
  4. package/dist/components/files/file-viewer-binary.js +2 -2
  5. package/dist/components/files/file-viewer-binary.js.map +1 -1
  6. package/dist/components/files/file-viewer.js +2 -2
  7. package/dist/components/files/file-viewer.js.map +1 -1
  8. package/dist/components/graph/graph-page.js +3 -3
  9. package/dist/components/graph/graph-page.js.map +1 -1
  10. package/dist/components/images/mask-editor.js +2 -2
  11. package/dist/components/images/mask-editor.js.map +1 -1
  12. package/dist/config.d.ts +37 -4
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/config.js +29 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/index.d.ts +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/lib/api-client.js +2 -2
  21. package/dist/lib/api-client.js.map +1 -1
  22. package/dist/lib/backend.d.ts +3 -3
  23. package/dist/lib/backend.d.ts.map +1 -1
  24. package/dist/lib/backend.js +16 -15
  25. package/dist/lib/backend.js.map +1 -1
  26. package/dist/lib/share-intake.js +2 -2
  27. package/dist/lib/share-intake.js.map +1 -1
  28. package/dist/lib/share.js +2 -2
  29. package/dist/lib/share.js.map +1 -1
  30. package/dist/stores/chat-store.d.ts.map +1 -1
  31. package/dist/stores/chat-store.js +9 -5
  32. package/dist/stores/chat-store.js.map +1 -1
  33. package/dist/stores/file-store.js +5 -5
  34. package/dist/stores/file-store.js.map +1 -1
  35. package/dist/stores/graph-store.d.ts.map +1 -1
  36. package/dist/stores/graph-store.js +8 -7
  37. package/dist/stores/graph-store.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/components/chat/brain-markdown.tsx +2 -2
  40. package/src/components/files/file-viewer-binary.tsx +2 -2
  41. package/src/components/files/file-viewer.tsx +2 -2
  42. package/src/components/graph/graph-page.tsx +3 -3
  43. package/src/components/images/mask-editor.tsx +2 -2
  44. package/src/config.ts +63 -4
  45. package/src/index.ts +1 -1
  46. package/src/lib/api-client.ts +2 -2
  47. package/src/lib/backend.ts +16 -15
  48. package/src/lib/share-intake.ts +2 -2
  49. package/src/lib/share.ts +2 -2
  50. package/src/stores/chat-store.ts +8 -5
  51. package/src/stores/file-store.ts +5 -5
  52. package/src/stores/graph-store.ts +7 -7
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from "react";
2
2
  import { Eraser, RotateCcw, X } from "lucide-react";
3
3
 
4
- import { API_BASE } from "../../lib/backend.js";
4
+ import { apiBase } from "../../lib/backend.js";
5
5
  import { useMaskStore } from "../../stores/mask-store.js";
6
6
 
7
7
  /**
@@ -43,7 +43,7 @@ export function MaskEditor({
43
43
  const [error, setError] = useState<string | null>(null);
44
44
 
45
45
  const rawUrl = request
46
- ? `${API_BASE}/files/content?path=${encodeURIComponent(request.imagePath)}&raw=1`
46
+ ? `${apiBase()}/files/content?path=${encodeURIComponent(request.imagePath)}&raw=1`
47
47
  : null;
48
48
 
49
49
  // Reset per request: a second mask on a different image must not inherit the
package/src/config.ts CHANGED
@@ -1,8 +1,21 @@
1
1
  /**
2
- * Deployment-tunable branding/copy. A module-level singleton, matching the
3
- * renderer/ASR registries: the shell calls `configureBrainUi()` once at boot
4
- * (before mounting), components read `uiConfig` at render time. Runtime
5
- * plugin-style reconfiguration is deliberately unsupported.
2
+ * The package's single configuration chokepoint.
3
+ *
4
+ * `ui-server` resolves its configuration once, at the edge, in `createApp()`,
5
+ * and nothing deeper in that package touches the ambient environment. This is
6
+ * the browser-side mirror of that rule: the deployment shell calls
7
+ * `configureBrainUi()` once at boot, before the first render, and every module
8
+ * below reads the resolved values instead of reaching for build-tool globals.
9
+ *
10
+ * The rule exists because a library that reads `import.meta.env` pins its
11
+ * consumers to one bundler. `VITE_BACKEND_URL` used to be read here at module
12
+ * load, so a webpack or Next.js consumer had no way to point the client at a
13
+ * split-topology backend at all, and no way to discover that from the types.
14
+ * `scripts/check-env-access.ts` now refuses `import.meta.env` anywhere in a
15
+ * package's `src`, so the loophole cannot reopen.
16
+ *
17
+ * Values are a module-level singleton, matching the renderer/ASR registries.
18
+ * Runtime plugin-style reconfiguration is deliberately unsupported.
6
19
  */
7
20
  export interface BrainUiConfig {
8
21
  /** Product name shown on the login screen and connection status. */
@@ -13,6 +26,24 @@ export interface BrainUiConfig {
13
26
  shareTitle: string;
14
27
  /** Composer placeholder. */
15
28
  composerPlaceholder: string;
29
+ /**
30
+ * Origin of the API/WebSocket backend, for a SPLIT topology (client and
31
+ * backend on different origins, e.g. a public frontend reaching its backend
32
+ * over a VPN). Empty — the default — means SAME-ORIGIN: API calls go to
33
+ * `/api` and the WebSocket derives its host from `window.location`.
34
+ *
35
+ * The shell resolves this however it likes (a Vite `VITE_*` define, a
36
+ * `<meta>` tag, a runtime fetch) and passes the result in. A trailing slash
37
+ * is stripped.
38
+ */
39
+ backendUrl: string;
40
+ /**
41
+ * Install the `window.__chatStore` / `window.__graphStore` debug handles,
42
+ * which let browser automation inject fixture messages without a live agent
43
+ * session. The shell decides what "development" means — this package must
44
+ * not infer it from a bundler's DEV flag.
45
+ */
46
+ devTools: boolean;
16
47
  }
17
48
 
18
49
  export const uiConfig: BrainUiConfig = {
@@ -20,8 +51,36 @@ export const uiConfig: BrainUiConfig = {
20
51
  assistantName: "Brain",
21
52
  shareTitle: "Shared from Brain",
22
53
  composerPlaceholder: "Ask your brain anything...",
54
+ backendUrl: "",
55
+ devTools: false,
23
56
  };
24
57
 
58
+ /**
59
+ * Dev-handle installers, registered at module scope by the stores that own a
60
+ * handle. They cannot read `uiConfig.devTools` themselves: ES imports are
61
+ * hoisted, so a store's module body runs BEFORE the shell's
62
+ * `configureBrainUi()` call. Registering instead of reading lets the flag
63
+ * arrive late and still take effect.
64
+ */
65
+ const devHandleInstallers: Array<() => void> = [];
66
+ let devHandlesInstalled = false;
67
+
68
+ function installDevHandles(): void {
69
+ if (devHandlesInstalled) return;
70
+ devHandlesInstalled = true;
71
+ for (const install of devHandleInstallers) install();
72
+ }
73
+
74
+ /** Register a debug handle to install if (and when) `devTools` is turned on. */
75
+ export function registerDevHandle(install: () => void): void {
76
+ devHandleInstallers.push(install);
77
+ if (uiConfig.devTools) install();
78
+ }
79
+
25
80
  export function configureBrainUi(overrides: Partial<BrainUiConfig>): void {
26
81
  Object.assign(uiConfig, overrides);
82
+ if (typeof overrides.backendUrl === "string") {
83
+ uiConfig.backendUrl = overrides.backendUrl.replace(/\/$/, "");
84
+ }
85
+ if (uiConfig.devTools) installDevHandles();
27
86
  }
package/src/index.ts CHANGED
@@ -72,4 +72,4 @@ export { ShareIntake } from "./components/chat/share-card.js";
72
72
 
73
73
  // API surface (typed REST client + backend URL helpers).
74
74
  export { api } from "./lib/api-client.js";
75
- export { API_BASE, getWsUrl, getBackendUrl } from "./lib/backend.js";
75
+ export { apiBase, getWsUrl, getBackendUrl } from "./lib/backend.js";
@@ -1,4 +1,4 @@
1
- import { API_BASE } from "./backend.js";
1
+ import { apiBase } from "./backend.js";
2
2
  import type {
3
3
  VoiceKeytermsResponse,
4
4
  VoiceTokenResponse,
@@ -46,7 +46,7 @@ export interface BackendInfo {
46
46
  }
47
47
 
48
48
  async function fetchJson<T>(path: string, init?: RequestInit): Promise<T> {
49
- const res = await fetch(`${API_BASE}${path}`, {
49
+ const res = await fetch(`${apiBase()}${path}`, {
50
50
  ...init,
51
51
  headers: {
52
52
  "Content-Type": "application/json",
@@ -1,26 +1,27 @@
1
1
  /**
2
- * Backend URL configuration.
2
+ * Where the API and WebSocket live.
3
3
  *
4
4
  * Default topology is SAME-ORIGIN: the server serves the built client and the
5
5
  * API/WS from one origin, so API calls go to "/api" and the WebSocket derives
6
- * its host from window.location. No build-time configuration is needed.
6
+ * its host from window.location. Nothing needs configuring.
7
7
  *
8
- * VITE_BACKEND_URL is optional advanced config for a SPLIT topology (client and
9
- * backend on different origins, e.g. a public frontend with the backend reached
10
- * over a VPN). When set at build time, API and WS calls target that origin.
11
- * The read is defensive — outside a Vite build (bun test, Node import of the
12
- * dist) `import.meta.env` does not exist.
8
+ * A SPLIT topology (client and backend on different origins) sets
9
+ * `backendUrl` through `configureBrainUi()`. These are functions rather than
10
+ * module constants on purpose: a constant would freeze the value at import
11
+ * time, and ES imports are hoisted, so it would always capture the default
12
+ * instead of what the shell configured.
13
13
  */
14
- const viteEnv = (import.meta as { env?: Record<string, string | undefined> }).env;
15
- const BACKEND_URL = viteEnv?.VITE_BACKEND_URL?.replace(/\/$/, "") ?? "";
14
+ import { uiConfig } from "../config.js";
16
15
 
17
- /** Base URL for API calls. Empty BACKEND_URL = same-origin "/api". */
18
- export const API_BASE = `${BACKEND_URL}/api`;
16
+ /** Base URL for API calls. Empty backendUrl = same-origin "/api". */
17
+ export function apiBase(): string {
18
+ return `${uiConfig.backendUrl}/api`;
19
+ }
19
20
 
20
- /** WebSocket URL. Derives wss/ws + host from BACKEND_URL, else same-origin. */
21
+ /** WebSocket URL. Derives wss/ws + host from backendUrl, else same-origin. */
21
22
  export function getWsUrl(): string {
22
- if (BACKEND_URL) {
23
- const url = new URL("/ws", BACKEND_URL);
23
+ if (uiConfig.backendUrl) {
24
+ const url = new URL("/ws", uiConfig.backendUrl);
24
25
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
25
26
  return url.toString();
26
27
  }
@@ -31,5 +32,5 @@ export function getWsUrl(): string {
31
32
 
32
33
  /** Base URL for direct fetch calls (streaming endpoints). */
33
34
  export function getBackendUrl(path: string): string {
34
- return `${BACKEND_URL}${path}`;
35
+ return `${uiConfig.backendUrl}${path}`;
35
36
  }
@@ -4,7 +4,7 @@ import {
4
4
  type ShareIntakeResult,
5
5
  } from "@schlessera/brain-ui-sdk/protocol";
6
6
  import type { StoredShare } from "@schlessera/brain-ui-sdk/share-target";
7
- import { API_BASE } from "./backend.js";
7
+ import { apiBase } from "./backend.js";
8
8
  import {
9
9
  fileToAttachment,
10
10
  validateAttachments,
@@ -85,7 +85,7 @@ export async function uploadShare(
85
85
 
86
86
  let response: Response;
87
87
  try {
88
- response = await fetchImpl(`${API_BASE}/share`, {
88
+ response = await fetchImpl(`${apiBase()}/share`, {
89
89
  method: "POST",
90
90
  body: form,
91
91
  });
package/src/lib/share.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { API_BASE } from "./backend.js";
1
+ import { apiBase } from "./backend.js";
2
2
  import type { RenderRequest } from "@schlessera/brain-ui-sdk/protocol";
3
3
 
4
4
  export type ShareKind = "file" | "text" | "richtext";
@@ -125,7 +125,7 @@ export async function renderToFile(
125
125
  req: RenderRequest,
126
126
  filename: string
127
127
  ): Promise<File> {
128
- const res = await fetch(`${API_BASE}/render`, {
128
+ const res = await fetch(`${apiBase()}/render`, {
129
129
  method: "POST",
130
130
  headers: { "Content-Type": "application/json" },
131
131
  body: JSON.stringify(req),
@@ -6,6 +6,7 @@ import type {
6
6
  AskUserAnnotation,
7
7
  } from "@schlessera/brain-ui-sdk/protocol";
8
8
  import { useProviderStore } from "./provider-store.js";
9
+ import { registerDevHandle } from "../config.js";
9
10
 
10
11
  export type { MessagePart };
11
12
 
@@ -631,10 +632,12 @@ export const useChatStore = create<ChatState>((set, get) => {
631
632
  });
632
633
 
633
634
  // Dev-only handle so browser automation / manual debugging can inject
634
- // fixture messages without a live Claude session. The env read is defensive:
635
- // outside a Vite build `import.meta.env` does not exist.
636
- const devEnv = (import.meta as { env?: Record<string, unknown> }).env;
637
- if (typeof window !== "undefined" && devEnv?.DEV) {
635
+ // fixture messages without a live Claude session. Registered rather than
636
+ // installed: whether this is a development build is the shell's call
637
+ // (`configureBrainUi({ devTools: true })`), not something a component library
638
+ // infers from its bundler.
639
+ registerDevHandle(() => {
640
+ if (typeof window === "undefined") return;
638
641
  (window as unknown as { __chatStore?: typeof useChatStore }).__chatStore =
639
642
  useChatStore;
640
- }
643
+ });
@@ -6,7 +6,7 @@ import type {
6
6
  WikilinkMapResponse,
7
7
  } from "@schlessera/brain-ui-sdk/protocol";
8
8
  import { FILE_SIZE_CAP_BYTES } from "@schlessera/brain-ui-sdk/protocol";
9
- import { API_BASE } from "../lib/backend.js";
9
+ import { apiBase } from "../lib/backend.js";
10
10
  import { isMermaidPath } from "../lib/mermaid.js";
11
11
 
12
12
  export type ViewMode = "preview" | "raw";
@@ -76,7 +76,7 @@ interface FileState {
76
76
  }
77
77
 
78
78
  async function fetchTree(path: string): Promise<FileEntry[]> {
79
- const url = `${API_BASE}/files/tree${path ? `?path=${encodeURIComponent(path)}` : ""}`;
79
+ const url = `${apiBase()}/files/tree${path ? `?path=${encodeURIComponent(path)}` : ""}`;
80
80
  const res = await fetch(url);
81
81
  if (!res.ok) {
82
82
  const body = await res.json().catch(() => ({ error: res.statusText }));
@@ -87,7 +87,7 @@ async function fetchTree(path: string): Promise<FileEntry[]> {
87
87
  }
88
88
 
89
89
  async function fetchContent(path: string): Promise<FileContentResponse> {
90
- const res = await fetch(`${API_BASE}/files/content?path=${encodeURIComponent(path)}`);
90
+ const res = await fetch(`${apiBase()}/files/content?path=${encodeURIComponent(path)}`);
91
91
  if (!res.ok) {
92
92
  const body = await res.json().catch(() => ({ error: res.statusText }));
93
93
  const err = new Error(body.error || `HTTP ${res.status}`);
@@ -99,7 +99,7 @@ async function fetchContent(path: string): Promise<FileContentResponse> {
99
99
  }
100
100
 
101
101
  async function fetchResolve(path: string): Promise<FileResolveResponse> {
102
- const res = await fetch(`${API_BASE}/files/resolve?path=${encodeURIComponent(path)}`);
102
+ const res = await fetch(`${apiBase()}/files/resolve?path=${encodeURIComponent(path)}`);
103
103
  if (!res.ok) {
104
104
  const body = await res.json().catch(() => ({ error: res.statusText }));
105
105
  throw new Error(body.error || `HTTP ${res.status}`);
@@ -108,7 +108,7 @@ async function fetchResolve(path: string): Promise<FileResolveResponse> {
108
108
  }
109
109
 
110
110
  async function fetchWikilinks(): Promise<WikilinkMapResponse> {
111
- const res = await fetch(`${API_BASE}/files/wikilinks`);
111
+ const res = await fetch(`${apiBase()}/files/wikilinks`);
112
112
  if (!res.ok) {
113
113
  const body = await res.json().catch(() => ({ error: res.statusText }));
114
114
  throw new Error(body.error || `HTTP ${res.status}`);
@@ -4,7 +4,8 @@ import type {
4
4
  GraphSubgraphResponse,
5
5
  GraphMaintenanceResponse,
6
6
  } from "@schlessera/brain-ui-sdk/protocol";
7
- import { API_BASE } from "../lib/backend.js";
7
+ import { apiBase } from "../lib/backend.js";
8
+ import { registerDevHandle } from "../config.js";
8
9
  import { buildQuery, mergeSubgraphs } from "../components/graph/lib/graph-helpers.js";
9
10
 
10
11
  export type GraphMode = "clusters" | "discovery" | "local" | "maintenance";
@@ -135,7 +136,7 @@ function cachePut(key: string, value: GraphSubgraphResponse | GraphMaintenanceRe
135
136
  }
136
137
 
137
138
  async function fetchGraphJson<T>(pathAndQuery: string): Promise<T> {
138
- const res = await fetch(`${API_BASE}/graph${pathAndQuery}`);
139
+ const res = await fetch(`${apiBase()}/graph${pathAndQuery}`);
139
140
  if (!res.ok) {
140
141
  const body = await res.json().catch(() => ({}) as Record<string, unknown>);
141
142
  const err = new Error(
@@ -336,10 +337,9 @@ export const useGraphStore = create<GraphState>((set, get) => ({
336
337
  }));
337
338
 
338
339
  // Dev-only handle for exercising the view with injected fixtures (the
339
- // window.__chatStore precedent). The env read is defensive: outside a Vite
340
- // build `import.meta.env` does not exist.
341
- const devEnv = (import.meta as { env?: Record<string, unknown> }).env;
342
- if (typeof window !== "undefined" && devEnv?.DEV) {
340
+ // window.__chatStore precedent), gated on the shell's `devTools` flag.
341
+ registerDevHandle(() => {
342
+ if (typeof window === "undefined") return;
343
343
  (window as unknown as { __graphStore?: typeof useGraphStore }).__graphStore =
344
344
  useGraphStore;
345
- }
345
+ });