@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,167 @@
1
+ import { API_BASE } from "./backend.js";
2
+ import type { RenderRequest } from "@schlessera/brain-ui-sdk/protocol";
3
+
4
+ export type ShareKind = "file" | "text" | "richtext";
5
+
6
+ /**
7
+ * True when the browser supports navigator.share at all. Note this doesn't
8
+ * guarantee that a specific payload is shareable — call canShareFiles too.
9
+ */
10
+ export function isShareSupported(): boolean {
11
+ return typeof navigator !== "undefined" && typeof navigator.share === "function";
12
+ }
13
+
14
+ function canShareFiles(files: File[]): boolean {
15
+ if (typeof navigator === "undefined" || typeof navigator.canShare !== "function") return false;
16
+ try {
17
+ return navigator.canShare({ files });
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ function triggerDownload(file: File): void {
24
+ const url = URL.createObjectURL(file);
25
+ const a = document.createElement("a");
26
+ a.href = url;
27
+ a.download = file.name;
28
+ document.body.appendChild(a);
29
+ a.click();
30
+ a.remove();
31
+ setTimeout(() => URL.revokeObjectURL(url), 5_000);
32
+ }
33
+
34
+ /**
35
+ * Share a File via the OS share sheet. Falls back to a browser download
36
+ * when the platform can't share files (desktop Chrome / Firefox).
37
+ *
38
+ * Returns true if the share dialog (or fallback) was triggered, false if
39
+ * the user dismissed it.
40
+ */
41
+ export async function shareFile(
42
+ file: File,
43
+ opts: { title?: string; text?: string } = {}
44
+ ): Promise<boolean> {
45
+ if (isShareSupported() && canShareFiles([file])) {
46
+ try {
47
+ await navigator.share({ files: [file], title: opts.title, text: opts.text });
48
+ return true;
49
+ } catch (err) {
50
+ // AbortError = user cancelled; anything else falls through to download
51
+ if (err instanceof DOMException && err.name === "AbortError") return false;
52
+ console.warn("[share] navigator.share failed, falling back to download:", err);
53
+ }
54
+ }
55
+ triggerDownload(file);
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * Share plain text / a URL via the OS share sheet. Falls back to clipboard
61
+ * copy when unavailable.
62
+ */
63
+ export async function shareText(
64
+ data: { text: string; title?: string; url?: string }
65
+ ): Promise<boolean> {
66
+ if (isShareSupported()) {
67
+ try {
68
+ await navigator.share({ title: data.title, text: data.text, url: data.url });
69
+ return true;
70
+ } catch (err) {
71
+ if (err instanceof DOMException && err.name === "AbortError") return false;
72
+ console.warn("[share] navigator.share text failed, falling back to clipboard:", err);
73
+ }
74
+ }
75
+ try {
76
+ await navigator.clipboard.writeText(data.url ? `${data.text}\n${data.url}` : data.text);
77
+ return true;
78
+ } catch (err) {
79
+ console.warn("[share] clipboard.writeText failed:", err);
80
+ return false;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Copy rich text (HTML + plain) to the clipboard. Best-effort: on browsers
86
+ * that don't support ClipboardItem, falls back to plain text only.
87
+ */
88
+ export async function copyRichText(html: string, plain: string): Promise<boolean> {
89
+ try {
90
+ if (typeof ClipboardItem !== "undefined" && navigator.clipboard?.write) {
91
+ const item = new ClipboardItem({
92
+ "text/html": new Blob([html], { type: "text/html" }),
93
+ "text/plain": new Blob([plain], { type: "text/plain" }),
94
+ });
95
+ await navigator.clipboard.write([item]);
96
+ return true;
97
+ }
98
+ await navigator.clipboard.writeText(plain);
99
+ return true;
100
+ } catch (err) {
101
+ console.warn("[share] copyRichText failed:", err);
102
+ return false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Fetch a URL into a File. Used to wrap the existing
108
+ * /api/files/content?raw=1 endpoint for share-the-bytes flows.
109
+ */
110
+ export async function fetchAsFile(
111
+ url: string,
112
+ filename: string,
113
+ mime?: string
114
+ ): Promise<File> {
115
+ const res = await fetch(url);
116
+ if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
117
+ const blob = await res.blob();
118
+ return new File([blob], filename, { type: mime ?? blob.type ?? "application/octet-stream" });
119
+ }
120
+
121
+ /**
122
+ * POST to /api/render and return the resulting File. Throws on non-2xx.
123
+ */
124
+ export async function renderToFile(
125
+ req: RenderRequest,
126
+ filename: string
127
+ ): Promise<File> {
128
+ const res = await fetch(`${API_BASE}/render`, {
129
+ method: "POST",
130
+ headers: { "Content-Type": "application/json" },
131
+ body: JSON.stringify(req),
132
+ });
133
+ if (!res.ok) {
134
+ let detail = res.statusText;
135
+ try {
136
+ const j = await res.json();
137
+ detail = (j as { error?: string; detail?: string }).detail ?? (j as { error?: string }).error ?? detail;
138
+ } catch {
139
+ // ignore
140
+ }
141
+ throw new Error(`Render failed (${res.status}): ${detail}`);
142
+ }
143
+ const blob = await res.blob();
144
+ const ext = req.format === "png" ? "png" : "pdf";
145
+ const safeBase = filename.replace(/\.(png|pdf|md|html?|txt)$/i, "");
146
+ return new File([blob], `${safeBase}.${ext}`, {
147
+ type: req.format === "png" ? "image/png" : "application/pdf",
148
+ });
149
+ }
150
+
151
+ /**
152
+ * One-call helper: render content to PNG/PDF and dispatch the share sheet.
153
+ */
154
+ export async function renderAndShare(opts: {
155
+ content: string;
156
+ contentType: RenderRequest["contentType"];
157
+ format: RenderRequest["format"];
158
+ filename: string;
159
+ title?: string;
160
+ text?: string;
161
+ }): Promise<boolean> {
162
+ const file = await renderToFile(
163
+ { content: opts.content, contentType: opts.contentType, format: opts.format, title: opts.title },
164
+ opts.filename
165
+ );
166
+ return shareFile(file, { title: opts.title, text: opts.text });
167
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Best-effort markdown → plain text for sharing. Keeps list structure
3
+ * (lines starting with "- ") and headings (as bare lines), drops syntax.
4
+ *
5
+ * Not a full markdown parser; intended for short snippets pasted into
6
+ * messengers. For correctness we'd want to render to HTML and grab
7
+ * textContent, but that requires a DOM round-trip.
8
+ */
9
+ export function stripMarkdown(md: string): string {
10
+ let s = md;
11
+
12
+ // Code fences — keep contents, drop the ``` and language hint
13
+ s = s.replace(/```[a-zA-Z0-9_+-]*\n([\s\S]*?)```/g, (_m, body) => body);
14
+ // Inline code
15
+ s = s.replace(/`([^`\n]+)`/g, "$1");
16
+ // Images: ![alt](url) → alt (or just drop if no alt)
17
+ s = s.replace(/!\[([^\]]*)\]\([^)]*\)/g, (_m, alt) => alt || "");
18
+ // Links: [text](url) → text (url)
19
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, text, url) => {
20
+ return text === url ? text : `${text} (${url})`;
21
+ });
22
+ // Bold / italic / strike — drop markers, keep text
23
+ s = s.replace(/(\*\*|__)(.+?)\1/g, "$2");
24
+ s = s.replace(/(\*|_)(.+?)\1/g, "$2");
25
+ s = s.replace(/~~(.+?)~~/g, "$1");
26
+ // Headings: # text → text
27
+ s = s.replace(/^#{1,6}\s+/gm, "");
28
+ // Blockquotes: leading "> "
29
+ s = s.replace(/^>\s?/gm, "");
30
+ // Horizontal rules
31
+ s = s.replace(/^---+$/gm, "");
32
+ // Wikilinks [[slug]] or [[slug|label]] → label or slug
33
+ s = s.replace(/\[\[([^\[\]\n|]+?)(?:\|([^\[\]\n]+?))?\]\]/g, (_m, target, label) => label?.trim() ?? target.trim());
34
+ // Collapse 3+ blank lines to 2
35
+ s = s.replace(/\n{3,}/g, "\n\n");
36
+
37
+ return s.trim();
38
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * MCP-prefixed names of brain-ui's own in-process tools, as they appear in the
3
+ * tool-call stream. Kept in one place so the timeline, history reconstruction,
4
+ * and per-tool rendering all agree on the identifiers.
5
+ */
6
+ export const ASK_USER_TOOL_NAME = "mcp__brain-ui__ask_user";
7
+ export const GET_LOCATION_TOOL_NAME = "mcp__brain-ui__get_current_location";
8
+
9
+ const LEGACY_MCP_PREFIX = "mcp__brain_ui__";
10
+ const MCP_PREFIX = "mcp__brain-ui__";
11
+
12
+ /**
13
+ * Pre-rename transcripts used the `mcp__brain_ui__` server prefix; the server is
14
+ * now `mcp__brain-ui__`. Normalize the legacy prefix so old tool calls resolve
15
+ * to the same identifiers (special-card renderers, icons) as new ones instead of
16
+ * falling back to a raw JSON blob.
17
+ */
18
+ export function normalizeToolName(name: string): string {
19
+ return name.startsWith(LEGACY_MCP_PREFIX)
20
+ ? MCP_PREFIX + name.slice(LEGACY_MCP_PREFIX.length)
21
+ : name;
22
+ }
23
+
24
+ export const isAskUserTool = (name: string | undefined): boolean =>
25
+ !!name && normalizeToolName(name) === ASK_USER_TOOL_NAME;
26
+ export const isLocationTool = (name: string | undefined): boolean =>
27
+ !!name && normalizeToolName(name) === GET_LOCATION_TOOL_NAME;
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,109 @@
1
+ import type { ClientMessage, ServerMessage } from "@schlessera/brain-ui-sdk/protocol";
2
+
3
+ type MessageHandler = (msg: ServerMessage) => void;
4
+ type StatusHandler = (status: "connecting" | "connected" | "disconnected") => void;
5
+
6
+ export class WSClient {
7
+ private ws: WebSocket | null = null;
8
+ private url: string;
9
+ private onMessage: MessageHandler;
10
+ private onStatusChange: StatusHandler;
11
+ private reconnectAttempt = 0;
12
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
13
+ private closed = false;
14
+
15
+ constructor(
16
+ url: string,
17
+ onMessage: MessageHandler,
18
+ onStatusChange: StatusHandler
19
+ ) {
20
+ this.url = url;
21
+ this.onMessage = onMessage;
22
+ this.onStatusChange = onStatusChange;
23
+ }
24
+
25
+ connect() {
26
+ this.closed = false;
27
+ this.onStatusChange("connecting");
28
+
29
+ try {
30
+ this.ws = new WebSocket(this.url);
31
+
32
+ this.ws.onopen = () => {
33
+ this.reconnectAttempt = 0;
34
+ this.onStatusChange("connected");
35
+ };
36
+
37
+ this.ws.onmessage = (evt) => {
38
+ try {
39
+ const msg = JSON.parse(evt.data) as ServerMessage;
40
+ this.onMessage(msg);
41
+ } catch {
42
+ console.error("[ws] Failed to parse message:", evt.data);
43
+ }
44
+ };
45
+
46
+ this.ws.onclose = () => {
47
+ this.onStatusChange("disconnected");
48
+ if (!this.closed) {
49
+ this.scheduleReconnect();
50
+ }
51
+ };
52
+
53
+ this.ws.onerror = () => {
54
+ // onclose will fire after onerror
55
+ };
56
+ } catch {
57
+ this.onStatusChange("disconnected");
58
+ if (!this.closed) {
59
+ this.scheduleReconnect();
60
+ }
61
+ }
62
+ }
63
+
64
+ send(msg: ClientMessage) {
65
+ if (this.ws?.readyState === WebSocket.OPEN) {
66
+ this.ws.send(JSON.stringify(msg));
67
+ }
68
+ }
69
+
70
+ close() {
71
+ this.closed = true;
72
+ if (this.reconnectTimer) {
73
+ clearTimeout(this.reconnectTimer);
74
+ this.reconnectTimer = null;
75
+ }
76
+ this.ws?.close();
77
+ this.ws = null;
78
+ }
79
+
80
+ get isConnected(): boolean {
81
+ return this.ws?.readyState === WebSocket.OPEN;
82
+ }
83
+
84
+ /**
85
+ * Skip the remaining backoff and reconnect immediately (e.g. when the
86
+ * browser fires an `online` event). No-op if open/connecting or closed
87
+ * deliberately.
88
+ */
89
+ reconnectNow() {
90
+ if (this.closed) return;
91
+ const state = this.ws?.readyState;
92
+ if (state === WebSocket.OPEN || state === WebSocket.CONNECTING) return;
93
+ if (this.reconnectTimer) {
94
+ clearTimeout(this.reconnectTimer);
95
+ this.reconnectTimer = null;
96
+ }
97
+ this.reconnectAttempt = 0;
98
+ this.connect();
99
+ }
100
+
101
+ private scheduleReconnect() {
102
+ const delay = Math.min(1000 * 2 ** this.reconnectAttempt, 30000);
103
+ this.reconnectAttempt++;
104
+ this.reconnectTimer = setTimeout(() => {
105
+ this.reconnectTimer = null;
106
+ this.connect();
107
+ }, delay);
108
+ }
109
+ }