@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.
Files changed (311) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/components/chat/ask-user-card.d.ts +12 -0
  4. package/dist/components/chat/ask-user-card.d.ts.map +1 -0
  5. package/dist/components/chat/ask-user-card.js +185 -0
  6. package/dist/components/chat/ask-user-card.js.map +1 -0
  7. package/dist/components/chat/brain-markdown.d.ts +35 -0
  8. package/dist/components/chat/brain-markdown.d.ts.map +1 -0
  9. package/dist/components/chat/brain-markdown.js +363 -0
  10. package/dist/components/chat/brain-markdown.js.map +1 -0
  11. package/dist/components/chat/chat-page.d.ts +2 -0
  12. package/dist/components/chat/chat-page.d.ts.map +1 -0
  13. package/dist/components/chat/chat-page.js +442 -0
  14. package/dist/components/chat/chat-page.js.map +1 -0
  15. package/dist/components/chat/command-palette.d.ts +5 -0
  16. package/dist/components/chat/command-palette.d.ts.map +1 -0
  17. package/dist/components/chat/command-palette.js +18 -0
  18. package/dist/components/chat/command-palette.js.map +1 -0
  19. package/dist/components/chat/markdown-content.d.ts +4 -0
  20. package/dist/components/chat/markdown-content.d.ts.map +1 -0
  21. package/dist/components/chat/markdown-content.js +6 -0
  22. package/dist/components/chat/markdown-content.js.map +1 -0
  23. package/dist/components/chat/message-bubble.d.ts +9 -0
  24. package/dist/components/chat/message-bubble.d.ts.map +1 -0
  25. package/dist/components/chat/message-bubble.js +129 -0
  26. package/dist/components/chat/message-bubble.js.map +1 -0
  27. package/dist/components/chat/message-share.d.ts +10 -0
  28. package/dist/components/chat/message-share.d.ts.map +1 -0
  29. package/dist/components/chat/message-share.js +65 -0
  30. package/dist/components/chat/message-share.js.map +1 -0
  31. package/dist/components/chat/renderers/claude-tools.d.ts +3 -0
  32. package/dist/components/chat/renderers/claude-tools.d.ts.map +1 -0
  33. package/dist/components/chat/renderers/claude-tools.js +38 -0
  34. package/dist/components/chat/renderers/claude-tools.js.map +1 -0
  35. package/dist/components/chat/renderers/generic.d.ts +9 -0
  36. package/dist/components/chat/renderers/generic.d.ts.map +1 -0
  37. package/dist/components/chat/renderers/generic.js +68 -0
  38. package/dist/components/chat/renderers/generic.js.map +1 -0
  39. package/dist/components/chat/renderers/index.d.ts +5 -0
  40. package/dist/components/chat/renderers/index.d.ts.map +1 -0
  41. package/dist/components/chat/renderers/index.js +17 -0
  42. package/dist/components/chat/renderers/index.js.map +1 -0
  43. package/dist/components/chat/risk-hints.d.ts +20 -0
  44. package/dist/components/chat/risk-hints.d.ts.map +1 -0
  45. package/dist/components/chat/risk-hints.js +81 -0
  46. package/dist/components/chat/risk-hints.js.map +1 -0
  47. package/dist/components/chat/session-drawer.d.ts +6 -0
  48. package/dist/components/chat/session-drawer.d.ts.map +1 -0
  49. package/dist/components/chat/session-drawer.js +96 -0
  50. package/dist/components/chat/session-drawer.js.map +1 -0
  51. package/dist/components/chat/share-block.d.ts +9 -0
  52. package/dist/components/chat/share-block.d.ts.map +1 -0
  53. package/dist/components/chat/share-block.js +70 -0
  54. package/dist/components/chat/share-block.js.map +1 -0
  55. package/dist/components/chat/tool-call-timeline.d.ts +8 -0
  56. package/dist/components/chat/tool-call-timeline.d.ts.map +1 -0
  57. package/dist/components/chat/tool-call-timeline.js +100 -0
  58. package/dist/components/chat/tool-call-timeline.js.map +1 -0
  59. package/dist/components/chat/tool-views.d.ts +116 -0
  60. package/dist/components/chat/tool-views.d.ts.map +1 -0
  61. package/dist/components/chat/tool-views.js +628 -0
  62. package/dist/components/chat/tool-views.js.map +1 -0
  63. package/dist/components/chat/welcome-state.d.ts +4 -0
  64. package/dist/components/chat/welcome-state.d.ts.map +1 -0
  65. package/dist/components/chat/welcome-state.js +10 -0
  66. package/dist/components/chat/welcome-state.js.map +1 -0
  67. package/dist/components/connectivity/connection-gate.d.ts +5 -0
  68. package/dist/components/connectivity/connection-gate.d.ts.map +1 -0
  69. package/dist/components/connectivity/connection-gate.js +42 -0
  70. package/dist/components/connectivity/connection-gate.js.map +1 -0
  71. package/dist/components/connectivity/login-screen.d.ts +9 -0
  72. package/dist/components/connectivity/login-screen.d.ts.map +1 -0
  73. package/dist/components/connectivity/login-screen.js +99 -0
  74. package/dist/components/connectivity/login-screen.js.map +1 -0
  75. package/dist/components/files/file-panel.d.ts +5 -0
  76. package/dist/components/files/file-panel.d.ts.map +1 -0
  77. package/dist/components/files/file-panel.js +46 -0
  78. package/dist/components/files/file-panel.js.map +1 -0
  79. package/dist/components/files/file-tree.d.ts +2 -0
  80. package/dist/components/files/file-tree.d.ts.map +1 -0
  81. package/dist/components/files/file-tree.js +63 -0
  82. package/dist/components/files/file-tree.js.map +1 -0
  83. package/dist/components/files/file-viewer-binary.d.ts +5 -0
  84. package/dist/components/files/file-viewer-binary.d.ts.map +1 -0
  85. package/dist/components/files/file-viewer-binary.js +20 -0
  86. package/dist/components/files/file-viewer-binary.js.map +1 -0
  87. package/dist/components/files/file-viewer-html.d.ts +4 -0
  88. package/dist/components/files/file-viewer-html.d.ts.map +1 -0
  89. package/dist/components/files/file-viewer-html.js +5 -0
  90. package/dist/components/files/file-viewer-html.js.map +1 -0
  91. package/dist/components/files/file-viewer-markdown.d.ts +4 -0
  92. package/dist/components/files/file-viewer-markdown.d.ts.map +1 -0
  93. package/dist/components/files/file-viewer-markdown.js +10 -0
  94. package/dist/components/files/file-viewer-markdown.js.map +1 -0
  95. package/dist/components/files/file-viewer-raw.d.ts +5 -0
  96. package/dist/components/files/file-viewer-raw.d.ts.map +1 -0
  97. package/dist/components/files/file-viewer-raw.js +15 -0
  98. package/dist/components/files/file-viewer-raw.js.map +1 -0
  99. package/dist/components/files/file-viewer.d.ts +2 -0
  100. package/dist/components/files/file-viewer.d.ts.map +1 -0
  101. package/dist/components/files/file-viewer.js +175 -0
  102. package/dist/components/files/file-viewer.js.map +1 -0
  103. package/dist/components/files/frontmatter-panel.d.ts +5 -0
  104. package/dist/components/files/frontmatter-panel.d.ts.map +1 -0
  105. package/dist/components/files/frontmatter-panel.js +12 -0
  106. package/dist/components/files/frontmatter-panel.js.map +1 -0
  107. package/dist/components/layout/app-shell.d.ts +5 -0
  108. package/dist/components/layout/app-shell.d.ts.map +1 -0
  109. package/dist/components/layout/app-shell.js +10 -0
  110. package/dist/components/layout/app-shell.js.map +1 -0
  111. package/dist/components/layout/mobile-tab-bar.d.ts +2 -0
  112. package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -0
  113. package/dist/components/layout/mobile-tab-bar.js +16 -0
  114. package/dist/components/layout/mobile-tab-bar.js.map +1 -0
  115. package/dist/components/layout/side-rail.d.ts +2 -0
  116. package/dist/components/layout/side-rail.d.ts.map +1 -0
  117. package/dist/components/layout/side-rail.js +26 -0
  118. package/dist/components/layout/side-rail.js.map +1 -0
  119. package/dist/components/layout/slide-panel.d.ts +9 -0
  120. package/dist/components/layout/slide-panel.d.ts.map +1 -0
  121. package/dist/components/layout/slide-panel.js +18 -0
  122. package/dist/components/layout/slide-panel.js.map +1 -0
  123. package/dist/components/quick-actions/streaming-modal.d.ts +8 -0
  124. package/dist/components/quick-actions/streaming-modal.d.ts.map +1 -0
  125. package/dist/components/quick-actions/streaming-modal.js +92 -0
  126. package/dist/components/quick-actions/streaming-modal.js.map +1 -0
  127. package/dist/components/quick-actions/whatsup-modal.d.ts +5 -0
  128. package/dist/components/quick-actions/whatsup-modal.d.ts.map +1 -0
  129. package/dist/components/quick-actions/whatsup-modal.js +82 -0
  130. package/dist/components/quick-actions/whatsup-modal.js.map +1 -0
  131. package/dist/components/settings/passkey-panel.d.ts +10 -0
  132. package/dist/components/settings/passkey-panel.d.ts.map +1 -0
  133. package/dist/components/settings/passkey-panel.js +118 -0
  134. package/dist/components/settings/passkey-panel.js.map +1 -0
  135. package/dist/components/share/share-menu.d.ts +23 -0
  136. package/dist/components/share/share-menu.d.ts.map +1 -0
  137. package/dist/components/share/share-menu.js +58 -0
  138. package/dist/components/share/share-menu.js.map +1 -0
  139. package/dist/components/voice/dictation-sheet.d.ts +6 -0
  140. package/dist/components/voice/dictation-sheet.d.ts.map +1 -0
  141. package/dist/components/voice/dictation-sheet.js +91 -0
  142. package/dist/components/voice/dictation-sheet.js.map +1 -0
  143. package/dist/components/voice/mic-button.d.ts +6 -0
  144. package/dist/components/voice/mic-button.d.ts.map +1 -0
  145. package/dist/components/voice/mic-button.js +9 -0
  146. package/dist/components/voice/mic-button.js.map +1 -0
  147. package/dist/components/voice/review-card.d.ts +8 -0
  148. package/dist/components/voice/review-card.d.ts.map +1 -0
  149. package/dist/components/voice/review-card.js +8 -0
  150. package/dist/components/voice/review-card.js.map +1 -0
  151. package/dist/config.d.ts +19 -0
  152. package/dist/config.d.ts.map +1 -0
  153. package/dist/config.js +10 -0
  154. package/dist/config.js.map +1 -0
  155. package/dist/hooks/use-vpn-status.d.ts +2 -0
  156. package/dist/hooks/use-vpn-status.d.ts.map +1 -0
  157. package/dist/hooks/use-vpn-status.js +71 -0
  158. package/dist/hooks/use-vpn-status.js.map +1 -0
  159. package/dist/hooks/use-websocket.d.ts +8 -0
  160. package/dist/hooks/use-websocket.d.ts.map +1 -0
  161. package/dist/hooks/use-websocket.js +366 -0
  162. package/dist/hooks/use-websocket.js.map +1 -0
  163. package/dist/index.d.ts +25 -0
  164. package/dist/index.d.ts.map +1 -0
  165. package/dist/index.js +31 -0
  166. package/dist/index.js.map +1 -0
  167. package/dist/lib/api-client.d.ts +115 -0
  168. package/dist/lib/api-client.d.ts.map +1 -0
  169. package/dist/lib/api-client.js +70 -0
  170. package/dist/lib/api-client.js.map +1 -0
  171. package/dist/lib/backend.d.ts +7 -0
  172. package/dist/lib/backend.d.ts.map +1 -0
  173. package/dist/lib/backend.js +33 -0
  174. package/dist/lib/backend.js.map +1 -0
  175. package/dist/lib/frontmatter.d.ts +21 -0
  176. package/dist/lib/frontmatter.d.ts.map +1 -0
  177. package/dist/lib/frontmatter.js +88 -0
  178. package/dist/lib/frontmatter.js.map +1 -0
  179. package/dist/lib/image-attachments.d.ts +36 -0
  180. package/dist/lib/image-attachments.d.ts.map +1 -0
  181. package/dist/lib/image-attachments.js +175 -0
  182. package/dist/lib/image-attachments.js.map +1 -0
  183. package/dist/lib/passkeys.d.ts +18 -0
  184. package/dist/lib/passkeys.d.ts.map +1 -0
  185. package/dist/lib/passkeys.js +36 -0
  186. package/dist/lib/passkeys.js.map +1 -0
  187. package/dist/lib/share.d.ts +53 -0
  188. package/dist/lib/share.d.ts.map +1 -0
  189. package/dist/lib/share.js +144 -0
  190. package/dist/lib/share.js.map +1 -0
  191. package/dist/lib/strip-markdown.d.ts +10 -0
  192. package/dist/lib/strip-markdown.d.ts.map +1 -0
  193. package/dist/lib/strip-markdown.js +37 -0
  194. package/dist/lib/strip-markdown.js.map +1 -0
  195. package/dist/lib/tool-names.d.ts +17 -0
  196. package/dist/lib/tool-names.d.ts.map +1 -0
  197. package/dist/lib/tool-names.js +23 -0
  198. package/dist/lib/tool-names.js.map +1 -0
  199. package/dist/lib/utils.d.ts +3 -0
  200. package/dist/lib/utils.d.ts.map +1 -0
  201. package/dist/lib/utils.js +6 -0
  202. package/dist/lib/utils.js.map +1 -0
  203. package/dist/lib/ws-client.d.ts +26 -0
  204. package/dist/lib/ws-client.d.ts.map +1 -0
  205. package/dist/lib/ws-client.js +93 -0
  206. package/dist/lib/ws-client.js.map +1 -0
  207. package/dist/stores/chat-store.d.ts +131 -0
  208. package/dist/stores/chat-store.d.ts.map +1 -0
  209. package/dist/stores/chat-store.js +376 -0
  210. package/dist/stores/chat-store.js.map +1 -0
  211. package/dist/stores/connection-store.d.ts +11 -0
  212. package/dist/stores/connection-store.d.ts.map +1 -0
  213. package/dist/stores/connection-store.js +8 -0
  214. package/dist/stores/connection-store.js.map +1 -0
  215. package/dist/stores/file-store.d.ts +53 -0
  216. package/dist/stores/file-store.d.ts.map +1 -0
  217. package/dist/stores/file-store.js +284 -0
  218. package/dist/stores/file-store.js.map +1 -0
  219. package/dist/stores/provider-store.d.ts +24 -0
  220. package/dist/stores/provider-store.d.ts.map +1 -0
  221. package/dist/stores/provider-store.js +56 -0
  222. package/dist/stores/provider-store.js.map +1 -0
  223. package/dist/stores/ui-store.d.ts +21 -0
  224. package/dist/stores/ui-store.d.ts.map +1 -0
  225. package/dist/stores/ui-store.js +23 -0
  226. package/dist/stores/ui-store.js.map +1 -0
  227. package/dist/styles.css +2 -0
  228. package/dist/voice/asr-clients.d.ts +2 -0
  229. package/dist/voice/asr-clients.d.ts.map +1 -0
  230. package/dist/voice/asr-clients.js +33 -0
  231. package/dist/voice/asr-clients.js.map +1 -0
  232. package/dist/voice/asr-deepgram.d.ts +38 -0
  233. package/dist/voice/asr-deepgram.d.ts.map +1 -0
  234. package/dist/voice/asr-deepgram.js +307 -0
  235. package/dist/voice/asr-deepgram.js.map +1 -0
  236. package/dist/voice/asr-webspeech.d.ts +23 -0
  237. package/dist/voice/asr-webspeech.d.ts.map +1 -0
  238. package/dist/voice/asr-webspeech.js +93 -0
  239. package/dist/voice/asr-webspeech.js.map +1 -0
  240. package/dist/voice/use-dictation.d.ts +6 -0
  241. package/dist/voice/use-dictation.d.ts.map +1 -0
  242. package/dist/voice/use-dictation.js +174 -0
  243. package/dist/voice/use-dictation.js.map +1 -0
  244. package/dist/voice/voice-store.d.ts +26 -0
  245. package/dist/voice/voice-store.d.ts.map +1 -0
  246. package/dist/voice/voice-store.js +27 -0
  247. package/dist/voice/voice-store.js.map +1 -0
  248. package/package.json +71 -0
  249. package/src/components/chat/ask-user-card.tsx +451 -0
  250. package/src/components/chat/brain-markdown.tsx +547 -0
  251. package/src/components/chat/chat-page.tsx +820 -0
  252. package/src/components/chat/command-palette.tsx +68 -0
  253. package/src/components/chat/markdown-content.tsx +5 -0
  254. package/src/components/chat/message-bubble.tsx +362 -0
  255. package/src/components/chat/message-share.ts +78 -0
  256. package/src/components/chat/renderers/claude-tools.tsx +53 -0
  257. package/src/components/chat/renderers/generic.tsx +85 -0
  258. package/src/components/chat/renderers/index.ts +19 -0
  259. package/src/components/chat/risk-hints.ts +95 -0
  260. package/src/components/chat/session-drawer.tsx +213 -0
  261. package/src/components/chat/share-block.tsx +128 -0
  262. package/src/components/chat/tool-call-timeline.tsx +322 -0
  263. package/src/components/chat/tool-views.tsx +898 -0
  264. package/src/components/chat/welcome-state.tsx +105 -0
  265. package/src/components/connectivity/connection-gate.tsx +137 -0
  266. package/src/components/connectivity/login-screen.tsx +169 -0
  267. package/src/components/files/file-panel.tsx +116 -0
  268. package/src/components/files/file-tree.tsx +143 -0
  269. package/src/components/files/file-viewer-binary.tsx +63 -0
  270. package/src/components/files/file-viewer-html.tsx +10 -0
  271. package/src/components/files/file-viewer-markdown.tsx +14 -0
  272. package/src/components/files/file-viewer-raw.tsx +44 -0
  273. package/src/components/files/file-viewer.tsx +290 -0
  274. package/src/components/files/frontmatter-panel.tsx +78 -0
  275. package/src/components/layout/app-shell.tsx +25 -0
  276. package/src/components/layout/mobile-tab-bar.tsx +54 -0
  277. package/src/components/layout/side-rail.tsx +128 -0
  278. package/src/components/layout/slide-panel.tsx +64 -0
  279. package/src/components/quick-actions/streaming-modal.tsx +168 -0
  280. package/src/components/quick-actions/whatsup-modal.tsx +149 -0
  281. package/src/components/settings/passkey-panel.tsx +281 -0
  282. package/src/components/share/share-menu.tsx +121 -0
  283. package/src/components/voice/dictation-sheet.tsx +217 -0
  284. package/src/components/voice/mic-button.tsx +31 -0
  285. package/src/components/voice/review-card.tsx +66 -0
  286. package/src/config.ts +27 -0
  287. package/src/hooks/use-vpn-status.ts +75 -0
  288. package/src/hooks/use-websocket.ts +403 -0
  289. package/src/index.ts +46 -0
  290. package/src/lib/api-client.ts +173 -0
  291. package/src/lib/backend.ts +35 -0
  292. package/src/lib/frontmatter.ts +99 -0
  293. package/src/lib/image-attachments.ts +221 -0
  294. package/src/lib/passkeys.ts +50 -0
  295. package/src/lib/share.ts +167 -0
  296. package/src/lib/strip-markdown.ts +38 -0
  297. package/src/lib/tool-names.ts +27 -0
  298. package/src/lib/utils.ts +6 -0
  299. package/src/lib/ws-client.ts +109 -0
  300. package/src/stores/chat-store.ts +619 -0
  301. package/src/stores/connection-store.ts +23 -0
  302. package/src/stores/file-store.ts +344 -0
  303. package/src/stores/provider-store.ts +85 -0
  304. package/src/stores/ui-store.ts +48 -0
  305. package/src/styles.css +4 -0
  306. package/src/theme.css +425 -0
  307. package/src/voice/asr-clients.ts +45 -0
  308. package/src/voice/asr-deepgram.ts +325 -0
  309. package/src/voice/asr-webspeech.ts +135 -0
  310. package/src/voice/use-dictation.ts +187 -0
  311. package/src/voice/voice-store.ts +55 -0
