@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,128 @@
1
+ import {
2
+ Brain,
3
+ RefreshCw,
4
+ Newspaper,
5
+ History,
6
+ KeyRound,
7
+ SquarePen,
8
+ FolderTree,
9
+ } from "lucide-react";
10
+ import { useConnectionStore } from "../../stores/connection-store.js";
11
+ import { useUIStore } from "../../stores/ui-store.js";
12
+ import { useChatStore, activeChat } from "../../stores/chat-store.js";
13
+ import { cn } from "../../lib/utils.js";
14
+
15
+ export function SideRail() {
16
+ const wsStatus = useConnectionStore((s) => s.wsStatus);
17
+ const toggleSessionPanel = useUIStore((s) => s.toggleSessionPanel);
18
+ const toggleSyncPanel = useUIStore((s) => s.toggleSyncPanel);
19
+ const toggleWhatsupPanel = useUIStore((s) => s.toggleWhatsupPanel);
20
+ const toggleFilePanel = useUIStore((s) => s.toggleFilePanel);
21
+ const toggleSecurityPanel = useUIStore((s) => s.toggleSecurityPanel);
22
+ const clearMessages = useChatStore((s) => s.clearMessages);
23
+ const hasMessages = useChatStore((s) => activeChat(s).messages.length > 0);
24
+ const isStreaming = useChatStore((s) => activeChat(s).isStreaming);
25
+
26
+ return (
27
+ <nav className="hidden md:flex w-16 shrink-0 flex-col items-center border-r border-border bg-surface py-4 gap-1">
28
+ {/* Logo */}
29
+ <div className="mb-4">
30
+ <Brain className="h-7 w-7 text-primary" />
31
+ </div>
32
+
33
+ {/* New chat */}
34
+ {hasMessages && (
35
+ <RailButton
36
+ icon={SquarePen}
37
+ label="New chat"
38
+ onClick={clearMessages}
39
+ />
40
+ )}
41
+
42
+ {/* Divider */}
43
+ <div className="my-2 h-px w-8 bg-border" />
44
+
45
+ {/* Quick actions */}
46
+ <RailButton
47
+ icon={RefreshCw}
48
+ label="Sync"
49
+ onClick={toggleSyncPanel}
50
+ disabled={isStreaming}
51
+ />
52
+ <RailButton
53
+ icon={Newspaper}
54
+ label="Whatsup"
55
+ onClick={toggleWhatsupPanel}
56
+ disabled={isStreaming}
57
+ />
58
+ <RailButton
59
+ icon={FolderTree}
60
+ label="Files"
61
+ onClick={toggleFilePanel}
62
+ />
63
+
64
+ {/* Spacer */}
65
+ <div className="flex-1" />
66
+
67
+ {/* History */}
68
+ <RailButton
69
+ icon={History}
70
+ label="Sessions"
71
+ onClick={toggleSessionPanel}
72
+ />
73
+
74
+ {/* Security (passkeys, sign out) */}
75
+ <RailButton
76
+ icon={KeyRound}
77
+ label="Security"
78
+ onClick={toggleSecurityPanel}
79
+ />
80
+
81
+ {/* Connection status */}
82
+ <div className="mt-2 flex flex-col items-center gap-1">
83
+ <span
84
+ className={cn(
85
+ "inline-block h-2 w-2 rounded-full",
86
+ wsStatus === "connected" && "bg-green-500",
87
+ wsStatus === "connecting" && "bg-primary animate-pulse",
88
+ wsStatus === "disconnected" && "bg-destructive"
89
+ )}
90
+ />
91
+ <span className="text-[10px] text-muted-foreground">
92
+ {wsStatus === "connected"
93
+ ? "Live"
94
+ : wsStatus === "connecting"
95
+ ? "..."
96
+ : "Off"}
97
+ </span>
98
+ </div>
99
+ </nav>
100
+ );
101
+ }
102
+
103
+ function RailButton({
104
+ icon: Icon,
105
+ label,
106
+ onClick,
107
+ disabled,
108
+ }: {
109
+ icon: typeof Brain;
110
+ label: string;
111
+ onClick: () => void;
112
+ disabled?: boolean;
113
+ }) {
114
+ return (
115
+ <button
116
+ onClick={onClick}
117
+ disabled={disabled}
118
+ title={label}
119
+ className="group relative flex h-10 w-10 items-center justify-center rounded-lg text-muted-foreground transition-all duration-150 hover:bg-surface-raised hover:text-foreground hover:scale-110 disabled:opacity-40 disabled:hover:scale-100"
120
+ >
121
+ <Icon className="h-4.5 w-4.5" />
122
+ {/* Tooltip */}
123
+ <span className="pointer-events-none absolute left-full ml-2 whitespace-nowrap rounded-md bg-surface-overlay px-2.5 py-1 text-xs font-medium text-foreground opacity-0 shadow-lg transition-opacity group-hover:opacity-100 border border-border">
124
+ {label}
125
+ </span>
126
+ </button>
127
+ );
128
+ }
@@ -0,0 +1,64 @@
1
+ import { useEffect, type ReactNode } from "react";
2
+ import { X } from "lucide-react";
3
+ import { cn } from "../../lib/utils.js";
4
+
5
+ export function SlidePanel({
6
+ open,
7
+ onClose,
8
+ title,
9
+ wide,
10
+ children,
11
+ }: {
12
+ open: boolean;
13
+ onClose: () => void;
14
+ title: string;
15
+ wide?: boolean;
16
+ children: ReactNode;
17
+ }) {
18
+ useEffect(() => {
19
+ if (!open) return;
20
+ const handler = (e: KeyboardEvent) => {
21
+ if (e.key === "Escape") onClose();
22
+ };
23
+ document.addEventListener("keydown", handler);
24
+ return () => document.removeEventListener("keydown", handler);
25
+ }, [open, onClose]);
26
+
27
+ return (
28
+ <>
29
+ {/* Backdrop */}
30
+ {open && (
31
+ <div
32
+ className="fixed inset-0 z-40 bg-black/40 transition-opacity md:bg-black/20"
33
+ onClick={onClose}
34
+ />
35
+ )}
36
+
37
+ {/* Panel */}
38
+ <div
39
+ className={cn(
40
+ "fixed right-0 top-0 z-50 flex h-full flex-col border-l border-border bg-surface shadow-[0_16px_48px_rgba(0,0,0,0.5)]",
41
+ "transform transition-transform duration-300 ease-out",
42
+ open ? "translate-x-0" : "translate-x-full",
43
+ wide ? "w-full md:w-[480px]" : "w-full md:w-80"
44
+ )}
45
+ >
46
+ {/* Header */}
47
+ <div className="flex items-center justify-between border-b border-border px-5 py-4">
48
+ <h2 className="font-[family-name:var(--font-display)] text-lg text-foreground">
49
+ {title}
50
+ </h2>
51
+ <button
52
+ onClick={onClose}
53
+ className="rounded-lg p-2.5 md:p-1.5 text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
54
+ >
55
+ <X className="h-5 w-5 md:h-4 md:w-4" />
56
+ </button>
57
+ </div>
58
+
59
+ {/* Content */}
60
+ <div className="flex-1 overflow-y-auto">{children}</div>
61
+ </div>
62
+ </>
63
+ );
64
+ }
@@ -0,0 +1,168 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { Loader2, CheckCircle2, XCircle } from "lucide-react";
3
+ import { SlidePanel } from "../layout/slide-panel.js";
4
+ import { linkifyPaths } from "../chat/brain-markdown.js";
5
+
6
+ type StreamState = "idle" | "running" | "success" | "error" | "cancelled";
7
+
8
+ export function StreamingPanel({
9
+ open,
10
+ onClose,
11
+ title,
12
+ endpoint,
13
+ method = "POST",
14
+ }: {
15
+ open: boolean;
16
+ onClose: () => void;
17
+ title: string;
18
+ endpoint: string;
19
+ method?: string;
20
+ }) {
21
+ const [state, setState] = useState<StreamState>("idle");
22
+ const [lines, setLines] = useState<string[]>([]);
23
+ const scrollRef = useRef<HTMLDivElement>(null);
24
+ const controllerRef = useRef<AbortController | null>(null);
25
+
26
+ useEffect(() => {
27
+ if (!open) return;
28
+
29
+ setState("running");
30
+ setLines([]);
31
+
32
+ const controller = new AbortController();
33
+ controllerRef.current = controller;
34
+
35
+ (async () => {
36
+ try {
37
+ const res = await fetch(endpoint, {
38
+ method,
39
+ signal: controller.signal,
40
+ });
41
+
42
+ if (!res.ok || !res.body) {
43
+ setState("error");
44
+ setLines((l) => [...l, `HTTP ${res.status}: ${res.statusText}`]);
45
+ return;
46
+ }
47
+
48
+ const reader = res.body.getReader();
49
+ const decoder = new TextDecoder();
50
+ let buffer = "";
51
+
52
+ while (true) {
53
+ const { done, value } = await reader.read();
54
+ if (done) break;
55
+
56
+ buffer += decoder.decode(value, { stream: true });
57
+ const events = buffer.split("\n\n");
58
+ buffer = events.pop() || "";
59
+
60
+ for (const event of events) {
61
+ const dataLine = event
62
+ .split("\n")
63
+ .find((l) => l.startsWith("data: "));
64
+ if (!dataLine) continue;
65
+
66
+ try {
67
+ const data = JSON.parse(dataLine.slice(6));
68
+ if (data.text !== undefined) {
69
+ setLines((l) => [...l, data.text]);
70
+ }
71
+ if (data.type === "done") {
72
+ setState(data.success ? "success" : "error");
73
+ }
74
+ } catch {}
75
+ }
76
+ }
77
+ } catch (err: any) {
78
+ if (err.name === "AbortError") {
79
+ setLines((l) => [...l, "Cancelled."]);
80
+ setState("cancelled");
81
+ } else {
82
+ setState("error");
83
+ setLines((l) => [...l, `Error: ${err.message}`]);
84
+ }
85
+ } finally {
86
+ controllerRef.current = null;
87
+ }
88
+ })();
89
+
90
+ return () => {
91
+ controller.abort();
92
+ controllerRef.current = null;
93
+ };
94
+ }, [open, endpoint, method]);
95
+
96
+ // Auto-scroll
97
+ useEffect(() => {
98
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
99
+ }, [lines]);
100
+
101
+ const isRunning = state === "running";
102
+
103
+ return (
104
+ <SlidePanel
105
+ open={open}
106
+ onClose={isRunning ? () => {} : onClose}
107
+ title={title}
108
+ >
109
+ <div className="flex h-full flex-col">
110
+ {/* Status bar */}
111
+ <div className="flex items-center gap-2 border-b border-border px-5 py-2">
112
+ {isRunning && (
113
+ <Loader2 className="h-3.5 w-3.5 animate-spin text-primary" />
114
+ )}
115
+ {state === "success" && (
116
+ <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
117
+ )}
118
+ {(state === "error" || state === "cancelled") && (
119
+ <XCircle className="h-3.5 w-3.5 text-destructive" />
120
+ )}
121
+ <span className="text-xs text-muted-foreground">
122
+ {isRunning
123
+ ? "Running..."
124
+ : state === "success"
125
+ ? "Complete"
126
+ : state === "error"
127
+ ? "Failed"
128
+ : state === "cancelled"
129
+ ? "Cancelled"
130
+ : "Ready"}
131
+ </span>
132
+ </div>
133
+
134
+ {/* Log output */}
135
+ <div className="flex-1 overflow-y-auto p-4 font-[family-name:var(--font-mono)] text-xs leading-relaxed text-muted-foreground">
136
+ {lines.map((line, i) => (
137
+ <div key={i} className="whitespace-pre-wrap">
138
+ {linkifyPaths(line)}
139
+ </div>
140
+ ))}
141
+ {isRunning && lines.length === 0 && (
142
+ <div className="text-muted-foreground/50">Starting...</div>
143
+ )}
144
+ <div ref={scrollRef} />
145
+ </div>
146
+
147
+ {/* Footer */}
148
+ <div className="border-t border-border px-5 py-3 flex justify-end">
149
+ {isRunning ? (
150
+ <button
151
+ onClick={() => controllerRef.current?.abort()}
152
+ className="rounded-lg border border-destructive/30 px-4 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/10"
153
+ >
154
+ Cancel
155
+ </button>
156
+ ) : (
157
+ <button
158
+ onClick={onClose}
159
+ className="rounded-lg bg-surface-raised px-4 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-surface-overlay"
160
+ >
161
+ Close
162
+ </button>
163
+ )}
164
+ </div>
165
+ </div>
166
+ </SlidePanel>
167
+ );
168
+ }
@@ -0,0 +1,149 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import { Loader2, XCircle } from "lucide-react";
3
+ import { SlidePanel } from "../layout/slide-panel.js";
4
+ import { BrainMarkdown } from "../chat/brain-markdown.js";
5
+ import { getBackendUrl } from "../../lib/backend.js";
6
+
7
+ type State = "loading" | "done" | "error" | "cancelled";
8
+
9
+ export function WhatsupPanel({
10
+ open,
11
+ onClose,
12
+ }: {
13
+ open: boolean;
14
+ onClose: () => void;
15
+ }) {
16
+ const [state, setState] = useState<State>("loading");
17
+ const [content, setContent] = useState("");
18
+ const controllerRef = useRef<AbortController | null>(null);
19
+
20
+ useEffect(() => {
21
+ if (!open) return;
22
+
23
+ setState("loading");
24
+ setContent("");
25
+
26
+ const controller = new AbortController();
27
+ controllerRef.current = controller;
28
+
29
+ (async () => {
30
+ try {
31
+ const res = await fetch(getBackendUrl("/api/brain/whatsup"), {
32
+ method: "POST",
33
+ signal: controller.signal,
34
+ });
35
+
36
+ if (!res.ok || !res.body) {
37
+ setState("error");
38
+ setContent(`HTTP ${res.status}: ${res.statusText}`);
39
+ return;
40
+ }
41
+
42
+ const reader = res.body.getReader();
43
+ const decoder = new TextDecoder();
44
+ let buffer = "";
45
+ const lines: string[] = [];
46
+
47
+ while (true) {
48
+ const { done, value } = await reader.read();
49
+ if (done) break;
50
+
51
+ buffer += decoder.decode(value, { stream: true });
52
+ const events = buffer.split("\n\n");
53
+ buffer = events.pop() || "";
54
+
55
+ for (const event of events) {
56
+ const dataLine = event
57
+ .split("\n")
58
+ .find((l) => l.startsWith("data: "));
59
+ if (!dataLine) continue;
60
+
61
+ try {
62
+ const data = JSON.parse(dataLine.slice(6));
63
+ if (data.type === "progress" && data.text !== undefined) {
64
+ lines.push(data.text);
65
+ }
66
+ if (data.type === "done") {
67
+ setState(data.success ? "done" : "error");
68
+ }
69
+ } catch {}
70
+ }
71
+ }
72
+
73
+ setContent(lines.join("\n"));
74
+ } catch (err: any) {
75
+ if (err.name === "AbortError") {
76
+ setState("cancelled");
77
+ setContent("Cancelled.");
78
+ } else {
79
+ setState("error");
80
+ setContent(`Error: ${err.message}`);
81
+ }
82
+ } finally {
83
+ controllerRef.current = null;
84
+ }
85
+ })();
86
+
87
+ return () => {
88
+ controller.abort();
89
+ controllerRef.current = null;
90
+ };
91
+ }, [open]);
92
+
93
+ const isLoading = state === "loading";
94
+
95
+ return (
96
+ <SlidePanel
97
+ open={open}
98
+ onClose={onClose}
99
+ title="Whatsup"
100
+ wide
101
+ >
102
+ <div className="flex h-full flex-col">
103
+ {/* Status bar (errors only — loading uses centered indicator) */}
104
+ {(state === "error" || state === "cancelled") && (
105
+ <div className="flex items-center gap-2 border-b border-border px-5 py-2">
106
+ <XCircle className="h-3.5 w-3.5 text-destructive" />
107
+ <span className="text-xs text-muted-foreground">Failed</span>
108
+ </div>
109
+ )}
110
+
111
+ {/* Content */}
112
+ <div className="flex-1 overflow-y-auto p-5">
113
+ {isLoading ? (
114
+ <div className="flex items-center justify-center h-32 text-muted-foreground text-sm">
115
+ <Loader2 className="h-5 w-5 animate-spin mr-2 text-primary" />
116
+ Generating briefing...
117
+ </div>
118
+ ) : (
119
+ <BrainMarkdown
120
+ content={content}
121
+ className="whatsup-briefing brain-prose"
122
+ entityTags
123
+ fileLinks
124
+ />
125
+ )}
126
+ </div>
127
+
128
+ {/* Footer */}
129
+ <div className="border-t border-border px-5 py-3 flex justify-end">
130
+ {isLoading ? (
131
+ <button
132
+ onClick={() => controllerRef.current?.abort()}
133
+ className="rounded-lg border border-destructive/30 px-4 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/10"
134
+ >
135
+ Cancel
136
+ </button>
137
+ ) : (
138
+ <button
139
+ onClick={onClose}
140
+ className="rounded-lg bg-surface-raised px-4 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-surface-overlay"
141
+ >
142
+ Close
143
+ </button>
144
+ )}
145
+ </div>
146
+ </div>
147
+ </SlidePanel>
148
+ );
149
+ }