@schlessera/brain-ui-react 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +70 -0
- package/dist/components/chat/ask-user-card.d.ts +12 -0
- package/dist/components/chat/ask-user-card.d.ts.map +1 -0
- package/dist/components/chat/ask-user-card.js +185 -0
- package/dist/components/chat/ask-user-card.js.map +1 -0
- package/dist/components/chat/brain-markdown.d.ts +35 -0
- package/dist/components/chat/brain-markdown.d.ts.map +1 -0
- package/dist/components/chat/brain-markdown.js +363 -0
- package/dist/components/chat/brain-markdown.js.map +1 -0
- package/dist/components/chat/chat-page.d.ts +2 -0
- package/dist/components/chat/chat-page.d.ts.map +1 -0
- package/dist/components/chat/chat-page.js +442 -0
- package/dist/components/chat/chat-page.js.map +1 -0
- package/dist/components/chat/command-palette.d.ts +5 -0
- package/dist/components/chat/command-palette.d.ts.map +1 -0
- package/dist/components/chat/command-palette.js +18 -0
- package/dist/components/chat/command-palette.js.map +1 -0
- package/dist/components/chat/markdown-content.d.ts +4 -0
- package/dist/components/chat/markdown-content.d.ts.map +1 -0
- package/dist/components/chat/markdown-content.js +6 -0
- package/dist/components/chat/markdown-content.js.map +1 -0
- package/dist/components/chat/message-bubble.d.ts +9 -0
- package/dist/components/chat/message-bubble.d.ts.map +1 -0
- package/dist/components/chat/message-bubble.js +129 -0
- package/dist/components/chat/message-bubble.js.map +1 -0
- package/dist/components/chat/message-share.d.ts +10 -0
- package/dist/components/chat/message-share.d.ts.map +1 -0
- package/dist/components/chat/message-share.js +65 -0
- package/dist/components/chat/message-share.js.map +1 -0
- package/dist/components/chat/renderers/claude-tools.d.ts +3 -0
- package/dist/components/chat/renderers/claude-tools.d.ts.map +1 -0
- package/dist/components/chat/renderers/claude-tools.js +38 -0
- package/dist/components/chat/renderers/claude-tools.js.map +1 -0
- package/dist/components/chat/renderers/generic.d.ts +9 -0
- package/dist/components/chat/renderers/generic.d.ts.map +1 -0
- package/dist/components/chat/renderers/generic.js +68 -0
- package/dist/components/chat/renderers/generic.js.map +1 -0
- package/dist/components/chat/renderers/index.d.ts +5 -0
- package/dist/components/chat/renderers/index.d.ts.map +1 -0
- package/dist/components/chat/renderers/index.js +17 -0
- package/dist/components/chat/renderers/index.js.map +1 -0
- package/dist/components/chat/risk-hints.d.ts +20 -0
- package/dist/components/chat/risk-hints.d.ts.map +1 -0
- package/dist/components/chat/risk-hints.js +81 -0
- package/dist/components/chat/risk-hints.js.map +1 -0
- package/dist/components/chat/session-drawer.d.ts +6 -0
- package/dist/components/chat/session-drawer.d.ts.map +1 -0
- package/dist/components/chat/session-drawer.js +96 -0
- package/dist/components/chat/session-drawer.js.map +1 -0
- package/dist/components/chat/share-block.d.ts +9 -0
- package/dist/components/chat/share-block.d.ts.map +1 -0
- package/dist/components/chat/share-block.js +70 -0
- package/dist/components/chat/share-block.js.map +1 -0
- package/dist/components/chat/tool-call-timeline.d.ts +8 -0
- package/dist/components/chat/tool-call-timeline.d.ts.map +1 -0
- package/dist/components/chat/tool-call-timeline.js +100 -0
- package/dist/components/chat/tool-call-timeline.js.map +1 -0
- package/dist/components/chat/tool-views.d.ts +116 -0
- package/dist/components/chat/tool-views.d.ts.map +1 -0
- package/dist/components/chat/tool-views.js +628 -0
- package/dist/components/chat/tool-views.js.map +1 -0
- package/dist/components/chat/welcome-state.d.ts +4 -0
- package/dist/components/chat/welcome-state.d.ts.map +1 -0
- package/dist/components/chat/welcome-state.js +10 -0
- package/dist/components/chat/welcome-state.js.map +1 -0
- package/dist/components/connectivity/connection-gate.d.ts +5 -0
- package/dist/components/connectivity/connection-gate.d.ts.map +1 -0
- package/dist/components/connectivity/connection-gate.js +42 -0
- package/dist/components/connectivity/connection-gate.js.map +1 -0
- package/dist/components/connectivity/login-screen.d.ts +9 -0
- package/dist/components/connectivity/login-screen.d.ts.map +1 -0
- package/dist/components/connectivity/login-screen.js +99 -0
- package/dist/components/connectivity/login-screen.js.map +1 -0
- package/dist/components/files/file-panel.d.ts +5 -0
- package/dist/components/files/file-panel.d.ts.map +1 -0
- package/dist/components/files/file-panel.js +46 -0
- package/dist/components/files/file-panel.js.map +1 -0
- package/dist/components/files/file-tree.d.ts +2 -0
- package/dist/components/files/file-tree.d.ts.map +1 -0
- package/dist/components/files/file-tree.js +63 -0
- package/dist/components/files/file-tree.js.map +1 -0
- package/dist/components/files/file-viewer-binary.d.ts +5 -0
- package/dist/components/files/file-viewer-binary.d.ts.map +1 -0
- package/dist/components/files/file-viewer-binary.js +20 -0
- package/dist/components/files/file-viewer-binary.js.map +1 -0
- package/dist/components/files/file-viewer-html.d.ts +4 -0
- package/dist/components/files/file-viewer-html.d.ts.map +1 -0
- package/dist/components/files/file-viewer-html.js +5 -0
- package/dist/components/files/file-viewer-html.js.map +1 -0
- package/dist/components/files/file-viewer-markdown.d.ts +4 -0
- package/dist/components/files/file-viewer-markdown.d.ts.map +1 -0
- package/dist/components/files/file-viewer-markdown.js +10 -0
- package/dist/components/files/file-viewer-markdown.js.map +1 -0
- package/dist/components/files/file-viewer-raw.d.ts +5 -0
- package/dist/components/files/file-viewer-raw.d.ts.map +1 -0
- package/dist/components/files/file-viewer-raw.js +15 -0
- package/dist/components/files/file-viewer-raw.js.map +1 -0
- package/dist/components/files/file-viewer.d.ts +2 -0
- package/dist/components/files/file-viewer.d.ts.map +1 -0
- package/dist/components/files/file-viewer.js +175 -0
- package/dist/components/files/file-viewer.js.map +1 -0
- package/dist/components/files/frontmatter-panel.d.ts +5 -0
- package/dist/components/files/frontmatter-panel.d.ts.map +1 -0
- package/dist/components/files/frontmatter-panel.js +12 -0
- package/dist/components/files/frontmatter-panel.js.map +1 -0
- package/dist/components/layout/app-shell.d.ts +5 -0
- package/dist/components/layout/app-shell.d.ts.map +1 -0
- package/dist/components/layout/app-shell.js +10 -0
- package/dist/components/layout/app-shell.js.map +1 -0
- package/dist/components/layout/mobile-tab-bar.d.ts +2 -0
- package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -0
- package/dist/components/layout/mobile-tab-bar.js +16 -0
- package/dist/components/layout/mobile-tab-bar.js.map +1 -0
- package/dist/components/layout/side-rail.d.ts +2 -0
- package/dist/components/layout/side-rail.d.ts.map +1 -0
- package/dist/components/layout/side-rail.js +26 -0
- package/dist/components/layout/side-rail.js.map +1 -0
- package/dist/components/layout/slide-panel.d.ts +9 -0
- package/dist/components/layout/slide-panel.d.ts.map +1 -0
- package/dist/components/layout/slide-panel.js +18 -0
- package/dist/components/layout/slide-panel.js.map +1 -0
- package/dist/components/quick-actions/streaming-modal.d.ts +8 -0
- package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/streaming-modal.js +92 -0
- package/dist/components/quick-actions/streaming-modal.js.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts +5 -0
- package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -0
- package/dist/components/quick-actions/whatsup-modal.js +82 -0
- package/dist/components/quick-actions/whatsup-modal.js.map +1 -0
- package/dist/components/settings/passkey-panel.d.ts +10 -0
- package/dist/components/settings/passkey-panel.d.ts.map +1 -0
- package/dist/components/settings/passkey-panel.js +118 -0
- package/dist/components/settings/passkey-panel.js.map +1 -0
- package/dist/components/share/share-menu.d.ts +23 -0
- package/dist/components/share/share-menu.d.ts.map +1 -0
- package/dist/components/share/share-menu.js +58 -0
- package/dist/components/share/share-menu.js.map +1 -0
- package/dist/components/voice/dictation-sheet.d.ts +6 -0
- package/dist/components/voice/dictation-sheet.d.ts.map +1 -0
- package/dist/components/voice/dictation-sheet.js +91 -0
- package/dist/components/voice/dictation-sheet.js.map +1 -0
- package/dist/components/voice/mic-button.d.ts +6 -0
- package/dist/components/voice/mic-button.d.ts.map +1 -0
- package/dist/components/voice/mic-button.js +9 -0
- package/dist/components/voice/mic-button.js.map +1 -0
- package/dist/components/voice/review-card.d.ts +8 -0
- package/dist/components/voice/review-card.d.ts.map +1 -0
- package/dist/components/voice/review-card.js +8 -0
- package/dist/components/voice/review-card.js.map +1 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +10 -0
- package/dist/config.js.map +1 -0
- package/dist/hooks/use-vpn-status.d.ts +2 -0
- package/dist/hooks/use-vpn-status.d.ts.map +1 -0
- package/dist/hooks/use-vpn-status.js +71 -0
- package/dist/hooks/use-vpn-status.js.map +1 -0
- package/dist/hooks/use-websocket.d.ts +8 -0
- package/dist/hooks/use-websocket.d.ts.map +1 -0
- package/dist/hooks/use-websocket.js +366 -0
- package/dist/hooks/use-websocket.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/api-client.d.ts +115 -0
- package/dist/lib/api-client.d.ts.map +1 -0
- package/dist/lib/api-client.js +70 -0
- package/dist/lib/api-client.js.map +1 -0
- package/dist/lib/backend.d.ts +7 -0
- package/dist/lib/backend.d.ts.map +1 -0
- package/dist/lib/backend.js +33 -0
- package/dist/lib/backend.js.map +1 -0
- package/dist/lib/frontmatter.d.ts +21 -0
- package/dist/lib/frontmatter.d.ts.map +1 -0
- package/dist/lib/frontmatter.js +88 -0
- package/dist/lib/frontmatter.js.map +1 -0
- package/dist/lib/image-attachments.d.ts +36 -0
- package/dist/lib/image-attachments.d.ts.map +1 -0
- package/dist/lib/image-attachments.js +175 -0
- package/dist/lib/image-attachments.js.map +1 -0
- package/dist/lib/passkeys.d.ts +18 -0
- package/dist/lib/passkeys.d.ts.map +1 -0
- package/dist/lib/passkeys.js +36 -0
- package/dist/lib/passkeys.js.map +1 -0
- package/dist/lib/share.d.ts +53 -0
- package/dist/lib/share.d.ts.map +1 -0
- package/dist/lib/share.js +144 -0
- package/dist/lib/share.js.map +1 -0
- package/dist/lib/strip-markdown.d.ts +10 -0
- package/dist/lib/strip-markdown.d.ts.map +1 -0
- package/dist/lib/strip-markdown.js +37 -0
- package/dist/lib/strip-markdown.js.map +1 -0
- package/dist/lib/tool-names.d.ts +17 -0
- package/dist/lib/tool-names.d.ts.map +1 -0
- package/dist/lib/tool-names.js +23 -0
- package/dist/lib/tool-names.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/utils.js +6 -0
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/ws-client.d.ts +26 -0
- package/dist/lib/ws-client.d.ts.map +1 -0
- package/dist/lib/ws-client.js +93 -0
- package/dist/lib/ws-client.js.map +1 -0
- package/dist/stores/chat-store.d.ts +131 -0
- package/dist/stores/chat-store.d.ts.map +1 -0
- package/dist/stores/chat-store.js +376 -0
- package/dist/stores/chat-store.js.map +1 -0
- package/dist/stores/connection-store.d.ts +11 -0
- package/dist/stores/connection-store.d.ts.map +1 -0
- package/dist/stores/connection-store.js +8 -0
- package/dist/stores/connection-store.js.map +1 -0
- package/dist/stores/file-store.d.ts +53 -0
- package/dist/stores/file-store.d.ts.map +1 -0
- package/dist/stores/file-store.js +284 -0
- package/dist/stores/file-store.js.map +1 -0
- package/dist/stores/provider-store.d.ts +24 -0
- package/dist/stores/provider-store.d.ts.map +1 -0
- package/dist/stores/provider-store.js +56 -0
- package/dist/stores/provider-store.js.map +1 -0
- package/dist/stores/ui-store.d.ts +21 -0
- package/dist/stores/ui-store.d.ts.map +1 -0
- package/dist/stores/ui-store.js +23 -0
- package/dist/stores/ui-store.js.map +1 -0
- package/dist/styles.css +2 -0
- package/dist/voice/asr-clients.d.ts +2 -0
- package/dist/voice/asr-clients.d.ts.map +1 -0
- package/dist/voice/asr-clients.js +33 -0
- package/dist/voice/asr-clients.js.map +1 -0
- package/dist/voice/asr-deepgram.d.ts +38 -0
- package/dist/voice/asr-deepgram.d.ts.map +1 -0
- package/dist/voice/asr-deepgram.js +307 -0
- package/dist/voice/asr-deepgram.js.map +1 -0
- package/dist/voice/asr-webspeech.d.ts +23 -0
- package/dist/voice/asr-webspeech.d.ts.map +1 -0
- package/dist/voice/asr-webspeech.js +93 -0
- package/dist/voice/asr-webspeech.js.map +1 -0
- package/dist/voice/use-dictation.d.ts +6 -0
- package/dist/voice/use-dictation.d.ts.map +1 -0
- package/dist/voice/use-dictation.js +174 -0
- package/dist/voice/use-dictation.js.map +1 -0
- package/dist/voice/voice-store.d.ts +26 -0
- package/dist/voice/voice-store.d.ts.map +1 -0
- package/dist/voice/voice-store.js +27 -0
- package/dist/voice/voice-store.js.map +1 -0
- package/package.json +71 -0
- package/src/components/chat/ask-user-card.tsx +451 -0
- package/src/components/chat/brain-markdown.tsx +547 -0
- package/src/components/chat/chat-page.tsx +820 -0
- package/src/components/chat/command-palette.tsx +68 -0
- package/src/components/chat/markdown-content.tsx +5 -0
- package/src/components/chat/message-bubble.tsx +362 -0
- package/src/components/chat/message-share.ts +78 -0
- package/src/components/chat/renderers/claude-tools.tsx +53 -0
- package/src/components/chat/renderers/generic.tsx +85 -0
- package/src/components/chat/renderers/index.ts +19 -0
- package/src/components/chat/risk-hints.ts +95 -0
- package/src/components/chat/session-drawer.tsx +213 -0
- package/src/components/chat/share-block.tsx +128 -0
- package/src/components/chat/tool-call-timeline.tsx +322 -0
- package/src/components/chat/tool-views.tsx +898 -0
- package/src/components/chat/welcome-state.tsx +105 -0
- package/src/components/connectivity/connection-gate.tsx +137 -0
- package/src/components/connectivity/login-screen.tsx +169 -0
- package/src/components/files/file-panel.tsx +116 -0
- package/src/components/files/file-tree.tsx +143 -0
- package/src/components/files/file-viewer-binary.tsx +63 -0
- package/src/components/files/file-viewer-html.tsx +10 -0
- package/src/components/files/file-viewer-markdown.tsx +14 -0
- package/src/components/files/file-viewer-raw.tsx +44 -0
- package/src/components/files/file-viewer.tsx +290 -0
- package/src/components/files/frontmatter-panel.tsx +78 -0
- package/src/components/layout/app-shell.tsx +25 -0
- package/src/components/layout/mobile-tab-bar.tsx +54 -0
- package/src/components/layout/side-rail.tsx +128 -0
- package/src/components/layout/slide-panel.tsx +64 -0
- package/src/components/quick-actions/streaming-modal.tsx +168 -0
- package/src/components/quick-actions/whatsup-modal.tsx +149 -0
- package/src/components/settings/passkey-panel.tsx +281 -0
- package/src/components/share/share-menu.tsx +121 -0
- package/src/components/voice/dictation-sheet.tsx +217 -0
- package/src/components/voice/mic-button.tsx +31 -0
- package/src/components/voice/review-card.tsx +66 -0
- package/src/config.ts +27 -0
- package/src/hooks/use-vpn-status.ts +75 -0
- package/src/hooks/use-websocket.ts +403 -0
- package/src/index.ts +46 -0
- package/src/lib/api-client.ts +173 -0
- package/src/lib/backend.ts +35 -0
- package/src/lib/frontmatter.ts +99 -0
- package/src/lib/image-attachments.ts +221 -0
- package/src/lib/passkeys.ts +50 -0
- package/src/lib/share.ts +167 -0
- package/src/lib/strip-markdown.ts +38 -0
- package/src/lib/tool-names.ts +27 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/ws-client.ts +109 -0
- package/src/stores/chat-store.ts +619 -0
- package/src/stores/connection-store.ts +23 -0
- package/src/stores/file-store.ts +344 -0
- package/src/stores/provider-store.ts +85 -0
- package/src/stores/ui-store.ts +48 -0
- package/src/styles.css +4 -0
- package/src/theme.css +425 -0
- package/src/voice/asr-clients.ts +45 -0
- package/src/voice/asr-deepgram.ts +325 -0
- package/src/voice/asr-webspeech.ts +135 -0
- package/src/voice/use-dictation.ts +187 -0
- package/src/voice/voice-store.ts +55 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type {
|
|
3
|
+
FileEntry,
|
|
4
|
+
FileContentResponse,
|
|
5
|
+
FileResolveResponse,
|
|
6
|
+
WikilinkMapResponse,
|
|
7
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
8
|
+
import { FILE_SIZE_CAP_BYTES } from "@schlessera/brain-ui-sdk/protocol";
|
|
9
|
+
import { API_BASE } from "../lib/backend.js";
|
|
10
|
+
|
|
11
|
+
export type ViewMode = "preview" | "raw";
|
|
12
|
+
|
|
13
|
+
const FRONTMATTER_COLLAPSED_KEY = "brain-ui:frontmatter-collapsed";
|
|
14
|
+
|
|
15
|
+
function readFrontmatterCollapsed(): boolean {
|
|
16
|
+
if (typeof localStorage === "undefined") return false;
|
|
17
|
+
return localStorage.getItem(FRONTMATTER_COLLAPSED_KEY) === "1";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface FileState {
|
|
21
|
+
// Tree state
|
|
22
|
+
dirCache: Record<string, FileEntry[]>;
|
|
23
|
+
loadingDirs: Set<string>;
|
|
24
|
+
dirErrors: Record<string, string>;
|
|
25
|
+
expandedDirs: Set<string>;
|
|
26
|
+
|
|
27
|
+
// Viewer state
|
|
28
|
+
currentPath: string | null;
|
|
29
|
+
currentContent: FileContentResponse | null;
|
|
30
|
+
contentLoading: boolean;
|
|
31
|
+
contentError: string | null;
|
|
32
|
+
viewMode: ViewMode;
|
|
33
|
+
treeExpanded: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the frontmatter table at the top of markdown files is collapsed.
|
|
36
|
+
* Persisted in localStorage so the preference carries across files.
|
|
37
|
+
*/
|
|
38
|
+
frontmatterCollapsed: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Directory most recently navigated to via openDir. Used by the tree to
|
|
41
|
+
* apply a brief highlight + scroll the node into view. Cleared shortly
|
|
42
|
+
* after by openDir itself.
|
|
43
|
+
*/
|
|
44
|
+
highlightedDir: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* Lowercase slug -> repo-relative .md path. Lazily loaded the first
|
|
47
|
+
* time a markdown surface needs to resolve a [[wikilink]].
|
|
48
|
+
*/
|
|
49
|
+
wikilinkMap: Record<string, string>;
|
|
50
|
+
wikilinkLoaded: boolean;
|
|
51
|
+
wikilinkLoading: boolean;
|
|
52
|
+
|
|
53
|
+
// Actions
|
|
54
|
+
loadDir: (path: string) => Promise<void>;
|
|
55
|
+
toggleDir: (path: string) => Promise<void>;
|
|
56
|
+
setExpanded: (path: string, expanded: boolean) => void;
|
|
57
|
+
openFile: (path: string) => Promise<void>;
|
|
58
|
+
openDir: (path: string) => Promise<void>;
|
|
59
|
+
closeFile: () => void;
|
|
60
|
+
setViewMode: (mode: ViewMode) => void;
|
|
61
|
+
setTreeExpanded: (expanded: boolean) => void;
|
|
62
|
+
toggleFrontmatter: () => void;
|
|
63
|
+
setHighlightedDir: (path: string | null) => void;
|
|
64
|
+
ensureWikilinks: () => Promise<void>;
|
|
65
|
+
reset: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function fetchTree(path: string): Promise<FileEntry[]> {
|
|
69
|
+
const url = `${API_BASE}/files/tree${path ? `?path=${encodeURIComponent(path)}` : ""}`;
|
|
70
|
+
const res = await fetch(url);
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
73
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
74
|
+
}
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
return data.entries;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function fetchContent(path: string): Promise<FileContentResponse> {
|
|
80
|
+
const res = await fetch(`${API_BASE}/files/content?path=${encodeURIComponent(path)}`);
|
|
81
|
+
if (!res.ok) {
|
|
82
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
83
|
+
const err = new Error(body.error || `HTTP ${res.status}`);
|
|
84
|
+
(err as Error & { status: number; size?: number }).status = res.status;
|
|
85
|
+
if (body.size) (err as Error & { status: number; size?: number }).size = body.size;
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
return res.json();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function fetchResolve(path: string): Promise<FileResolveResponse> {
|
|
92
|
+
const res = await fetch(`${API_BASE}/files/resolve?path=${encodeURIComponent(path)}`);
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
95
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
96
|
+
}
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fetchWikilinks(): Promise<WikilinkMapResponse> {
|
|
101
|
+
const res = await fetch(`${API_BASE}/files/wikilinks`);
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
104
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
105
|
+
}
|
|
106
|
+
return res.json();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const useFileStore = create<FileState>((set, get) => ({
|
|
110
|
+
dirCache: {},
|
|
111
|
+
loadingDirs: new Set(),
|
|
112
|
+
dirErrors: {},
|
|
113
|
+
expandedDirs: new Set([""]),
|
|
114
|
+
|
|
115
|
+
currentPath: null,
|
|
116
|
+
currentContent: null,
|
|
117
|
+
contentLoading: false,
|
|
118
|
+
contentError: null,
|
|
119
|
+
viewMode: "preview",
|
|
120
|
+
treeExpanded: true,
|
|
121
|
+
frontmatterCollapsed: readFrontmatterCollapsed(),
|
|
122
|
+
highlightedDir: null,
|
|
123
|
+
wikilinkMap: {},
|
|
124
|
+
wikilinkLoaded: false,
|
|
125
|
+
wikilinkLoading: false,
|
|
126
|
+
|
|
127
|
+
loadDir: async (path) => {
|
|
128
|
+
const { dirCache, loadingDirs } = get();
|
|
129
|
+
if (dirCache[path] || loadingDirs.has(path)) return;
|
|
130
|
+
const next = new Set(loadingDirs);
|
|
131
|
+
next.add(path);
|
|
132
|
+
set({ loadingDirs: next });
|
|
133
|
+
try {
|
|
134
|
+
const entries = await fetchTree(path);
|
|
135
|
+
set((s) => {
|
|
136
|
+
const loading = new Set(s.loadingDirs);
|
|
137
|
+
loading.delete(path);
|
|
138
|
+
const errors = { ...s.dirErrors };
|
|
139
|
+
delete errors[path];
|
|
140
|
+
return {
|
|
141
|
+
dirCache: { ...s.dirCache, [path]: entries },
|
|
142
|
+
loadingDirs: loading,
|
|
143
|
+
dirErrors: errors,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
} catch (err) {
|
|
147
|
+
set((s) => {
|
|
148
|
+
const loading = new Set(s.loadingDirs);
|
|
149
|
+
loading.delete(path);
|
|
150
|
+
return {
|
|
151
|
+
loadingDirs: loading,
|
|
152
|
+
dirErrors: { ...s.dirErrors, [path]: err instanceof Error ? err.message : "load_failed" },
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
toggleDir: async (path) => {
|
|
159
|
+
const { expandedDirs } = get();
|
|
160
|
+
if (expandedDirs.has(path)) {
|
|
161
|
+
const next = new Set(expandedDirs);
|
|
162
|
+
next.delete(path);
|
|
163
|
+
set({ expandedDirs: next });
|
|
164
|
+
} else {
|
|
165
|
+
const next = new Set(expandedDirs);
|
|
166
|
+
next.add(path);
|
|
167
|
+
set({ expandedDirs: next });
|
|
168
|
+
await get().loadDir(path);
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
setExpanded: (path, expanded) => {
|
|
173
|
+
set((s) => {
|
|
174
|
+
const next = new Set(s.expandedDirs);
|
|
175
|
+
if (expanded) next.add(path);
|
|
176
|
+
else next.delete(path);
|
|
177
|
+
return { expandedDirs: next };
|
|
178
|
+
});
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
openFile: async (path) => {
|
|
182
|
+
const normalized = path.replace(/^\/+/, "").replace(/\\/g, "/");
|
|
183
|
+
set({ currentPath: normalized, contentLoading: true, contentError: null, currentContent: null, treeExpanded: false });
|
|
184
|
+
|
|
185
|
+
// Fire-and-await: load ancestor chain then content in parallel
|
|
186
|
+
const ancestorsPromise = (async () => {
|
|
187
|
+
try {
|
|
188
|
+
const r = await fetchResolve(normalized);
|
|
189
|
+
// Expand each ancestor and root, load their contents
|
|
190
|
+
const dirsToLoad = ["", ...r.ancestors];
|
|
191
|
+
set((s) => {
|
|
192
|
+
const next = new Set(s.expandedDirs);
|
|
193
|
+
for (const d of dirsToLoad) next.add(d);
|
|
194
|
+
return { expandedDirs: next };
|
|
195
|
+
});
|
|
196
|
+
await Promise.all(dirsToLoad.map((d) => get().loadDir(d)));
|
|
197
|
+
} catch {
|
|
198
|
+
// Tree expansion best-effort
|
|
199
|
+
}
|
|
200
|
+
})();
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const content = await fetchContent(normalized);
|
|
204
|
+
set({ currentContent: content, contentLoading: false });
|
|
205
|
+
// Default mode: prefer preview when available; persist otherwise
|
|
206
|
+
const { viewMode } = get();
|
|
207
|
+
if (content.kind !== "markdown" && content.kind !== "html" && viewMode === "preview") {
|
|
208
|
+
set({ viewMode: "raw" });
|
|
209
|
+
} else if ((content.kind === "markdown" || content.kind === "html") && viewMode === "raw") {
|
|
210
|
+
// Keep raw if user toggled — but on a fresh open default back to preview
|
|
211
|
+
set({ viewMode: "preview" });
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const e = err as Error & { status?: number; size?: number };
|
|
215
|
+
let msg = e.message || "load_failed";
|
|
216
|
+
if (e.status === 413 && e.size) msg = `File too large (${(e.size / 1024 / 1024).toFixed(2)} MB; cap ${(FILE_SIZE_CAP_BYTES / 1024 / 1024).toFixed(0)} MB).`;
|
|
217
|
+
else if (e.status === 404) msg = "File not found.";
|
|
218
|
+
else if (e.message === "invalid_path") msg = "Invalid path.";
|
|
219
|
+
set({ contentError: msg, contentLoading: false });
|
|
220
|
+
} finally {
|
|
221
|
+
await ancestorsPromise;
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
closeFile: () => {
|
|
226
|
+
set({ currentPath: null, currentContent: null, contentError: null, treeExpanded: true });
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
openDir: async (path) => {
|
|
230
|
+
const normalized = path
|
|
231
|
+
.replace(/^\/+/, "")
|
|
232
|
+
.replace(/\\/g, "/")
|
|
233
|
+
.replace(/\/+$/, "");
|
|
234
|
+
if (!normalized) {
|
|
235
|
+
// Just expand the root view
|
|
236
|
+
set({ treeExpanded: true });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Make the tree visible even if a file is currently open.
|
|
240
|
+
set({ treeExpanded: true, highlightedDir: normalized });
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
const r = await fetchResolve(normalized);
|
|
244
|
+
const dirs = ["", ...r.ancestors, normalized];
|
|
245
|
+
set((s) => {
|
|
246
|
+
const next = new Set(s.expandedDirs);
|
|
247
|
+
for (const d of dirs) next.add(d);
|
|
248
|
+
return { expandedDirs: next };
|
|
249
|
+
});
|
|
250
|
+
await Promise.all(dirs.map((d) => get().loadDir(d)));
|
|
251
|
+
} catch {
|
|
252
|
+
// Best-effort expansion — failure surfaces via per-dir error state
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
setViewMode: (mode) => set({ viewMode: mode }),
|
|
257
|
+
|
|
258
|
+
setTreeExpanded: (expanded) => set({ treeExpanded: expanded }),
|
|
259
|
+
|
|
260
|
+
setHighlightedDir: (path) => set({ highlightedDir: path }),
|
|
261
|
+
|
|
262
|
+
ensureWikilinks: async () => {
|
|
263
|
+
const { wikilinkLoaded, wikilinkLoading } = get();
|
|
264
|
+
if (wikilinkLoaded || wikilinkLoading) return;
|
|
265
|
+
set({ wikilinkLoading: true });
|
|
266
|
+
try {
|
|
267
|
+
const data = await fetchWikilinks();
|
|
268
|
+
set({
|
|
269
|
+
wikilinkMap: data.slugs,
|
|
270
|
+
wikilinkLoaded: true,
|
|
271
|
+
wikilinkLoading: false,
|
|
272
|
+
});
|
|
273
|
+
} catch {
|
|
274
|
+
// Soft-fail — leave the map empty; wikilinks will render as plain text
|
|
275
|
+
set({ wikilinkLoading: false });
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
toggleFrontmatter: () =>
|
|
280
|
+
set((s) => {
|
|
281
|
+
const next = !s.frontmatterCollapsed;
|
|
282
|
+
if (typeof localStorage !== "undefined") {
|
|
283
|
+
if (next) localStorage.setItem(FRONTMATTER_COLLAPSED_KEY, "1");
|
|
284
|
+
else localStorage.removeItem(FRONTMATTER_COLLAPSED_KEY);
|
|
285
|
+
}
|
|
286
|
+
return { frontmatterCollapsed: next };
|
|
287
|
+
}),
|
|
288
|
+
|
|
289
|
+
reset: () =>
|
|
290
|
+
set({
|
|
291
|
+
currentPath: null,
|
|
292
|
+
currentContent: null,
|
|
293
|
+
contentLoading: false,
|
|
294
|
+
contentError: null,
|
|
295
|
+
viewMode: "preview",
|
|
296
|
+
treeExpanded: true,
|
|
297
|
+
}),
|
|
298
|
+
}));
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Heuristics: is this string a plausible repo-relative path reference?
|
|
302
|
+
*
|
|
303
|
+
* Files require a trailing `.ext` (1-8 alphanumeric chars). Directories
|
|
304
|
+
* require at least one `/` segment and a trailing `/`. Neither may look
|
|
305
|
+
* like a URL, an absolute path, or a version number.
|
|
306
|
+
*/
|
|
307
|
+
const FILE_REF_RE = /^[a-z0-9_.-]+(\/[a-z0-9_.-]+)*\.[a-z0-9]{1,8}$/i;
|
|
308
|
+
const DIR_REF_RE = /^[a-z0-9_.-]+(\/[a-z0-9_.-]+)+\/$/i;
|
|
309
|
+
const URL_LIKE_RE = /^(?:https?:|mailto:|tel:|#|\/)/i;
|
|
310
|
+
const VERSION_RE = /^\d+(\.\d+){1,3}$/;
|
|
311
|
+
|
|
312
|
+
function rejectsCommonNonPath(href: string): boolean {
|
|
313
|
+
if (URL_LIKE_RE.test(href)) return true;
|
|
314
|
+
if (VERSION_RE.test(href)) return true;
|
|
315
|
+
if (href.includes(" ")) return true;
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function isInternalRepoPath(href: string | undefined): href is string {
|
|
320
|
+
if (!href) return false;
|
|
321
|
+
if (rejectsCommonNonPath(href)) return false;
|
|
322
|
+
return FILE_REF_RE.test(href);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function isInternalRepoDir(href: string | undefined): href is string {
|
|
326
|
+
if (!href) return false;
|
|
327
|
+
if (rejectsCommonNonPath(href)) return false;
|
|
328
|
+
return DIR_REF_RE.test(href);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export type RepoPathKind = "file" | "dir";
|
|
332
|
+
|
|
333
|
+
export function classifyRepoPath(
|
|
334
|
+
href: string | undefined
|
|
335
|
+
): RepoPathKind | null {
|
|
336
|
+
if (isInternalRepoPath(href)) return "file";
|
|
337
|
+
if (isInternalRepoDir(href)) return "dir";
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Normalize a directory ref by trimming the trailing slash. */
|
|
342
|
+
export function normalizeDirPath(path: string): string {
|
|
343
|
+
return path.replace(/\/+$/, "");
|
|
344
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
import type { ProviderInfo } from "@schlessera/brain-ui-sdk/protocol";
|
|
3
|
+
import { api, type BackendInfo } from "../lib/api-client.js";
|
|
4
|
+
|
|
5
|
+
const PROVIDER_ID_KEY = "brain-ui:provider-id";
|
|
6
|
+
|
|
7
|
+
function readSelectedId(): string {
|
|
8
|
+
if (typeof localStorage === "undefined") return "";
|
|
9
|
+
return localStorage.getItem(PROVIDER_ID_KEY) ?? "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface ProviderState {
|
|
13
|
+
/** Provider combos the server currently offers (metadata only). */
|
|
14
|
+
available: ProviderInfo[];
|
|
15
|
+
/** User's chosen combo for the next new conversation. Persisted. */
|
|
16
|
+
selectedId: string;
|
|
17
|
+
/**
|
|
18
|
+
* Provider the active session is pinned to (from `session_info`). When set,
|
|
19
|
+
* it takes display precedence over `selectedId` and the picker locks.
|
|
20
|
+
*/
|
|
21
|
+
pinnedId: string | null;
|
|
22
|
+
/** Capability metadata keyed by backend id. */
|
|
23
|
+
backends: Record<string, BackendInfo>;
|
|
24
|
+
loaded: boolean;
|
|
25
|
+
|
|
26
|
+
loadProviders: () => Promise<void>;
|
|
27
|
+
setSelected: (id: string) => void;
|
|
28
|
+
setPinned: (id: string | null) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Resolve the backend metadata for an available provider profile. */
|
|
32
|
+
export function selectBackendForProvider(
|
|
33
|
+
state: Pick<ProviderState, "available" | "backends">,
|
|
34
|
+
providerId: string | null | undefined
|
|
35
|
+
): BackendInfo | null {
|
|
36
|
+
const backendId = state.available.find(
|
|
37
|
+
(provider) => provider.id === providerId
|
|
38
|
+
)?.backendId;
|
|
39
|
+
return backendId ? state.backends[backendId] ?? null : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const useProviderStore = create<ProviderState>((set, get) => ({
|
|
43
|
+
available: [],
|
|
44
|
+
selectedId: readSelectedId(),
|
|
45
|
+
pinnedId: null,
|
|
46
|
+
backends: {},
|
|
47
|
+
loaded: false,
|
|
48
|
+
|
|
49
|
+
loadProviders: async () => {
|
|
50
|
+
try {
|
|
51
|
+
const { providers, backends } = await api.providers();
|
|
52
|
+
set((s) => {
|
|
53
|
+
// Keep the persisted choice if it's still on offer; otherwise fall
|
|
54
|
+
// back to the first available combo.
|
|
55
|
+
const stillValid = providers.some((p) => p.id === s.selectedId);
|
|
56
|
+
const selectedId =
|
|
57
|
+
stillValid && s.selectedId
|
|
58
|
+
? s.selectedId
|
|
59
|
+
: providers[0]?.id ?? "";
|
|
60
|
+
return {
|
|
61
|
+
available: providers,
|
|
62
|
+
selectedId,
|
|
63
|
+
backends: backends ?? {},
|
|
64
|
+
loaded: true,
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
} catch {
|
|
68
|
+
// Soft-fail: leave the picker empty; the composer still works and
|
|
69
|
+
// sends run on the server default.
|
|
70
|
+
set({ loaded: true });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
setSelected: (id) => {
|
|
75
|
+
if (typeof localStorage !== "undefined") {
|
|
76
|
+
localStorage.setItem(PROVIDER_ID_KEY, id);
|
|
77
|
+
}
|
|
78
|
+
set({ selectedId: id });
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
setPinned: (id) => {
|
|
82
|
+
if (get().pinnedId === id) return;
|
|
83
|
+
set({ pinnedId: id });
|
|
84
|
+
},
|
|
85
|
+
}));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface UIState {
|
|
4
|
+
sessionPanelOpen: boolean;
|
|
5
|
+
syncPanelOpen: boolean;
|
|
6
|
+
whatsupPanelOpen: boolean;
|
|
7
|
+
filePanelOpen: boolean;
|
|
8
|
+
securityPanelOpen: boolean;
|
|
9
|
+
toggleSessionPanel: () => void;
|
|
10
|
+
toggleSyncPanel: () => void;
|
|
11
|
+
toggleWhatsupPanel: () => void;
|
|
12
|
+
toggleFilePanel: () => void;
|
|
13
|
+
toggleSecurityPanel: () => void;
|
|
14
|
+
closeAllPanels: () => void;
|
|
15
|
+
setSyncPanelOpen: (open: boolean) => void;
|
|
16
|
+
setWhatsupPanelOpen: (open: boolean) => void;
|
|
17
|
+
setSessionPanelOpen: (open: boolean) => void;
|
|
18
|
+
setFilePanelOpen: (open: boolean) => void;
|
|
19
|
+
setSecurityPanelOpen: (open: boolean) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const CLOSED = {
|
|
23
|
+
sessionPanelOpen: false,
|
|
24
|
+
syncPanelOpen: false,
|
|
25
|
+
whatsupPanelOpen: false,
|
|
26
|
+
filePanelOpen: false,
|
|
27
|
+
securityPanelOpen: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
31
|
+
...CLOSED,
|
|
32
|
+
toggleSessionPanel: () =>
|
|
33
|
+
set((s) => ({ ...CLOSED, sessionPanelOpen: !s.sessionPanelOpen })),
|
|
34
|
+
toggleSyncPanel: () =>
|
|
35
|
+
set((s) => ({ ...CLOSED, syncPanelOpen: !s.syncPanelOpen })),
|
|
36
|
+
toggleWhatsupPanel: () =>
|
|
37
|
+
set((s) => ({ ...CLOSED, whatsupPanelOpen: !s.whatsupPanelOpen })),
|
|
38
|
+
toggleFilePanel: () =>
|
|
39
|
+
set((s) => ({ ...CLOSED, filePanelOpen: !s.filePanelOpen })),
|
|
40
|
+
toggleSecurityPanel: () =>
|
|
41
|
+
set((s) => ({ ...CLOSED, securityPanelOpen: !s.securityPanelOpen })),
|
|
42
|
+
closeAllPanels: () => set({ ...CLOSED }),
|
|
43
|
+
setSyncPanelOpen: (open) => set({ syncPanelOpen: open }),
|
|
44
|
+
setWhatsupPanelOpen: (open) => set({ whatsupPanelOpen: open }),
|
|
45
|
+
setSessionPanelOpen: (open) => set({ sessionPanelOpen: open }),
|
|
46
|
+
setFilePanelOpen: (open) => set({ filePanelOpen: open }),
|
|
47
|
+
setSecurityPanelOpen: (open) => set({ securityPanelOpen: open }),
|
|
48
|
+
}));
|
package/src/styles.css
ADDED