@@ -0,0 +1,63 @@
1
+ import { FileWarning, Download } from "lucide-react";
2
+ import type { FileContentResponse } from "@schlessera/brain-ui-sdk/protocol";
3
+ import { API_BASE } from "../../lib/backend.js";
4
+
5
+ export function FileViewerBinary({ content }: { content: FileContentResponse }) {
6
+ const isImage = content.mime?.startsWith("image/");
7
+ const rawUrl = `${API_BASE}/files/content?path=${encodeURIComponent(content.path)}&raw=1`;
8
+
9
+ if (isImage) {
10
+ const filename = content.path.split("/").pop() ?? content.path;
11
+ return (
12
+ <div className="flex h-full flex-col items-center justify-center gap-3 p-6">
13
+ <div className="flex max-h-full max-w-full overflow-auto rounded-lg border border-border bg-surface p-2">
14
+ <img src={rawUrl} alt={content.path} className="max-h-[70vh] max-w-full object-contain" />
15
+ </div>
16
+ <div className="flex flex-wrap items-center justify-center gap-2 text-xs text-muted-foreground">
17
+ <span>
18
+ {content.mime} · {formatSize(content.size)}
19
+ </span>
20
+ <span className="text-muted-foreground/40">·</span>
21
+ <a
22
+ href={rawUrl}
23
+ download={filename}
24
+ target="_blank"
25
+ rel="noopener noreferrer"
26
+ className="inline-flex items-center gap-1 rounded text-muted-foreground transition-colors hover:text-foreground"
27
+ >
28
+ <Download className="h-3 w-3" />
29
+ Download
30
+ </a>
31
+ </div>
32
+ </div>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <div className="m-6 flex flex-col items-start gap-3 rounded-lg border border-border bg-surface p-6">
38
+ <div className="flex items-center gap-2 text-foreground">
39
+ <FileWarning className="h-5 w-5 text-muted-foreground" />
40
+ <span className="font-medium">Preview not available</span>
41
+ </div>
42
+ <div className="text-xs text-muted-foreground">
43
+ {content.mime ?? "unknown"} · {formatSize(content.size)}
44
+ </div>
45
+ <a
46
+ href={rawUrl}
47
+ download={content.path.split("/").pop() ?? content.path}
48
+ target="_blank"
49
+ rel="noopener noreferrer"
50
+ className="mt-1 inline-flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs text-foreground transition-colors hover:bg-surface-raised"
51
+ >
52
+ <Download className="h-3.5 w-3.5" />
53
+ Download
54
+ </a>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function formatSize(bytes: number): string {
60
+ if (bytes < 1024) return `${bytes} B`;
61
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
62
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
63
+ }
@@ -0,0 +1,10 @@
1
+ export function FileViewerHtml({ content }: { content: string }) {
2
+ return (
3
+ <iframe
4
+ sandbox=""
5
+ srcDoc={content}
6
+ className="h-full w-full border-0 bg-white"
7
+ title="HTML preview"
8
+ />
9
+ );
10
+ }
@@ -0,0 +1,14 @@
1
+ import { useMemo } from "react";
2
+ import { BrainMarkdown } from "../chat/brain-markdown.js";
3
+ import { splitFrontmatter } from "../../lib/frontmatter.js";
4
+ import { FrontmatterPanel } from "./frontmatter-panel.js";
5
+
6
+ export function FileViewerMarkdown({ content }: { content: string }) {
7
+ const { fields, body } = useMemo(() => splitFrontmatter(content), [content]);
8
+ return (
9
+ <div className="px-6 py-4">
10
+ {fields.length > 0 && <FrontmatterPanel fields={fields} />}
11
+ <BrainMarkdown content={body} className="brain-prose max-w-none" fileLinks />
12
+ </div>
13
+ );
14
+ }
@@ -0,0 +1,44 @@
1
+ import { useState } from "react";
2
+ import { Copy, Check } from "lucide-react";
3
+
4
+ export function FileViewerRaw({ content, fileName }: { content: string; fileName: string }) {
5
+ const [copied, setCopied] = useState(false);
6
+ const [wrap, setWrap] = useState(false);
7
+
8
+ return (
9
+ <div className="group relative">
10
+ <div className="sticky top-0 z-10 flex items-center justify-end gap-2 border-b border-border/30 bg-surface/80 px-4 py-1.5 backdrop-blur">
11
+ <label className="flex cursor-pointer items-center gap-1.5 text-[11px] text-muted-foreground">
12
+ <input
13
+ type="checkbox"
14
+ checked={wrap}
15
+ onChange={(e) => setWrap(e.target.checked)}
16
+ className="h-3 w-3 accent-primary"
17
+ />
18
+ Wrap
19
+ </label>
20
+ <button
21
+ onClick={() => {
22
+ navigator.clipboard.writeText(content);
23
+ setCopied(true);
24
+ setTimeout(() => setCopied(false), 1500);
25
+ }}
26
+ title="Copy file content"
27
+ className="flex items-center gap-1 rounded px-2 py-0.5 text-[11px] text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
28
+ >
29
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
30
+ {copied ? "Copied" : "Copy"}
31
+ </button>
32
+ </div>
33
+ <pre
34
+ className={
35
+ wrap
36
+ ? "whitespace-pre-wrap break-words p-4 font-[family-name:var(--font-mono)] text-[13px] leading-relaxed text-foreground"
37
+ : "overflow-x-auto p-4 font-[family-name:var(--font-mono)] text-[13px] leading-relaxed text-foreground"
38
+ }
39
+ >
40
+ <code data-filename={fileName}>{content}</code>
41
+ </pre>
42
+ </div>
43
+ );
44
+ }
@@ -0,0 +1,290 @@
1
+ import { Loader2, AlertCircle, Eye, Code, Copy, Check, FolderOpen } from "lucide-react";
2
+ import { useState } from "react";
3
+ import { useFileStore } from "../../stores/file-store.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ import { FileViewerMarkdown } from "./file-viewer-markdown.js";
6
+ import { FileViewerHtml } from "./file-viewer-html.js";
7
+ import { FileViewerRaw } from "./file-viewer-raw.js";
8
+ import { FileViewerBinary } from "./file-viewer-binary.js";
9
+ import { ShareMenu, type ShareOption } from "../share/share-menu.js";
10
+ import { API_BASE } from "../../lib/backend.js";
11
+ import { fetchAsFile, shareFile, renderAndShare } from "../../lib/share.js";
12
+ import { splitFrontmatter } from "../../lib/frontmatter.js";
13
+ import { stripMarkdown } from "../../lib/strip-markdown.js";
14
+ import type { FileContentResponse } from "@schlessera/brain-ui-sdk/protocol";
15
+
16
+ export function FileViewer() {
17
+ const currentPath = useFileStore((s) => s.currentPath);
18
+ const content = useFileStore((s) => s.currentContent);
19
+ const loading = useFileStore((s) => s.contentLoading);
20
+ const error = useFileStore((s) => s.contentError);
21
+ const viewMode = useFileStore((s) => s.viewMode);
22
+ const setViewMode = useFileStore((s) => s.setViewMode);
23
+ const setTreeExpanded = useFileStore((s) => s.setTreeExpanded);
24
+
25
+ if (!currentPath) {
26
+ return (
27
+ <div className="flex h-full flex-col items-center justify-center gap-2 px-6 text-center text-sm text-muted-foreground">
28
+ <FolderOpen className="h-8 w-8 text-muted-foreground/40" />
29
+ <span>Select a file from the tree to view it.</span>
30
+ </div>
31
+ );
32
+ }
33
+
34
+ const fileName = currentPath.split("/").pop() ?? currentPath;
35
+ const previewAvailable = content?.kind === "markdown" || content?.kind === "html";
36
+
37
+ return (
38
+ <div className="flex h-full flex-col">
39
+ <Toolbar
40
+ fileName={fileName}
41
+ fullPath={currentPath}
42
+ size={content?.size}
43
+ viewMode={viewMode}
44
+ setViewMode={setViewMode}
45
+ previewAvailable={previewAvailable}
46
+ content={content ?? null}
47
+ onRevealInTree={() => setTreeExpanded(true)}
48
+ />
49
+
50
+ <div className="flex-1 overflow-auto">
51
+ {loading && (
52
+ <div className="flex items-center justify-center gap-2 py-8 text-sm text-muted-foreground">
53
+ <Loader2 className="h-4 w-4 animate-spin" />
54
+ Loading file...
55
+ </div>
56
+ )}
57
+ {error && (
58
+ <div className="m-4 flex items-start gap-2 rounded-lg border border-destructive/30 bg-destructive/5 p-4 text-sm text-destructive">
59
+ <AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
60
+ <span>{error}</span>
61
+ </div>
62
+ )}
63
+ {!loading && !error && content && <ViewerBody content={content} viewMode={viewMode} />}
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function ViewerBody({ content, viewMode }: { content: NonNullable<ReturnType<typeof useFileStore.getState>["currentContent"]>; viewMode: "preview" | "raw" }) {
70
+ if (content.kind === "binary") {
71
+ return <FileViewerBinary content={content} />;
72
+ }
73
+ if (viewMode === "raw" || (content.kind !== "markdown" && content.kind !== "html")) {
74
+ return <FileViewerRaw content={content.content ?? ""} fileName={content.path} />;
75
+ }
76
+ if (content.kind === "markdown") {
77
+ return <FileViewerMarkdown content={content.content ?? ""} />;
78
+ }
79
+ return <FileViewerHtml content={content.content ?? ""} />;
80
+ }
81
+
82
+ function Toolbar({
83
+ fileName,
84
+ fullPath,
85
+ size,
86
+ viewMode,
87
+ setViewMode,
88
+ previewAvailable,
89
+ content,
90
+ onRevealInTree,
91
+ }: {
92
+ fileName: string;
93
+ fullPath: string;
94
+ size?: number;
95
+ viewMode: "preview" | "raw";
96
+ setViewMode: (m: "preview" | "raw") => void;
97
+ previewAvailable: boolean;
98
+ content: FileContentResponse | null;
99
+ onRevealInTree: () => void;
100
+ }) {
101
+ const [copied, setCopied] = useState(false);
102
+ const shareOptions = content ? buildFileShareOptions(content, fileName) : [];
103
+ return (
104
+ <div className="flex items-center gap-2 border-b border-border bg-surface px-4 py-2">
105
+ <button
106
+ onClick={onRevealInTree}
107
+ title="Reveal in tree"
108
+ className="rounded p-1 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
109
+ >
110
+ <FolderOpen className="h-3.5 w-3.5" />
111
+ </button>
112
+ <div className="min-w-0 flex-1">
113
+ <div className="truncate text-sm font-medium text-foreground" title={fullPath}>
114
+ {fileName}
115
+ </div>
116
+ <div className="truncate text-[10px] text-muted-foreground" title={fullPath}>
117
+ {fullPath}
118
+ {size !== undefined && <span className="ml-2">· {formatSize(size)}</span>}
119
+ </div>
120
+ </div>
121
+ <button
122
+ onClick={() => {
123
+ navigator.clipboard.writeText(fullPath);
124
+ setCopied(true);
125
+ setTimeout(() => setCopied(false), 1500);
126
+ }}
127
+ title="Copy path"
128
+ className="rounded p-1.5 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
129
+ >
130
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
131
+ </button>
132
+ {shareOptions.length > 0 && <ShareMenu options={shareOptions} title="Share" />}
133
+ {previewAvailable && (
134
+ <div className="flex items-center rounded-md border border-border bg-background">
135
+ <ModeButton active={viewMode === "preview"} onClick={() => setViewMode("preview")} title="Preview">
136
+ <Eye className="h-3.5 w-3.5" />
137
+ </ModeButton>
138
+ <ModeButton active={viewMode === "raw"} onClick={() => setViewMode("raw")} title="Raw">
139
+ <Code className="h-3.5 w-3.5" />
140
+ </ModeButton>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ }
146
+
147
+ function ModeButton({ active, onClick, title, children }: { active: boolean; onClick: () => void; title: string; children: React.ReactNode }) {
148
+ return (
149
+ <button
150
+ onClick={onClick}
151
+ title={title}
152
+ className={cn(
153
+ "flex h-7 w-7 items-center justify-center transition-colors",
154
+ active ? "bg-surface-raised text-primary" : "text-muted-foreground hover:text-foreground"
155
+ )}
156
+ >
157
+ {children}
158
+ </button>
159
+ );
160
+ }
161
+
162
+ function formatSize(bytes: number): string {
163
+ if (bytes < 1024) return `${bytes} B`;
164
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
165
+ return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
166
+ }
167
+
168
+ function buildFileShareOptions(content: FileContentResponse, fileName: string): ShareOption[] {
169
+ const rawUrl = `${API_BASE}/files/content?path=${encodeURIComponent(content.path)}&raw=1`;
170
+
171
+ // Single-action share for previewable images / PDFs: ship the bytes as-is.
172
+ if (content.kind === "binary") {
173
+ if (content.mime?.startsWith("image/") || content.mime === "application/pdf") {
174
+ return [
175
+ {
176
+ id: "file",
177
+ label: "Share file",
178
+ run: async () => {
179
+ const file = await fetchAsFile(rawUrl, fileName, content.mime);
180
+ return shareFile(file, { title: fileName });
181
+ },
182
+ },
183
+ ];
184
+ }
185
+ // Non-previewable binary: no share menu (user can still use Download link).
186
+ return [];
187
+ }
188
+
189
+ if (content.kind === "markdown") {
190
+ const md = content.content ?? "";
191
+ const { body } = splitFrontmatter(md);
192
+ return [
193
+ {
194
+ id: "md-file",
195
+ label: "Share as .md file",
196
+ hint: "Original markdown source",
197
+ run: async () => {
198
+ const file = new File([md], fileName.endsWith(".md") ? fileName : `${fileName}.md`, {
199
+ type: "text/markdown",
200
+ });
201
+ return shareFile(file, { title: fileName });
202
+ },
203
+ },
204
+ {
205
+ id: "md-text",
206
+ label: "Share body as plain text",
207
+ hint: "Frontmatter dropped, markdown stripped",
208
+ run: async () => {
209
+ const file = new File([stripMarkdown(body)], `${baseName(fileName)}.txt`, {
210
+ type: "text/plain",
211
+ });
212
+ return shareFile(file, { title: fileName });
213
+ },
214
+ },
215
+ {
216
+ id: "md-png",
217
+ label: "Share as image",
218
+ hint: "Rendered PNG snapshot",
219
+ run: () =>
220
+ renderAndShare({
221
+ content: body,
222
+ contentType: "markdown",
223
+ format: "png",
224
+ filename: baseName(fileName),
225
+ title: fileName,
226
+ }),
227
+ },
228
+ {
229
+ id: "md-pdf",
230
+ label: "Share as PDF",
231
+ hint: "Vector PDF, A4",
232
+ run: () =>
233
+ renderAndShare({
234
+ content: body,
235
+ contentType: "markdown",
236
+ format: "pdf",
237
+ filename: baseName(fileName),
238
+ title: fileName,
239
+ }),
240
+ },
241
+ ];
242
+ }
243
+
244
+ if (content.kind === "html") {
245
+ const html = content.content ?? "";
246
+ return [
247
+ {
248
+ id: "html-file",
249
+ label: "Share as .html file",
250
+ run: async () => {
251
+ const file = new File([html], fileName.endsWith(".html") ? fileName : `${fileName}.html`, {
252
+ type: "text/html",
253
+ });
254
+ return shareFile(file, { title: fileName });
255
+ },
256
+ },
257
+ {
258
+ id: "html-png",
259
+ label: "Share as image",
260
+ run: () =>
261
+ renderAndShare({
262
+ content: html,
263
+ contentType: "html",
264
+ format: "png",
265
+ filename: baseName(fileName),
266
+ title: fileName,
267
+ }),
268
+ },
269
+ {
270
+ id: "html-pdf",
271
+ label: "Share as PDF",
272
+ run: () =>
273
+ renderAndShare({
274
+ content: html,
275
+ contentType: "html",
276
+ format: "pdf",
277
+ filename: baseName(fileName),
278
+ title: fileName,
279
+ }),
280
+ },
281
+ ];
282
+ }
283
+
284
+ // Raw text / unknown: no share for V1 (user limited to previewable types).
285
+ return [];
286
+ }
287
+
288
+ function baseName(name: string): string {
289
+ return name.replace(/\.[a-z0-9]{1,8}$/i, "");
290
+ }
@@ -0,0 +1,78 @@
1
+ import { ChevronRight } from "lucide-react";
2
+ import type { FrontmatterField } from "../../lib/frontmatter.js";
3
+ import { useFileStore } from "../../stores/file-store.js";
4
+ import { cn } from "../../lib/utils.js";
5
+
6
+ export function FrontmatterPanel({ fields }: { fields: FrontmatterField[] }) {
7
+ const collapsed = useFileStore((s) => s.frontmatterCollapsed);
8
+ const toggle = useFileStore((s) => s.toggleFrontmatter);
9
+
10
+ if (fields.length === 0) return null;
11
+
12
+ return (
13
+ <div className="mb-4 overflow-hidden rounded-md border border-border/60 bg-surface/50">
14
+ <button
15
+ type="button"
16
+ onClick={toggle}
17
+ aria-expanded={!collapsed}
18
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-left font-[family-name:var(--font-mono)] text-[11px] uppercase tracking-wider text-muted-foreground transition-colors hover:bg-surface-raised"
19
+ >
20
+ <ChevronRight
21
+ className={cn(
22
+ "h-3 w-3 transition-transform",
23
+ !collapsed && "rotate-90"
24
+ )}
25
+ />
26
+ <span>frontmatter</span>
27
+ <span className="text-muted-foreground/50">·</span>
28
+ <span>{fields.length} {fields.length === 1 ? "field" : "fields"}</span>
29
+ {collapsed && (
30
+ <span className="ml-auto truncate text-muted-foreground/40">
31
+ {fields.slice(0, 3).map((f) => f.key).join(", ")}
32
+ {fields.length > 3 ? "…" : ""}
33
+ </span>
34
+ )}
35
+ </button>
36
+
37
+ {!collapsed && (
38
+ <table className="w-full border-collapse text-xs">
39
+ <tbody>
40
+ {fields.map((f) => (
41
+ <tr
42
+ key={f.key}
43
+ className="border-t border-border/30 align-top first:border-t-0"
44
+ >
45
+ <th
46
+ scope="row"
47
+ className="w-32 max-w-[40%] whitespace-nowrap px-3 py-1.5 text-right font-[family-name:var(--font-mono)] font-normal text-muted-foreground"
48
+ >
49
+ {f.key}
50
+ </th>
51
+ <td className="px-3 py-1.5 text-foreground/90">
52
+ {f.list ? (
53
+ <div className="flex flex-wrap gap-1">
54
+ {f.list.map((item, i) => (
55
+ <span
56
+ key={`${item}-${i}`}
57
+ className="rounded border border-border bg-background px-1.5 py-[1px] font-[family-name:var(--font-mono)] text-[11px] text-muted-foreground"
58
+ >
59
+ {item}
60
+ </span>
61
+ ))}
62
+ </div>
63
+ ) : (
64
+ <span className="font-[family-name:var(--font-mono)] text-[11px] leading-relaxed">
65
+ {f.value || (
66
+ <span className="text-muted-foreground/40">—</span>
67
+ )}
68
+ </span>
69
+ )}
70
+ </td>
71
+ </tr>
72
+ ))}
73
+ </tbody>
74
+ </table>
75
+ )}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+ import { SideRail } from "./side-rail.js";
3
+ import { MobileTabBar } from "./mobile-tab-bar.js";
4
+ import { useChatStore, activeChat } from "../../stores/chat-store.js";
5
+ import { cn } from "../../lib/utils.js";
6
+
7
+ export function AppShell({ children }: { children: ReactNode }) {
8
+ const isStreaming = useChatStore((s) => activeChat(s).isStreaming);
9
+
10
+ return (
11
+ <div className="flex h-[100dvh] bg-background text-foreground">
12
+ <SideRail />
13
+ <div className="flex flex-1 flex-col overflow-hidden">
14
+ {/* Amber filament line */}
15
+ <div
16
+ className={cn("filament", isStreaming && "filament--active")}
17
+ />
18
+ <main className="flex flex-1 flex-col overflow-hidden pb-16 md:pb-0">
19
+ {children}
20
+ </main>
21
+ </div>
22
+ <MobileTabBar />
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,54 @@
1
+ import {
2
+ Brain,
3
+ Newspaper,
4
+ RefreshCw,
5
+ History,
6
+ KeyRound,
7
+ FolderTree,
8
+ } from "lucide-react";
9
+ import { useUIStore } from "../../stores/ui-store.js";
10
+ import { cn } from "../../lib/utils.js";
11
+
12
+ export function MobileTabBar() {
13
+ const toggleSessionPanel = useUIStore((s) => s.toggleSessionPanel);
14
+ const toggleSyncPanel = useUIStore((s) => s.toggleSyncPanel);
15
+ const toggleWhatsupPanel = useUIStore((s) => s.toggleWhatsupPanel);
16
+ const toggleFilePanel = useUIStore((s) => s.toggleFilePanel);
17
+ const toggleSecurityPanel = useUIStore((s) => s.toggleSecurityPanel);
18
+
19
+ return (
20
+ <nav className="md:hidden fixed bottom-0 inset-x-0 z-30 flex h-14 items-center justify-around border-t border-border bg-surface px-1 pb-[env(safe-area-inset-bottom)]">
21
+ <TabIcon icon={Brain} label="Chat" active />
22
+ <TabIcon icon={Newspaper} label="Brief" onClick={toggleWhatsupPanel} />
23
+ <TabIcon icon={FolderTree} label="Files" onClick={toggleFilePanel} />
24
+ <TabIcon icon={RefreshCw} label="Sync" onClick={toggleSyncPanel} />
25
+ <TabIcon icon={History} label="History" onClick={toggleSessionPanel} />
26
+ <TabIcon icon={KeyRound} label="Security" onClick={toggleSecurityPanel} />
27
+ </nav>
28
+ );
29
+ }
30
+
31
+ function TabIcon({
32
+ icon: Icon,
33
+ label,
34
+ active,
35
+ onClick,
36
+ }: {
37
+ icon: typeof Brain;
38
+ label: string;
39
+ active?: boolean;
40
+ onClick?: () => void;
41
+ }) {
42
+ return (
43
+ <button
44
+ onClick={onClick}
45
+ className={cn(
46
+ "flex flex-col items-center gap-0.5 px-2 py-1 text-[10px]",
47
+ active ? "text-primary" : "text-muted-foreground"
48
+ )}
49
+ >
50
+ <Icon className="h-5 w-5" />
51
+ {label}
52
+ </button>
53
+ );
54
+ }