@crafter/rn-ai-elements 0.0.1

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 (344) hide show
  1. package/lib/commonjs/chatbot/AIImage.js +126 -0
  2. package/lib/commonjs/chatbot/AIImage.js.map +1 -0
  3. package/lib/commonjs/chatbot/Attachments.js +317 -0
  4. package/lib/commonjs/chatbot/Attachments.js.map +1 -0
  5. package/lib/commonjs/chatbot/ChatErrorBoundary.js +201 -0
  6. package/lib/commonjs/chatbot/ChatErrorBoundary.js.map +1 -0
  7. package/lib/commonjs/chatbot/ChatMessageItem.js +169 -0
  8. package/lib/commonjs/chatbot/ChatMessageItem.js.map +1 -0
  9. package/lib/commonjs/chatbot/Conversation.js +415 -0
  10. package/lib/commonjs/chatbot/Conversation.js.map +1 -0
  11. package/lib/commonjs/chatbot/ConversationScrollButton.js +131 -0
  12. package/lib/commonjs/chatbot/ConversationScrollButton.js.map +1 -0
  13. package/lib/commonjs/chatbot/Message.js +203 -0
  14. package/lib/commonjs/chatbot/Message.js.map +1 -0
  15. package/lib/commonjs/chatbot/PromptInput.js +352 -0
  16. package/lib/commonjs/chatbot/PromptInput.js.map +1 -0
  17. package/lib/commonjs/chatbot/Reasoning.js +184 -0
  18. package/lib/commonjs/chatbot/Reasoning.js.map +1 -0
  19. package/lib/commonjs/chatbot/Shimmer.js +116 -0
  20. package/lib/commonjs/chatbot/Shimmer.js.map +1 -0
  21. package/lib/commonjs/chatbot/Sources.js +212 -0
  22. package/lib/commonjs/chatbot/Sources.js.map +1 -0
  23. package/lib/commonjs/chatbot/Suggestion.js +99 -0
  24. package/lib/commonjs/chatbot/Suggestion.js.map +1 -0
  25. package/lib/commonjs/chatbot/Tool.js +307 -0
  26. package/lib/commonjs/chatbot/Tool.js.map +1 -0
  27. package/lib/commonjs/chatbot/adapters/uiMessageAdapter.js +141 -0
  28. package/lib/commonjs/chatbot/adapters/uiMessageAdapter.js.map +1 -0
  29. package/lib/commonjs/chatbot/index.js +140 -0
  30. package/lib/commonjs/chatbot/index.js.map +1 -0
  31. package/lib/commonjs/chatbot/types.js +6 -0
  32. package/lib/commonjs/chatbot/types.js.map +1 -0
  33. package/lib/commonjs/hooks/index.js +34 -0
  34. package/lib/commonjs/hooks/index.js.map +1 -0
  35. package/lib/commonjs/hooks/useAutoScroll.js +39 -0
  36. package/lib/commonjs/hooks/useAutoScroll.js.map +1 -0
  37. package/lib/commonjs/hooks/useClipboard.js +44 -0
  38. package/lib/commonjs/hooks/useClipboard.js.map +1 -0
  39. package/lib/commonjs/hooks/useCollapsible.js +35 -0
  40. package/lib/commonjs/hooks/useCollapsible.js.map +1 -0
  41. package/lib/commonjs/hooks/useStickToBottom.js +68 -0
  42. package/lib/commonjs/hooks/useStickToBottom.js.map +1 -0
  43. package/lib/commonjs/index.js +257 -0
  44. package/lib/commonjs/index.js.map +1 -0
  45. package/lib/commonjs/package.json +1 -0
  46. package/lib/commonjs/primitives/Badge.js +119 -0
  47. package/lib/commonjs/primitives/Badge.js.map +1 -0
  48. package/lib/commonjs/primitives/Button.js +185 -0
  49. package/lib/commonjs/primitives/Button.js.map +1 -0
  50. package/lib/commonjs/primitives/Card.js +166 -0
  51. package/lib/commonjs/primitives/Card.js.map +1 -0
  52. package/lib/commonjs/primitives/Collapsible.js +137 -0
  53. package/lib/commonjs/primitives/Collapsible.js.map +1 -0
  54. package/lib/commonjs/primitives/ScrollArea.js +40 -0
  55. package/lib/commonjs/primitives/ScrollArea.js.map +1 -0
  56. package/lib/commonjs/primitives/index.js +83 -0
  57. package/lib/commonjs/primitives/index.js.map +1 -0
  58. package/lib/commonjs/streaming/StreamingMarkdown.js +252 -0
  59. package/lib/commonjs/streaming/StreamingMarkdown.js.map +1 -0
  60. package/lib/commonjs/streaming/index.js +13 -0
  61. package/lib/commonjs/streaming/index.js.map +1 -0
  62. package/lib/commonjs/streaming/parser.js +482 -0
  63. package/lib/commonjs/streaming/parser.js.map +1 -0
  64. package/lib/commonjs/streaming/renderers/BlockquoteRenderer.js +35 -0
  65. package/lib/commonjs/streaming/renderers/BlockquoteRenderer.js.map +1 -0
  66. package/lib/commonjs/streaming/renderers/CodeRenderer.js +128 -0
  67. package/lib/commonjs/streaming/renderers/CodeRenderer.js.map +1 -0
  68. package/lib/commonjs/streaming/renderers/HeadingRenderer.js +61 -0
  69. package/lib/commonjs/streaming/renderers/HeadingRenderer.js.map +1 -0
  70. package/lib/commonjs/streaming/renderers/ImageRenderer.js +53 -0
  71. package/lib/commonjs/streaming/renderers/ImageRenderer.js.map +1 -0
  72. package/lib/commonjs/streaming/renderers/LinkRenderer.js +49 -0
  73. package/lib/commonjs/streaming/renderers/LinkRenderer.js.map +1 -0
  74. package/lib/commonjs/streaming/renderers/ListRenderer.js +63 -0
  75. package/lib/commonjs/streaming/renderers/ListRenderer.js.map +1 -0
  76. package/lib/commonjs/streaming/renderers/TableRenderer.js +77 -0
  77. package/lib/commonjs/streaming/renderers/TableRenderer.js.map +1 -0
  78. package/lib/commonjs/streaming/renderers/TextRenderer.js +41 -0
  79. package/lib/commonjs/streaming/renderers/TextRenderer.js.map +1 -0
  80. package/lib/commonjs/streaming/renderers/index.js +76 -0
  81. package/lib/commonjs/streaming/renderers/index.js.map +1 -0
  82. package/lib/commonjs/streaming/renderers/renderInlineChildren.js +112 -0
  83. package/lib/commonjs/streaming/renderers/renderInlineChildren.js.map +1 -0
  84. package/lib/commonjs/streaming/renderers/renderNode.js +81 -0
  85. package/lib/commonjs/streaming/renderers/renderNode.js.map +1 -0
  86. package/lib/commonjs/theme/ThemeProvider.js +68 -0
  87. package/lib/commonjs/theme/ThemeProvider.js.map +1 -0
  88. package/lib/commonjs/theme/defaultTheme.js +96 -0
  89. package/lib/commonjs/theme/defaultTheme.js.map +1 -0
  90. package/lib/commonjs/theme/index.js +32 -0
  91. package/lib/commonjs/theme/index.js.map +1 -0
  92. package/lib/commonjs/theme/tokens.js +2 -0
  93. package/lib/commonjs/theme/tokens.js.map +1 -0
  94. package/lib/commonjs/types.d.js +2 -0
  95. package/lib/commonjs/types.d.js.map +1 -0
  96. package/lib/commonjs/voice/index.js +13 -0
  97. package/lib/commonjs/voice/index.js.map +1 -0
  98. package/lib/commonjs/voice/useSpeechRecognition.js +172 -0
  99. package/lib/commonjs/voice/useSpeechRecognition.js.map +1 -0
  100. package/lib/module/chatbot/AIImage.js +121 -0
  101. package/lib/module/chatbot/AIImage.js.map +1 -0
  102. package/lib/module/chatbot/Attachments.js +312 -0
  103. package/lib/module/chatbot/Attachments.js.map +1 -0
  104. package/lib/module/chatbot/ChatErrorBoundary.js +196 -0
  105. package/lib/module/chatbot/ChatErrorBoundary.js.map +1 -0
  106. package/lib/module/chatbot/ChatMessageItem.js +164 -0
  107. package/lib/module/chatbot/ChatMessageItem.js.map +1 -0
  108. package/lib/module/chatbot/Conversation.js +412 -0
  109. package/lib/module/chatbot/Conversation.js.map +1 -0
  110. package/lib/module/chatbot/ConversationScrollButton.js +126 -0
  111. package/lib/module/chatbot/ConversationScrollButton.js.map +1 -0
  112. package/lib/module/chatbot/Message.js +198 -0
  113. package/lib/module/chatbot/Message.js.map +1 -0
  114. package/lib/module/chatbot/PromptInput.js +347 -0
  115. package/lib/module/chatbot/PromptInput.js.map +1 -0
  116. package/lib/module/chatbot/Reasoning.js +179 -0
  117. package/lib/module/chatbot/Reasoning.js.map +1 -0
  118. package/lib/module/chatbot/Shimmer.js +111 -0
  119. package/lib/module/chatbot/Shimmer.js.map +1 -0
  120. package/lib/module/chatbot/Sources.js +207 -0
  121. package/lib/module/chatbot/Sources.js.map +1 -0
  122. package/lib/module/chatbot/Suggestion.js +94 -0
  123. package/lib/module/chatbot/Suggestion.js.map +1 -0
  124. package/lib/module/chatbot/Tool.js +303 -0
  125. package/lib/module/chatbot/Tool.js.map +1 -0
  126. package/lib/module/chatbot/adapters/uiMessageAdapter.js +137 -0
  127. package/lib/module/chatbot/adapters/uiMessageAdapter.js.map +1 -0
  128. package/lib/module/chatbot/index.js +39 -0
  129. package/lib/module/chatbot/index.js.map +1 -0
  130. package/lib/module/chatbot/types.js +4 -0
  131. package/lib/module/chatbot/types.js.map +1 -0
  132. package/lib/module/hooks/index.js +7 -0
  133. package/lib/module/hooks/index.js.map +1 -0
  134. package/lib/module/hooks/useAutoScroll.js +35 -0
  135. package/lib/module/hooks/useAutoScroll.js.map +1 -0
  136. package/lib/module/hooks/useClipboard.js +40 -0
  137. package/lib/module/hooks/useClipboard.js.map +1 -0
  138. package/lib/module/hooks/useCollapsible.js +31 -0
  139. package/lib/module/hooks/useCollapsible.js.map +1 -0
  140. package/lib/module/hooks/useStickToBottom.js +64 -0
  141. package/lib/module/hooks/useStickToBottom.js.map +1 -0
  142. package/lib/module/index.js +19 -0
  143. package/lib/module/index.js.map +1 -0
  144. package/lib/module/package.json +1 -0
  145. package/lib/module/primitives/Badge.js +114 -0
  146. package/lib/module/primitives/Badge.js.map +1 -0
  147. package/lib/module/primitives/Button.js +180 -0
  148. package/lib/module/primitives/Button.js.map +1 -0
  149. package/lib/module/primitives/Card.js +156 -0
  150. package/lib/module/primitives/Card.js.map +1 -0
  151. package/lib/module/primitives/Collapsible.js +130 -0
  152. package/lib/module/primitives/Collapsible.js.map +1 -0
  153. package/lib/module/primitives/ScrollArea.js +35 -0
  154. package/lib/module/primitives/ScrollArea.js.map +1 -0
  155. package/lib/module/primitives/index.js +8 -0
  156. package/lib/module/primitives/index.js.map +1 -0
  157. package/lib/module/streaming/StreamingMarkdown.js +246 -0
  158. package/lib/module/streaming/StreamingMarkdown.js.map +1 -0
  159. package/lib/module/streaming/index.js +4 -0
  160. package/lib/module/streaming/index.js.map +1 -0
  161. package/lib/module/streaming/parser.js +477 -0
  162. package/lib/module/streaming/parser.js.map +1 -0
  163. package/lib/module/streaming/renderers/BlockquoteRenderer.js +30 -0
  164. package/lib/module/streaming/renderers/BlockquoteRenderer.js.map +1 -0
  165. package/lib/module/streaming/renderers/CodeRenderer.js +123 -0
  166. package/lib/module/streaming/renderers/CodeRenderer.js.map +1 -0
  167. package/lib/module/streaming/renderers/HeadingRenderer.js +56 -0
  168. package/lib/module/streaming/renderers/HeadingRenderer.js.map +1 -0
  169. package/lib/module/streaming/renderers/ImageRenderer.js +48 -0
  170. package/lib/module/streaming/renderers/ImageRenderer.js.map +1 -0
  171. package/lib/module/streaming/renderers/LinkRenderer.js +44 -0
  172. package/lib/module/streaming/renderers/LinkRenderer.js.map +1 -0
  173. package/lib/module/streaming/renderers/ListRenderer.js +58 -0
  174. package/lib/module/streaming/renderers/ListRenderer.js.map +1 -0
  175. package/lib/module/streaming/renderers/TableRenderer.js +72 -0
  176. package/lib/module/streaming/renderers/TableRenderer.js.map +1 -0
  177. package/lib/module/streaming/renderers/TextRenderer.js +36 -0
  178. package/lib/module/streaming/renderers/TextRenderer.js.map +1 -0
  179. package/lib/module/streaming/renderers/index.js +13 -0
  180. package/lib/module/streaming/renderers/index.js.map +1 -0
  181. package/lib/module/streaming/renderers/renderInlineChildren.js +107 -0
  182. package/lib/module/streaming/renderers/renderInlineChildren.js.map +1 -0
  183. package/lib/module/streaming/renderers/renderNode.js +78 -0
  184. package/lib/module/streaming/renderers/renderNode.js.map +1 -0
  185. package/lib/module/theme/ThemeProvider.js +62 -0
  186. package/lib/module/theme/ThemeProvider.js.map +1 -0
  187. package/lib/module/theme/defaultTheme.js +92 -0
  188. package/lib/module/theme/defaultTheme.js.map +1 -0
  189. package/lib/module/theme/index.js +5 -0
  190. package/lib/module/theme/index.js.map +1 -0
  191. package/lib/module/theme/tokens.js +2 -0
  192. package/lib/module/theme/tokens.js.map +1 -0
  193. package/lib/module/types.d.js +2 -0
  194. package/lib/module/types.d.js.map +1 -0
  195. package/lib/module/voice/index.js +14 -0
  196. package/lib/module/voice/index.js.map +1 -0
  197. package/lib/module/voice/useSpeechRecognition.js +169 -0
  198. package/lib/module/voice/useSpeechRecognition.js.map +1 -0
  199. package/lib/typescript/src/chatbot/AIImage.d.ts +24 -0
  200. package/lib/typescript/src/chatbot/AIImage.d.ts.map +1 -0
  201. package/lib/typescript/src/chatbot/Attachments.d.ts +20 -0
  202. package/lib/typescript/src/chatbot/Attachments.d.ts.map +1 -0
  203. package/lib/typescript/src/chatbot/ChatErrorBoundary.d.ts +57 -0
  204. package/lib/typescript/src/chatbot/ChatErrorBoundary.d.ts.map +1 -0
  205. package/lib/typescript/src/chatbot/ChatMessageItem.d.ts +45 -0
  206. package/lib/typescript/src/chatbot/ChatMessageItem.d.ts.map +1 -0
  207. package/lib/typescript/src/chatbot/Conversation.d.ts +94 -0
  208. package/lib/typescript/src/chatbot/Conversation.d.ts.map +1 -0
  209. package/lib/typescript/src/chatbot/ConversationScrollButton.d.ts +62 -0
  210. package/lib/typescript/src/chatbot/ConversationScrollButton.d.ts.map +1 -0
  211. package/lib/typescript/src/chatbot/Message.d.ts +39 -0
  212. package/lib/typescript/src/chatbot/Message.d.ts.map +1 -0
  213. package/lib/typescript/src/chatbot/PromptInput.d.ts +93 -0
  214. package/lib/typescript/src/chatbot/PromptInput.d.ts.map +1 -0
  215. package/lib/typescript/src/chatbot/Reasoning.d.ts +14 -0
  216. package/lib/typescript/src/chatbot/Reasoning.d.ts.map +1 -0
  217. package/lib/typescript/src/chatbot/Shimmer.d.ts +13 -0
  218. package/lib/typescript/src/chatbot/Shimmer.d.ts.map +1 -0
  219. package/lib/typescript/src/chatbot/Sources.d.ts +17 -0
  220. package/lib/typescript/src/chatbot/Sources.d.ts.map +1 -0
  221. package/lib/typescript/src/chatbot/Suggestion.d.ts +15 -0
  222. package/lib/typescript/src/chatbot/Suggestion.d.ts.map +1 -0
  223. package/lib/typescript/src/chatbot/Tool.d.ts +30 -0
  224. package/lib/typescript/src/chatbot/Tool.d.ts.map +1 -0
  225. package/lib/typescript/src/chatbot/adapters/uiMessageAdapter.d.ts +24 -0
  226. package/lib/typescript/src/chatbot/adapters/uiMessageAdapter.d.ts.map +1 -0
  227. package/lib/typescript/src/chatbot/index.d.ts +29 -0
  228. package/lib/typescript/src/chatbot/index.d.ts.map +1 -0
  229. package/lib/typescript/src/chatbot/types.d.ts +49 -0
  230. package/lib/typescript/src/chatbot/types.d.ts.map +1 -0
  231. package/lib/typescript/src/hooks/index.d.ts +9 -0
  232. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  233. package/lib/typescript/src/hooks/useAutoScroll.d.ts +23 -0
  234. package/lib/typescript/src/hooks/useAutoScroll.d.ts.map +1 -0
  235. package/lib/typescript/src/hooks/useClipboard.d.ts +22 -0
  236. package/lib/typescript/src/hooks/useClipboard.d.ts.map +1 -0
  237. package/lib/typescript/src/hooks/useCollapsible.d.ts +28 -0
  238. package/lib/typescript/src/hooks/useCollapsible.d.ts.map +1 -0
  239. package/lib/typescript/src/hooks/useStickToBottom.d.ts +39 -0
  240. package/lib/typescript/src/hooks/useStickToBottom.d.ts.map +1 -0
  241. package/lib/typescript/src/index.d.ts +11 -0
  242. package/lib/typescript/src/index.d.ts.map +1 -0
  243. package/lib/typescript/src/primitives/Badge.d.ts +10 -0
  244. package/lib/typescript/src/primitives/Badge.d.ts.map +1 -0
  245. package/lib/typescript/src/primitives/Button.d.ts +16 -0
  246. package/lib/typescript/src/primitives/Button.d.ts.map +1 -0
  247. package/lib/typescript/src/primitives/Card.d.ts +33 -0
  248. package/lib/typescript/src/primitives/Card.d.ts.map +1 -0
  249. package/lib/typescript/src/primitives/Collapsible.d.ts +20 -0
  250. package/lib/typescript/src/primitives/Collapsible.d.ts.map +1 -0
  251. package/lib/typescript/src/primitives/ScrollArea.d.ts +10 -0
  252. package/lib/typescript/src/primitives/ScrollArea.d.ts.map +1 -0
  253. package/lib/typescript/src/primitives/index.d.ts +11 -0
  254. package/lib/typescript/src/primitives/index.d.ts.map +1 -0
  255. package/lib/typescript/src/streaming/StreamingMarkdown.d.ts +47 -0
  256. package/lib/typescript/src/streaming/StreamingMarkdown.d.ts.map +1 -0
  257. package/lib/typescript/src/streaming/index.d.ts +3 -0
  258. package/lib/typescript/src/streaming/index.d.ts.map +1 -0
  259. package/lib/typescript/src/streaming/parser.d.ts +41 -0
  260. package/lib/typescript/src/streaming/parser.d.ts.map +1 -0
  261. package/lib/typescript/src/streaming/renderers/BlockquoteRenderer.d.ts +7 -0
  262. package/lib/typescript/src/streaming/renderers/BlockquoteRenderer.d.ts.map +1 -0
  263. package/lib/typescript/src/streaming/renderers/CodeRenderer.d.ts +7 -0
  264. package/lib/typescript/src/streaming/renderers/CodeRenderer.d.ts.map +1 -0
  265. package/lib/typescript/src/streaming/renderers/HeadingRenderer.d.ts +7 -0
  266. package/lib/typescript/src/streaming/renderers/HeadingRenderer.d.ts.map +1 -0
  267. package/lib/typescript/src/streaming/renderers/ImageRenderer.d.ts +7 -0
  268. package/lib/typescript/src/streaming/renderers/ImageRenderer.d.ts.map +1 -0
  269. package/lib/typescript/src/streaming/renderers/LinkRenderer.d.ts +7 -0
  270. package/lib/typescript/src/streaming/renderers/LinkRenderer.d.ts.map +1 -0
  271. package/lib/typescript/src/streaming/renderers/ListRenderer.d.ts +7 -0
  272. package/lib/typescript/src/streaming/renderers/ListRenderer.d.ts.map +1 -0
  273. package/lib/typescript/src/streaming/renderers/TableRenderer.d.ts +7 -0
  274. package/lib/typescript/src/streaming/renderers/TableRenderer.d.ts.map +1 -0
  275. package/lib/typescript/src/streaming/renderers/TextRenderer.d.ts +7 -0
  276. package/lib/typescript/src/streaming/renderers/TextRenderer.d.ts.map +1 -0
  277. package/lib/typescript/src/streaming/renderers/index.d.ts +19 -0
  278. package/lib/typescript/src/streaming/renderers/index.d.ts.map +1 -0
  279. package/lib/typescript/src/streaming/renderers/renderInlineChildren.d.ts +12 -0
  280. package/lib/typescript/src/streaming/renderers/renderInlineChildren.d.ts.map +1 -0
  281. package/lib/typescript/src/streaming/renderers/renderNode.d.ts +8 -0
  282. package/lib/typescript/src/streaming/renderers/renderNode.d.ts.map +1 -0
  283. package/lib/typescript/src/theme/ThemeProvider.d.ts +14 -0
  284. package/lib/typescript/src/theme/ThemeProvider.d.ts.map +1 -0
  285. package/lib/typescript/src/theme/defaultTheme.d.ts +4 -0
  286. package/lib/typescript/src/theme/defaultTheme.d.ts.map +1 -0
  287. package/lib/typescript/src/theme/index.d.ts +5 -0
  288. package/lib/typescript/src/theme/index.d.ts.map +1 -0
  289. package/lib/typescript/src/theme/tokens.d.ts +66 -0
  290. package/lib/typescript/src/theme/tokens.d.ts.map +1 -0
  291. package/lib/typescript/src/voice/index.d.ts +3 -0
  292. package/lib/typescript/src/voice/index.d.ts.map +1 -0
  293. package/lib/typescript/src/voice/useSpeechRecognition.d.ts +77 -0
  294. package/lib/typescript/src/voice/useSpeechRecognition.d.ts.map +1 -0
  295. package/package.json +132 -0
  296. package/src/chatbot/AIImage.tsx +166 -0
  297. package/src/chatbot/Attachments.tsx +382 -0
  298. package/src/chatbot/ChatErrorBoundary.tsx +230 -0
  299. package/src/chatbot/ChatMessageItem.tsx +195 -0
  300. package/src/chatbot/Conversation.tsx +537 -0
  301. package/src/chatbot/ConversationScrollButton.tsx +149 -0
  302. package/src/chatbot/Message.tsx +266 -0
  303. package/src/chatbot/PromptInput.tsx +532 -0
  304. package/src/chatbot/Reasoning.tsx +198 -0
  305. package/src/chatbot/Shimmer.tsx +146 -0
  306. package/src/chatbot/Sources.tsx +263 -0
  307. package/src/chatbot/Suggestion.tsx +123 -0
  308. package/src/chatbot/Tool.tsx +340 -0
  309. package/src/chatbot/adapters/uiMessageAdapter.ts +177 -0
  310. package/src/chatbot/index.ts +97 -0
  311. package/src/chatbot/types.ts +66 -0
  312. package/src/hooks/index.ts +17 -0
  313. package/src/hooks/useAutoScroll.ts +43 -0
  314. package/src/hooks/useClipboard.ts +46 -0
  315. package/src/hooks/useCollapsible.ts +42 -0
  316. package/src/hooks/useStickToBottom.ts +82 -0
  317. package/src/index.ts +139 -0
  318. package/src/primitives/Badge.tsx +119 -0
  319. package/src/primitives/Button.tsx +213 -0
  320. package/src/primitives/Card.tsx +221 -0
  321. package/src/primitives/Collapsible.tsx +168 -0
  322. package/src/primitives/ScrollArea.tsx +53 -0
  323. package/src/primitives/index.ts +36 -0
  324. package/src/streaming/StreamingMarkdown.tsx +282 -0
  325. package/src/streaming/index.ts +2 -0
  326. package/src/streaming/parser.ts +506 -0
  327. package/src/streaming/renderers/BlockquoteRenderer.tsx +42 -0
  328. package/src/streaming/renderers/CodeRenderer.tsx +158 -0
  329. package/src/streaming/renderers/HeadingRenderer.tsx +64 -0
  330. package/src/streaming/renderers/ImageRenderer.tsx +62 -0
  331. package/src/streaming/renderers/LinkRenderer.tsx +53 -0
  332. package/src/streaming/renderers/ListRenderer.tsx +65 -0
  333. package/src/streaming/renderers/TableRenderer.tsx +103 -0
  334. package/src/streaming/renderers/TextRenderer.tsx +39 -0
  335. package/src/streaming/renderers/index.ts +26 -0
  336. package/src/streaming/renderers/renderInlineChildren.tsx +115 -0
  337. package/src/streaming/renderers/renderNode.tsx +72 -0
  338. package/src/theme/ThemeProvider.tsx +77 -0
  339. package/src/theme/defaultTheme.ts +93 -0
  340. package/src/theme/index.ts +4 -0
  341. package/src/theme/tokens.ts +69 -0
  342. package/src/types.d.ts +71 -0
  343. package/src/voice/index.ts +15 -0
  344. package/src/voice/useSpeechRecognition.ts +230 -0
@@ -0,0 +1,537 @@
1
+ import React, {
2
+ memo,
3
+ useCallback,
4
+ useEffect,
5
+ useImperativeHandle,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ type ReactElement,
10
+ type ReactNode,
11
+ type Ref,
12
+ } from 'react';
13
+ import {
14
+ FlatList,
15
+ StyleSheet,
16
+ View,
17
+ type LayoutChangeEvent,
18
+ type ListRenderItemInfo,
19
+ type NativeScrollEvent,
20
+ type NativeSyntheticEvent,
21
+ type ViewProps,
22
+ } from 'react-native';
23
+ import { useAIElementsTheme } from '../theme';
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Constants
27
+ // ---------------------------------------------------------------------------
28
+
29
+ /**
30
+ * Distance in px from the bottom of the content that still counts as "at the
31
+ * bottom" for the purposes of auto-following content growth (e.g. expanding
32
+ * a Sources collapsible).
33
+ */
34
+ const STICK_TO_BOTTOM_THRESHOLD = 80;
35
+
36
+ /** Delay before retrying a failed `scrollToIndex` once layout settles. */
37
+ const SCROLL_RETRY_DELAY_MS = 120;
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Types
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Minimum shape Conversation requires from a message. Most chat libraries
45
+ * (the Vercel `ai` SDK, OpenAI's `ChatMessage`, Anthropic's `Message`) all
46
+ * satisfy this contract via `id` + `role`.
47
+ */
48
+ export interface ConversationMessage {
49
+ id: string;
50
+ /** Optional — used by the default `isUserMessage` predicate. */
51
+ role?: string;
52
+ }
53
+
54
+ /** Order in which `messages` is provided. */
55
+ export type ConversationMessageOrder = 'oldest-first' | 'newest-first';
56
+
57
+ /**
58
+ * How `Conversation` reacts to new messages and content growth.
59
+ *
60
+ * - `'anchor-user-message'` *(default)* — When a new user message arrives,
61
+ * scroll it to the top of the viewport and use dynamic bottom padding so
62
+ * the assistant response streams downward beneath it. Auto-scroll on
63
+ * collapsible expansion (e.g. opening a Sources block) when the user is
64
+ * already near the bottom and not currently streaming. This mimics how
65
+ * ChatGPT, Claude, and Vercel ai-elements anchor each new turn.
66
+ * - `'stick-to-bottom'` — Classic chat behavior: always follow the bottom
67
+ * on any content size change, even mid-stream. The list "sticks" to the
68
+ * newest content. Matches what `use-stick-to-bottom` does on the web.
69
+ * - `'none'` — Disable all automatic scrolling and dynamic padding. The
70
+ * consumer is fully responsible for managing scroll position via the
71
+ * imperative `ConversationRef`.
72
+ */
73
+ export type ConversationScrollBehavior =
74
+ | 'anchor-user-message'
75
+ | 'stick-to-bottom'
76
+ | 'none';
77
+
78
+ /** Imperative handle exposed via ref. */
79
+ export interface ConversationRef {
80
+ /** Scroll to a given display index, optionally animated. */
81
+ scrollToIndex: (
82
+ index: number,
83
+ opts?: { animated?: boolean; viewPosition?: number },
84
+ ) => void;
85
+ /** Scroll to the top of the list (oldest message). */
86
+ scrollToTop: (opts?: { animated?: boolean }) => void;
87
+ /** Scroll to the bottom of the list (newest message). */
88
+ scrollToBottom: (opts?: { animated?: boolean }) => void;
89
+ /** Whether the user is currently scrolled near the bottom of content. */
90
+ isAtBottom: () => boolean;
91
+ }
92
+
93
+ export interface ConversationProps<T extends ConversationMessage>
94
+ extends Omit<ViewProps, 'children'> {
95
+ /** Imperative handle. Use `useRef<ConversationRef>(null)` and pass it here. */
96
+ ref?: Ref<ConversationRef>;
97
+ /** The conversation messages. Order is controlled by the `order` prop. */
98
+ messages: T[];
99
+ /** Render a single message. Called for each item in display order. */
100
+ renderMessage: (item: T) => ReactNode;
101
+ /** Rendered when `messages` is empty. Filled to the chat area (`flex: 1`). */
102
+ emptyState?: ReactNode;
103
+ /**
104
+ * Whether the assistant is currently generating. While true, content
105
+ * growth from streaming is **not** auto-scrolled — the user-message
106
+ * anchor takes precedence. Once it flips back to false, content growth
107
+ * (e.g. expanding a Sources / Reasoning collapsible) auto-scrolls if
108
+ * the user was already near the bottom.
109
+ */
110
+ isStreaming?: boolean;
111
+ /**
112
+ * Order of items in the `messages` array. Defaults to `'oldest-first'`,
113
+ * matching what `@ai-sdk/react`'s `useChat()` and most LLM SDKs return.
114
+ * Use `'newest-first'` if your state is built with `[newMsg, ...prev]`.
115
+ */
116
+ order?: ConversationMessageOrder;
117
+ /**
118
+ * Predicate for identifying user messages — used to anchor the most
119
+ * recent user message to the top of the viewport when it appears.
120
+ * Defaults to `(m) => m.role === 'user'`.
121
+ */
122
+ isUserMessage?: (item: T) => boolean;
123
+ /**
124
+ * Push-based notification when the "near the bottom" state changes.
125
+ * Fires once on each transition (true → false or false → true), driven
126
+ * by either user scroll events or content-size growth from streaming.
127
+ * Use this to drive a floating scroll-to-bottom button — see
128
+ * `ConversationScrollButton`.
129
+ */
130
+ onIsAtBottomChange?: (isAtBottom: boolean) => void;
131
+ /**
132
+ * How to react to new messages and content growth. Defaults to
133
+ * `'anchor-user-message'`. See {@link ConversationScrollBehavior}.
134
+ */
135
+ scrollBehavior?: ConversationScrollBehavior;
136
+ }
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Helpers
140
+ // ---------------------------------------------------------------------------
141
+
142
+ const defaultIsUserMessage = (m: ConversationMessage): boolean =>
143
+ m.role === 'user';
144
+
145
+ /**
146
+ * Find the most-recently-added user message regardless of array order.
147
+ * - For `'newest-first'`: the newest user msg is the *first* match in the
148
+ * forward direction.
149
+ * - For `'oldest-first'`: the newest user msg is the *last* match, so we
150
+ * walk backward.
151
+ */
152
+ function findNewestUserMessage<T extends ConversationMessage>(
153
+ messages: T[],
154
+ isUserMessage: (m: T) => boolean,
155
+ order: ConversationMessageOrder,
156
+ ): T | undefined {
157
+ if (order === 'newest-first') {
158
+ return messages.find(isUserMessage);
159
+ }
160
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
161
+ if (isUserMessage(messages[i])) return messages[i];
162
+ }
163
+ return undefined;
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // Component
168
+ // ---------------------------------------------------------------------------
169
+
170
+ /**
171
+ * Chat list, top-to-bottom, with three signature behaviors:
172
+ *
173
+ * 1. **Anchor on send.** When a new user message arrives, the list scrolls
174
+ * that message to the top of the viewport. The assistant response then
175
+ * streams downward beneath it.
176
+ * 2. **Dynamic bottom padding.** When the conversation is shorter than the
177
+ * viewport, padding fills the gap so the user message can still be
178
+ * anchored to the top. Once natural content fills the viewport, the
179
+ * padding collapses to zero so the user can't scroll into empty space.
180
+ * 3. **Stick-to-bottom on growth.** When the user is already near the
181
+ * bottom and content grows (e.g. they tap "Sources" to expand), the
182
+ * list animates to show the new content. Disabled while
183
+ * `isStreaming === true` to avoid fighting the anchor.
184
+ *
185
+ * @example
186
+ * ```tsx
187
+ * const ref = useRef<ConversationRef>(null);
188
+ *
189
+ * <Conversation
190
+ * ref={ref}
191
+ * messages={messages} // any T extends { id, role? }
192
+ * renderMessage={(m) => <Bubble msg={m} />}
193
+ * isStreaming={isLoading}
194
+ * order="oldest-first" // or "newest-first"
195
+ * emptyState={<EmptyChat />}
196
+ * />
197
+ * ```
198
+ */
199
+ function ConversationInner<T extends ConversationMessage>({
200
+ ref,
201
+ messages,
202
+ renderMessage,
203
+ emptyState,
204
+ isStreaming = false,
205
+ order = 'oldest-first',
206
+ isUserMessage = defaultIsUserMessage as (item: T) => boolean,
207
+ onIsAtBottomChange,
208
+ scrollBehavior = 'anchor-user-message',
209
+ style,
210
+ ...viewProps
211
+ }: ConversationProps<T>) {
212
+ const theme = useAIElementsTheme();
213
+ const listRef = useRef<FlatList<T>>(null);
214
+
215
+ // --- Refs (frequently updated, no re-render) ----------------------------
216
+
217
+ /** Latest scroll position (true when within threshold of the bottom). */
218
+ const isAtBottomRef = useRef(true);
219
+ /** Latest "natural" content height (i.e. without dynamic padding). */
220
+ const previousNaturalRef = useRef(0);
221
+ /** Newest natural content height — readable from the imperative API. */
222
+ const naturalContentHeightRef = useRef(0);
223
+ /** Mirror of `isStreaming` so callbacks can read the freshest value. */
224
+ const isStreamingRef = useRef(isStreaming);
225
+ isStreamingRef.current = isStreaming;
226
+ /** Mirror of `scrollBehavior` so callbacks read the freshest value. */
227
+ const scrollBehaviorRef = useRef(scrollBehavior);
228
+ scrollBehaviorRef.current = scrollBehavior;
229
+ /** Last user-message id we anchored to — guards against re-anchoring. */
230
+ const lastUserMsgIdRef = useRef<string | null>(null);
231
+ /**
232
+ * The natural content height as it stood **before** the most recent user
233
+ * message was added. Used as the baseline for the dynamic-padding
234
+ * formula so the new user message can always be scrolled to viewport
235
+ * top, regardless of how much history sits above it. `null` until the
236
+ * first user message has been anchored.
237
+ */
238
+ const naturalAtAnchorRef = useRef<number | null>(null);
239
+
240
+ // --- onIsAtBottomChange plumbing ----------------------------------------
241
+ /** Mirror the callback so call sites don't bust useCallback memos. */
242
+ const onIsAtBottomChangeRef = useRef(onIsAtBottomChange);
243
+ onIsAtBottomChangeRef.current = onIsAtBottomChange;
244
+ /** Last value emitted to the consumer — used to dedupe transitions. */
245
+ const lastEmittedIsAtBottomRef = useRef(true);
246
+ /** Latest scroll metrics — needed so contentSize changes can recompute. */
247
+ const scrollMetricsRef = useRef({ contentOffset: 0, layoutHeight: 0 });
248
+
249
+ /**
250
+ * Compute isAtBottom from the latest scroll metrics + a known content
251
+ * height. Updates `isAtBottomRef` and fires the change callback if the
252
+ * value transitioned.
253
+ *
254
+ * Skips when `layoutHeight === 0` — that's the "no scroll event has
255
+ * fired yet" sentinel. Without this guard, the first
256
+ * `handleListContentSizeChange` after mount would compute
257
+ * `distanceFromBottom = contentHeight - 0 - 0 = contentHeight`, which
258
+ * is always huge, and emit a false `isAtBottom: false` before the user
259
+ * has even scrolled.
260
+ */
261
+ const updateIsAtBottom = useCallback((contentHeight: number) => {
262
+ const { contentOffset, layoutHeight } = scrollMetricsRef.current;
263
+ if (layoutHeight === 0) return;
264
+ const distanceFromBottom = contentHeight - (contentOffset + layoutHeight);
265
+ const isAtBottom = distanceFromBottom < STICK_TO_BOTTOM_THRESHOLD;
266
+ isAtBottomRef.current = isAtBottom;
267
+ if (isAtBottom !== lastEmittedIsAtBottomRef.current) {
268
+ lastEmittedIsAtBottomRef.current = isAtBottom;
269
+ onIsAtBottomChangeRef.current?.(isAtBottom);
270
+ }
271
+ }, []);
272
+
273
+ // --- Display order ------------------------------------------------------
274
+ // We always render top-to-bottom = oldest-first. If the caller passes
275
+ // newest-first, reverse internally.
276
+ const orderedMessages = useMemo(
277
+ () => (order === 'newest-first' ? [...messages].reverse() : messages),
278
+ [messages, order],
279
+ );
280
+
281
+ // --- Dynamic bottom padding ---------------------------------------------
282
+ const [chatAreaHeight, setChatAreaHeight] = useState(0);
283
+ const [listPaddingBottom, setListPaddingBottom] = useState(0);
284
+
285
+ const handleAreaLayout = useCallback((e: LayoutChangeEvent) => {
286
+ setChatAreaHeight(e.nativeEvent.layout.height);
287
+ }, []);
288
+
289
+ const handleListContentSizeChange = useCallback(
290
+ (_w: number, h: number) => {
291
+ // `h` is the *total* content height, which already includes whatever
292
+ // bottom padding we previously set. Subtract it back out to get the
293
+ // natural (real) content height.
294
+ const natural = Math.max(0, h - listPaddingBottom);
295
+ const grew = natural > previousNaturalRef.current;
296
+ previousNaturalRef.current = natural;
297
+ naturalContentHeightRef.current = natural;
298
+
299
+ const behavior = scrollBehaviorRef.current;
300
+
301
+ // --- Dynamic bottom padding (anchor-user-message mode only) ---------
302
+ // Reserve enough room so the most recent user message can be
303
+ // scrolled to the top of the viewport. We measure this as "content
304
+ // added since the user message arrived" and pad the rest of the
305
+ // viewport. As the assistant streams below the user message,
306
+ // contentBelowAnchor grows and the padding shrinks proportionally —
307
+ // total content height stays constant until the assistant has
308
+ // produced enough text to fill the viewport on its own, at which
309
+ // point padding naturally hits zero.
310
+ //
311
+ // The other modes don't need padding manipulation, so they fall
312
+ // back to zero.
313
+ let desired = 0;
314
+ if (behavior === 'anchor-user-message') {
315
+ const contentBelowAnchor =
316
+ naturalAtAnchorRef.current !== null
317
+ ? Math.max(0, natural - naturalAtAnchorRef.current)
318
+ : natural;
319
+ desired = Math.max(0, chatAreaHeight - contentBelowAnchor);
320
+ }
321
+ if (desired !== listPaddingBottom) {
322
+ setListPaddingBottom(desired);
323
+ }
324
+
325
+ // Re-evaluate isAtBottom against the new content height. The user
326
+ // may have been "at the bottom" before this growth and now no longer
327
+ // be — common during streaming when the assistant pushes content
328
+ // past the viewport while the user passively watches.
329
+ updateIsAtBottom(h);
330
+
331
+ // --- Auto-scroll on growth ------------------------------------------
332
+ // Behavior depends on the mode:
333
+ //
334
+ // anchor-user-message: only follow the bottom when the user is
335
+ // already near it AND not streaming. Streaming uses the
336
+ // user-message anchor instead, and auto-scrolling would fight
337
+ // it. Post-streaming this catches collapsible expansions.
338
+ // stick-to-bottom: always follow the bottom on growth, even
339
+ // mid-stream — classic chat behavior.
340
+ // none: never auto-scroll. Consumer drives the list via the ref.
341
+ if (!grew || behavior === 'none') return;
342
+
343
+ const shouldFollow =
344
+ behavior === 'stick-to-bottom'
345
+ ? true
346
+ : !isStreamingRef.current && isAtBottomRef.current;
347
+
348
+ if (shouldFollow) {
349
+ requestAnimationFrame(() => {
350
+ listRef.current?.scrollToEnd({ animated: true });
351
+ });
352
+ }
353
+ },
354
+ [listPaddingBottom, chatAreaHeight, updateIsAtBottom],
355
+ );
356
+
357
+ const handleScroll = useCallback(
358
+ (e: NativeSyntheticEvent<NativeScrollEvent>) => {
359
+ const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
360
+ scrollMetricsRef.current = {
361
+ contentOffset: contentOffset.y,
362
+ layoutHeight: layoutMeasurement.height,
363
+ };
364
+ updateIsAtBottom(contentSize.height);
365
+ },
366
+ [updateIsAtBottom],
367
+ );
368
+
369
+ // Recompute the padding if the viewport itself changes (rotation, keyboard).
370
+ // Mirrors the formula in handleListContentSizeChange — same anchor-aware
371
+ // contentBelowAnchor calculation so the user message stays anchored
372
+ // through orientation changes. Skipped entirely outside the
373
+ // anchor-user-message mode since other modes don't manage padding.
374
+ useEffect(() => {
375
+ if (chatAreaHeight === 0) return;
376
+ if (scrollBehavior !== 'anchor-user-message') {
377
+ if (listPaddingBottom !== 0) setListPaddingBottom(0);
378
+ return;
379
+ }
380
+ const contentBelowAnchor =
381
+ naturalAtAnchorRef.current !== null
382
+ ? Math.max(
383
+ 0,
384
+ naturalContentHeightRef.current - naturalAtAnchorRef.current,
385
+ )
386
+ : naturalContentHeightRef.current;
387
+ const desired = Math.max(0, chatAreaHeight - contentBelowAnchor);
388
+ setListPaddingBottom(desired);
389
+ // listPaddingBottom intentionally omitted to avoid feedback loops —
390
+ // the same set of conditions runs again whenever it actually matters.
391
+ // eslint-disable-next-line react-hooks/exhaustive-deps
392
+ }, [chatAreaHeight, scrollBehavior]);
393
+
394
+ // --- Auto-anchor new user message to the top of the viewport -----------
395
+ // Only the `'anchor-user-message'` mode performs this scroll. The other
396
+ // modes leave scroll position to the consumer (or to stick-to-bottom).
397
+ useEffect(() => {
398
+ if (scrollBehavior !== 'anchor-user-message') return;
399
+
400
+ const newestUserMsg = findNewestUserMessage(messages, isUserMessage, order);
401
+ if (!newestUserMsg || newestUserMsg.id === lastUserMsgIdRef.current) {
402
+ return;
403
+ }
404
+ lastUserMsgIdRef.current = newestUserMsg.id;
405
+
406
+ // Capture the natural content height as it stood **before** this new
407
+ // user message arrived. handleListContentSizeChange may or may not
408
+ // have already processed the new content, but `previousNaturalRef`
409
+ // always holds the value from the most recent step *before* the
410
+ // latest update — which for a freshly-arrived message is the height
411
+ // without it. This becomes the baseline for the dynamic-padding
412
+ // formula so the new message can be scrolled all the way to the top
413
+ // even when there's history above it.
414
+ naturalAtAnchorRef.current = previousNaturalRef.current;
415
+
416
+ const displayIndex = orderedMessages.findIndex(
417
+ (m) => m.id === newestUserMsg.id,
418
+ );
419
+ if (displayIndex < 0) return;
420
+
421
+ // Wait for the new item to be laid out before scrolling.
422
+ requestAnimationFrame(() => {
423
+ listRef.current?.scrollToIndex({
424
+ index: displayIndex,
425
+ viewPosition: 0,
426
+ animated: true,
427
+ });
428
+ });
429
+ }, [messages, orderedMessages, order, isUserMessage, scrollBehavior]);
430
+
431
+ // --- Imperative handle --------------------------------------------------
432
+ useImperativeHandle(
433
+ ref,
434
+ () => ({
435
+ scrollToIndex: (index, opts) => {
436
+ listRef.current?.scrollToIndex({
437
+ index,
438
+ viewPosition: opts?.viewPosition ?? 0,
439
+ animated: opts?.animated ?? true,
440
+ });
441
+ },
442
+ scrollToTop: (opts) => {
443
+ listRef.current?.scrollToOffset({
444
+ offset: 0,
445
+ animated: opts?.animated ?? true,
446
+ });
447
+ },
448
+ scrollToBottom: (opts) => {
449
+ listRef.current?.scrollToEnd({ animated: opts?.animated ?? true });
450
+ },
451
+ isAtBottom: () => isAtBottomRef.current,
452
+ }),
453
+ [],
454
+ );
455
+
456
+ // --- Row renderer -------------------------------------------------------
457
+ const renderItem = useCallback(
458
+ ({ item }: ListRenderItemInfo<T>) => <>{renderMessage(item)}</>,
459
+ [renderMessage],
460
+ );
461
+
462
+ const keyExtractor = useCallback((item: T) => item.id, []);
463
+
464
+ const handleScrollToIndexFailed = useCallback(
465
+ (info: {
466
+ index: number;
467
+ highestMeasuredFrameIndex: number;
468
+ averageItemLength: number;
469
+ }) => {
470
+ // Fallback for items not yet measured: approximate offset, then
471
+ // retry once layout settles.
472
+ const offset = info.averageItemLength * info.index;
473
+ listRef.current?.scrollToOffset({ offset, animated: true });
474
+ setTimeout(() => {
475
+ listRef.current?.scrollToIndex({
476
+ index: info.index,
477
+ viewPosition: 0,
478
+ animated: true,
479
+ });
480
+ }, SCROLL_RETRY_DELAY_MS);
481
+ },
482
+ [],
483
+ );
484
+
485
+ return (
486
+ <View
487
+ {...viewProps}
488
+ style={[styles.root, style]}
489
+ onLayout={handleAreaLayout}
490
+ >
491
+ {messages.length === 0 ? (
492
+ <View style={styles.emptyFill}>{emptyState}</View>
493
+ ) : (
494
+ <FlatList
495
+ ref={listRef}
496
+ data={orderedMessages}
497
+ renderItem={renderItem}
498
+ keyExtractor={keyExtractor}
499
+ scrollEventThrottle={16}
500
+ showsVerticalScrollIndicator={false}
501
+ keyboardDismissMode="interactive"
502
+ onScroll={handleScroll}
503
+ onContentSizeChange={handleListContentSizeChange}
504
+ contentContainerStyle={{
505
+ paddingTop: theme.spacing.lg,
506
+ paddingBottom: listPaddingBottom,
507
+ }}
508
+ onScrollToIndexFailed={handleScrollToIndexFailed}
509
+ />
510
+ )}
511
+ </View>
512
+ );
513
+ }
514
+
515
+ ConversationInner.displayName = 'Conversation';
516
+
517
+ // `memo` strips the generic from the function signature, so we cast it back
518
+ // to a generic-friendly call signature for the public export. The runtime
519
+ // behavior is unchanged — only the type is preserved.
520
+ export const Conversation = memo(ConversationInner) as <
521
+ T extends ConversationMessage,
522
+ >(
523
+ props: ConversationProps<T>,
524
+ ) => ReactElement;
525
+
526
+ // ---------------------------------------------------------------------------
527
+ // Styles
528
+ // ---------------------------------------------------------------------------
529
+
530
+ const styles = StyleSheet.create({
531
+ root: {
532
+ flex: 1,
533
+ },
534
+ emptyFill: {
535
+ flex: 1,
536
+ },
537
+ });
@@ -0,0 +1,149 @@
1
+ import React from 'react';
2
+ import {
3
+ Platform,
4
+ Pressable,
5
+ StyleSheet,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import { ArrowDown } from 'lucide-react-native';
10
+ import { GlassView } from 'expo-glass-effect';
11
+ import { useAIElementsTheme } from '../theme';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface ConversationScrollButtonProps {
18
+ /**
19
+ * Whether the button is currently visible. When false, the component
20
+ * returns `null` (mount/unmount instead of fade) so the underlying
21
+ * `GlassView` native view is never wrapped in something Reanimated
22
+ * touches — see the comment block below for why that matters.
23
+ */
24
+ visible: boolean;
25
+ /** Press handler — typically calls `conversationRef.current.scrollToBottom()`. */
26
+ onPress: () => void;
27
+ /** Outer style for the floating wrapper (use for absolute positioning). */
28
+ style?: StyleProp<ViewStyle>;
29
+ /** Optional accessibility label override. */
30
+ accessibilityLabel?: string;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Constants
35
+ // ---------------------------------------------------------------------------
36
+
37
+ // Match the PromptInput's CircleButton dimensions exactly so the scroll
38
+ // button visually stacks above the Plus button as a continuation of the
39
+ // same control column.
40
+ const CIRCLE_BUTTON_SIZE = 36;
41
+ const ICON_SIZE = 18;
42
+ const isIOS = Platform.OS === 'ios';
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Component
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * A floating circle button that appears when the user has scrolled away
50
+ * from the bottom of a conversation, and disappears when they return.
51
+ *
52
+ * Designed to sit in the chat area's bottom-left corner so it visually
53
+ * stacks above the PromptInput's Plus button — same size, same glass
54
+ * treatment, same press feedback. Pairs with
55
+ * `<Conversation onIsAtBottomChange={...} />`: the parent screen tracks
56
+ * the boolean and passes it as `visible`.
57
+ *
58
+ * @example
59
+ * ```tsx
60
+ * const [isAtBottom, setIsAtBottom] = useState(true);
61
+ * const ref = useRef<ConversationRef>(null);
62
+ *
63
+ * <View style={{ flex: 1 }}>
64
+ * <Conversation
65
+ * ref={ref}
66
+ * onIsAtBottomChange={setIsAtBottom}
67
+ * ...
68
+ * />
69
+ * <ConversationScrollButton
70
+ * visible={!isAtBottom}
71
+ * onPress={() => ref.current?.scrollToBottom()}
72
+ * style={{ position: 'absolute', left: 12, bottom: 12 }}
73
+ * />
74
+ * </View>
75
+ * ```
76
+ *
77
+ * **Why no fade animation?**
78
+ * Earlier iterations wrapped this in a Reanimated `Animated.View` for a
79
+ * smooth fade-in/out. On the iOS simulator (and likely device) the
80
+ * `expo-glass-effect` `GlassView` would render correctly on first mount
81
+ * but lose its glass material after a Fast Refresh or reload. Reanimated
82
+ * drives styles on the UI thread via a code path that conflicts with
83
+ * `expo-glass-effect`'s custom native view manager — the prop diff that
84
+ * re-applies the glass layer never reaches the native view. Dropping the
85
+ * animation wrapper and using a plain conditional render keeps the
86
+ * GlassView in exactly the same context as the working `Plus` button
87
+ * inside `PromptInput.CircleButton`.
88
+ */
89
+ export function ConversationScrollButton({
90
+ visible,
91
+ onPress,
92
+ style,
93
+ accessibilityLabel = 'Scroll to latest message',
94
+ }: ConversationScrollButtonProps) {
95
+ const theme = useAIElementsTheme();
96
+
97
+ if (!visible) return null;
98
+
99
+ return (
100
+ <GlassView
101
+ glassEffectStyle={isIOS ? 'regular' : 'none'}
102
+ isInteractive
103
+ colorScheme={theme.dark ? 'dark' : 'light'}
104
+ style={[
105
+ styles.circleButton,
106
+ // On non-iOS GlassView is just a plain View — paint the bg
107
+ // ourselves to match the PromptInput's Plus fallback.
108
+ !isIOS && { backgroundColor: theme.colors.secondary },
109
+ style,
110
+ ]}
111
+ >
112
+ <Pressable
113
+ onPress={onPress}
114
+ accessibilityRole="button"
115
+ accessibilityLabel={accessibilityLabel}
116
+ style={({ pressed }) => [
117
+ styles.circleButtonInner,
118
+ { opacity: pressed ? 0.6 : 1 },
119
+ ]}
120
+ >
121
+ <ArrowDown size={ICON_SIZE} color={theme.colors.foreground} />
122
+ </Pressable>
123
+ </GlassView>
124
+ );
125
+ }
126
+
127
+ ConversationScrollButton.displayName = 'ConversationScrollButton';
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Styles — mirror the PromptInput CircleButton's dimensions exactly
131
+ // ---------------------------------------------------------------------------
132
+
133
+ const styles = StyleSheet.create({
134
+ circleButton: {
135
+ width: CIRCLE_BUTTON_SIZE,
136
+ height: CIRCLE_BUTTON_SIZE,
137
+ borderRadius: CIRCLE_BUTTON_SIZE / 2,
138
+ // Clip the GlassView blur (and the non-iOS bg) to the rounded shape.
139
+ overflow: 'hidden',
140
+ alignItems: 'center',
141
+ justifyContent: 'center',
142
+ },
143
+ circleButtonInner: {
144
+ width: '100%',
145
+ height: '100%',
146
+ alignItems: 'center',
147
+ justifyContent: 'center',
148
+ },
149
+ });