vibefast-cli 0.1.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 (250) hide show
  1. package/FINAL-STATUS.md +144 -0
  2. package/HOW-IT-WORKS.md +559 -0
  3. package/PLAN.md +453 -0
  4. package/README.md +129 -0
  5. package/RECIPES-READY.md +172 -0
  6. package/STATUS.md +199 -0
  7. package/SUCCESS.md +259 -0
  8. package/TESTING-CHECKLIST.md +450 -0
  9. package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
  10. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
  11. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
  12. package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
  13. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
  14. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
  15. package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
  16. package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
  17. package/cloudflare-worker/MIGRATION.md +160 -0
  18. package/cloudflare-worker/QUICKSTART.md +200 -0
  19. package/cloudflare-worker/README.md +242 -0
  20. package/cloudflare-worker/generate-token.js +32 -0
  21. package/cloudflare-worker/mini-native@latest.zip +0 -0
  22. package/cloudflare-worker/setup.sh +143 -0
  23. package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
  24. package/cloudflare-worker/test-recipe/recipe.json +16 -0
  25. package/cloudflare-worker/worker.js +308 -0
  26. package/cloudflare-worker/wrangler.toml +13 -0
  27. package/dist/commands/add.d.ts +3 -0
  28. package/dist/commands/add.d.ts.map +1 -0
  29. package/dist/commands/add.js +149 -0
  30. package/dist/commands/add.js.map +1 -0
  31. package/dist/commands/devices.d.ts +3 -0
  32. package/dist/commands/devices.d.ts.map +1 -0
  33. package/dist/commands/devices.js +35 -0
  34. package/dist/commands/devices.js.map +1 -0
  35. package/dist/commands/doctor.d.ts +3 -0
  36. package/dist/commands/doctor.d.ts.map +1 -0
  37. package/dist/commands/doctor.js +67 -0
  38. package/dist/commands/doctor.js.map +1 -0
  39. package/dist/commands/list.d.ts +3 -0
  40. package/dist/commands/list.d.ts.map +1 -0
  41. package/dist/commands/list.js +40 -0
  42. package/dist/commands/list.js.map +1 -0
  43. package/dist/commands/login.d.ts +3 -0
  44. package/dist/commands/login.d.ts.map +1 -0
  45. package/dist/commands/login.js +23 -0
  46. package/dist/commands/login.js.map +1 -0
  47. package/dist/commands/logout.d.ts +3 -0
  48. package/dist/commands/logout.d.ts.map +1 -0
  49. package/dist/commands/logout.js +16 -0
  50. package/dist/commands/logout.js.map +1 -0
  51. package/dist/commands/remove.d.ts +3 -0
  52. package/dist/commands/remove.d.ts.map +1 -0
  53. package/dist/commands/remove.js +67 -0
  54. package/dist/commands/remove.js.map +1 -0
  55. package/dist/core/__tests__/journal.test.d.ts +2 -0
  56. package/dist/core/__tests__/journal.test.d.ts.map +1 -0
  57. package/dist/core/__tests__/journal.test.js +101 -0
  58. package/dist/core/__tests__/journal.test.js.map +1 -0
  59. package/dist/core/__tests__/validate.test.d.ts +2 -0
  60. package/dist/core/__tests__/validate.test.d.ts.map +1 -0
  61. package/dist/core/__tests__/validate.test.js +53 -0
  62. package/dist/core/__tests__/validate.test.js.map +1 -0
  63. package/dist/core/archive.d.ts +2 -0
  64. package/dist/core/archive.d.ts.map +1 -0
  65. package/dist/core/archive.js +59 -0
  66. package/dist/core/archive.js.map +1 -0
  67. package/dist/core/auth.d.ts +15 -0
  68. package/dist/core/auth.d.ts.map +1 -0
  69. package/dist/core/auth.js +76 -0
  70. package/dist/core/auth.js.map +1 -0
  71. package/dist/core/codemod.d.ts +20 -0
  72. package/dist/core/codemod.d.ts.map +1 -0
  73. package/dist/core/codemod.js +150 -0
  74. package/dist/core/codemod.js.map +1 -0
  75. package/dist/core/fsx.d.ts +12 -0
  76. package/dist/core/fsx.d.ts.map +1 -0
  77. package/dist/core/fsx.js +70 -0
  78. package/dist/core/fsx.js.map +1 -0
  79. package/dist/core/http.d.ts +30 -0
  80. package/dist/core/http.d.ts.map +1 -0
  81. package/dist/core/http.js +95 -0
  82. package/dist/core/http.js.map +1 -0
  83. package/dist/core/journal.d.ts +18 -0
  84. package/dist/core/journal.d.ts.map +1 -0
  85. package/dist/core/journal.js +34 -0
  86. package/dist/core/journal.js.map +1 -0
  87. package/dist/core/log.d.ts +8 -0
  88. package/dist/core/log.d.ts.map +1 -0
  89. package/dist/core/log.js +9 -0
  90. package/dist/core/log.js.map +1 -0
  91. package/dist/core/pathGuard.d.ts +3 -0
  92. package/dist/core/pathGuard.d.ts.map +1 -0
  93. package/dist/core/pathGuard.js +18 -0
  94. package/dist/core/pathGuard.js.map +1 -0
  95. package/dist/core/paths.d.ts +11 -0
  96. package/dist/core/paths.d.ts.map +1 -0
  97. package/dist/core/paths.js +22 -0
  98. package/dist/core/paths.js.map +1 -0
  99. package/dist/core/validate.d.ts +8 -0
  100. package/dist/core/validate.d.ts.map +1 -0
  101. package/dist/core/validate.js +27 -0
  102. package/dist/core/validate.js.map +1 -0
  103. package/dist/index.d.ts +3 -0
  104. package/dist/index.d.ts.map +1 -0
  105. package/dist/index.js +23 -0
  106. package/dist/index.js.map +1 -0
  107. package/docs/decisions.md +55 -0
  108. package/package.json +39 -0
  109. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  110. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  111. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  112. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  113. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  114. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  115. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  116. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  117. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  118. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  119. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  120. package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  121. package/recipes/audio-recorder/recipe.json +22 -0
  122. package/recipes/audio-recorder@latest.zip +0 -0
  123. package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
  124. package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
  125. package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
  126. package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
  127. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
  128. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
  129. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
  130. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
  131. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
  132. package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
  133. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
  134. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
  135. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
  136. package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
  137. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
  138. package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
  139. package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
  140. package/recipes/charts/recipe.json +22 -0
  141. package/recipes/charts@latest.zip +0 -0
  142. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  143. package/recipes/chatbot/apps/native/src/features/chatbot/app/index.tsx +302 -0
  144. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  145. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
  146. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  147. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
  148. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  149. package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
  150. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  151. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  152. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  153. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  154. package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
  155. package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  156. package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  157. package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  158. package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
  159. package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  160. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
  161. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
  162. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
  163. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  164. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
  165. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  166. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  167. package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
  168. package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
  169. package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
  170. package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
  171. package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
  172. package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
  173. package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
  174. package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
  175. package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  176. package/recipes/chatbot/recipe.json +22 -0
  177. package/recipes/chatbot@latest.zip +0 -0
  178. package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
  179. package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
  180. package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
  181. package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  182. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
  183. package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
  184. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  185. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
  186. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  187. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  188. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  189. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
  190. package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
  191. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
  192. package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
  193. package/recipes/image-generator/recipe.json +22 -0
  194. package/recipes/image-generator@latest.zip +0 -0
  195. package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
  196. package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
  197. package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
  198. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
  199. package/recipes/quiz/recipe.json +22 -0
  200. package/recipes/quiz@latest.zip +0 -0
  201. package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
  202. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
  203. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
  204. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
  205. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
  206. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
  207. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
  208. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
  209. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
  210. package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
  211. package/recipes/tracker-app/recipe.json +22 -0
  212. package/recipes/tracker-app@latest.zip +0 -0
  213. package/recipes/upload-all.sh +32 -0
  214. package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
  215. package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
  216. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
  217. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
  218. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
  219. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
  220. package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
  221. package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
  222. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
  223. package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
  224. package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
  225. package/recipes/voice-bot/recipe.json +22 -0
  226. package/recipes/voice-bot@latest.zip +0 -0
  227. package/scripts/create-recipes.mjs +189 -0
  228. package/src/commands/add.ts +183 -0
  229. package/src/commands/devices.ts +38 -0
  230. package/src/commands/doctor.ts +67 -0
  231. package/src/commands/list.ts +45 -0
  232. package/src/commands/login.ts +24 -0
  233. package/src/commands/logout.ts +15 -0
  234. package/src/commands/remove.ts +78 -0
  235. package/src/core/__tests__/journal.test.ts +119 -0
  236. package/src/core/__tests__/validate.test.ts +64 -0
  237. package/src/core/archive.ts +69 -0
  238. package/src/core/auth.ts +103 -0
  239. package/src/core/codemod.ts +211 -0
  240. package/src/core/fsx.ts +80 -0
  241. package/src/core/http.ts +136 -0
  242. package/src/core/journal.ts +64 -0
  243. package/src/core/log.ts +9 -0
  244. package/src/core/pathGuard.ts +22 -0
  245. package/src/core/paths.ts +33 -0
  246. package/src/core/validate.ts +44 -0
  247. package/src/index.ts +27 -0
  248. package/test-critical-cases.mjs +258 -0
  249. package/tsconfig.json +21 -0
  250. package/vitest.config.mts +12 -0
@@ -0,0 +1,246 @@
1
+ import { Feather, Ionicons } from '@expo/vector-icons';
2
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
3
+ import type { Message } from 'ai'; // Base Message type
4
+ import * as Clipboard from 'expo-clipboard';
5
+ import React from 'react';
6
+ import { View } from 'react-native';
7
+
8
+ import { Image, Pressable, Text } from '@/components/ui';
9
+ import { useToast } from '@/components/ui/utils';
10
+ import { ChatMarkdown } from '@/features/chatbot/components/chat-markdown';
11
+ import { translate } from '@/lib';
12
+ import { useThemeConfig } from '@/lib/use-theme-config';
13
+
14
+ // Local definition for MessageContentPart compatible with Vercel AI SDK structure
15
+ // This should be consistent with how `useChat` hook and AI SDK handle content.
16
+ export type LocalMessageContentPart =
17
+ | { type: 'text'; text: string }
18
+ | { type: 'image'; image: string | URL }; // Allow string for input from client, server can convert to URL
19
+
20
+ type ConvexAttachment = {
21
+ type: 'image';
22
+ storageId: Id<'_storage'>;
23
+ url?: string; // This is now pre-fetched from the parent component
24
+ fileName?: string;
25
+ mimeType?: string;
26
+ };
27
+
28
+ // The message prop for ChatMessageBubble
29
+ export type AppMessage = Omit<Message, 'content'> & {
30
+ content: string | LocalMessageContentPart[]; // Content matches AI SDK
31
+ attachments?: ConvexAttachment[]; // Custom prop for Convex-stored attachments
32
+ };
33
+
34
+ type ChatMessageBubbleProps = {
35
+ message: AppMessage;
36
+ onReportMessage?: (message: AppMessage) => void; // AppMessage type for handler
37
+ };
38
+
39
+ const ConvexImage: React.FC<{
40
+ storageId: Id<'_storage'>;
41
+ isUser: boolean;
42
+ fileName?: string;
43
+ preloadedUrl?: string | null; // Pre-fetched URL from parent
44
+ }> = React.memo(({ storageId, isUser, fileName, preloadedUrl }) => {
45
+ // Use pre-loaded URL if available, otherwise show loading
46
+ const imageUrl = preloadedUrl;
47
+ const theme = useThemeConfig();
48
+
49
+ // Fixed aspect ratio container to prevent layout shift when image loads
50
+ return (
51
+ <View
52
+ className="my-1 w-48 overflow-hidden rounded-lg"
53
+ style={{ aspectRatio: 1.5, minHeight: 128 }}
54
+ testID={`convex-image-container-${storageId}`}
55
+ >
56
+ {!imageUrl ? (
57
+ <View
58
+ className="flex-1 items-center justify-center"
59
+ style={{ backgroundColor: theme.colors.muted }}
60
+ >
61
+ <Text
62
+ className={`text-sm ${isUser ? 'text-white' : 'text-neutral-500 dark:text-neutral-400'}`}
63
+ >
64
+ Loading{fileName ? ` ${fileName}` : ''}...
65
+ </Text>
66
+ </View>
67
+ ) : (
68
+ <Image
69
+ source={{ uri: imageUrl }}
70
+ className="flex-1"
71
+ accessibilityRole="image"
72
+ accessibilityLabel={
73
+ fileName || 'Message image attachment from storage'
74
+ }
75
+ testID={`convex-image-loaded-${storageId}`}
76
+ contentFit="cover"
77
+ transition={0}
78
+ />
79
+ )}
80
+ </View>
81
+ );
82
+ });
83
+
84
+ export const ChatMessageBubble: React.FC<ChatMessageBubbleProps> = ({
85
+ message,
86
+ onReportMessage,
87
+ }) => {
88
+ const theme = useThemeConfig();
89
+ const isUser = message.role === 'user';
90
+ const isBotMessage = message.role === 'assistant';
91
+ const toast = useToast();
92
+
93
+ const handleCopyMessage = async () => {
94
+ try {
95
+ let textToCopy = '';
96
+ if (typeof message.content === 'string') {
97
+ textToCopy = message.content;
98
+ } else if (Array.isArray(message.content)) {
99
+ textToCopy = message.content
100
+ .filter((part) => part.type === 'text')
101
+ .map((part) => part.text)
102
+ .join('\n');
103
+ }
104
+
105
+ if (textToCopy) {
106
+ await Clipboard.setStringAsync(textToCopy);
107
+ } else {
108
+ toast.warning(translate('chatbot.empty_message'), { duration: 2000 });
109
+ }
110
+ } catch (error) {
111
+ console.error('[CHATBOT_FRONTEND] Copy failed:', {
112
+ error: error instanceof Error ? error.message : 'Unknown error',
113
+ });
114
+ toast.error(translate('chatbot.copy_failed'), { duration: 3000 });
115
+ }
116
+ };
117
+
118
+ // Separate text content from images
119
+ const textContent: string[] = [];
120
+ const imageContent: React.ReactNode[] = [];
121
+
122
+ // Handle message.content (string or LocalMessageContentPart[])
123
+ if (typeof message.content === 'string') {
124
+ if (message.content.trim()) {
125
+ textContent.push(message.content);
126
+ }
127
+ } else if (Array.isArray(message.content)) {
128
+ message.content.forEach((part, index) => {
129
+ if (part.type === 'text' && part.text) {
130
+ textContent.push(part.text);
131
+ } else if (part.type === 'image' && part.image) {
132
+ const uri =
133
+ typeof part.image === 'string' ? part.image : part.image.toString();
134
+
135
+ imageContent.push(
136
+ <Image
137
+ key={`content-image-${index}`}
138
+ source={{ uri }}
139
+ className="mb-2 h-32 w-48 rounded-2xl"
140
+ accessibilityRole="image"
141
+ accessibilityLabel="Image from message content"
142
+ testID={`content-image-${index}`}
143
+ contentFit="cover"
144
+ />,
145
+ );
146
+ }
147
+ });
148
+ }
149
+
150
+ // Handle message.attachments (from Convex) - now with pre-fetched URLs
151
+ if (message.attachments && message.attachments.length > 0) {
152
+ message.attachments.forEach((attachment, index) => {
153
+ if (attachment.type === 'image' && attachment.storageId) {
154
+ imageContent.push(
155
+ <ConvexImage
156
+ key={`attachment-image-${attachment.storageId}-${index}`}
157
+ storageId={attachment.storageId}
158
+ isUser={isUser}
159
+ fileName={attachment.fileName}
160
+ preloadedUrl={attachment.url}
161
+ />,
162
+ );
163
+ }
164
+ });
165
+ }
166
+
167
+ return (
168
+ <View
169
+ className={`mb-1 ${isUser ? 'items-end' : 'items-start'}`}
170
+ accessibilityLabel={`${isUser ? 'Your' : 'Assistant'} message`}
171
+ >
172
+ {/* Images displayed outside bubble */}
173
+ {imageContent.length > 0 && (
174
+ <View className={`mb-2 ${isUser ? 'items-end' : 'items-start'}`}>
175
+ {imageContent}
176
+ </View>
177
+ )}
178
+
179
+ {/* Text bubble */}
180
+ {textContent.length > 0 && (
181
+ <View className={`w-full ${isUser ? 'items-end' : 'items-start'}`}>
182
+ <View
183
+ className={`${isUser ? 'max-w-[80%] rounded-3xl bg-neutral-200 px-5 py-4 dark:bg-neutral-800' : 'w-full py-2'}`}
184
+ >
185
+ {isBotMessage ? (
186
+ <ChatMarkdown content={textContent.join('\n\n')} />
187
+ ) : (
188
+ textContent.map((text, index) => (
189
+ <Text
190
+ key={`text-${index}`}
191
+ className={`text-[15.8px] ${isUser ? 'text-neutral-900 dark:text-white' : 'text-black dark:text-white'}`}
192
+ selectable
193
+ selectionColor={theme.colors.primary}
194
+ >
195
+ {text}
196
+ </Text>
197
+ ))
198
+ )}
199
+ </View>
200
+
201
+ {isBotMessage && (textContent.length > 0 || onReportMessage) && (
202
+ <View className="flex-row gap-x-2">
203
+ {textContent.length > 0 && (
204
+ <Pressable
205
+ onPress={handleCopyMessage}
206
+ className="rounded p-1 opacity-60 hover:opacity-100 active:opacity-40"
207
+ accessibilityRole="button"
208
+ accessibilityLabel={translate('chatbot.copy_message')}
209
+ accessibilityHint={translate('chatbot.copy_hint')}
210
+ testID="copy-message-button"
211
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
212
+ >
213
+ <Feather
214
+ name="copy"
215
+ size={20}
216
+ color={theme.colors.mutedForeground}
217
+ />
218
+ </Pressable>
219
+ )}
220
+
221
+ {onReportMessage && (
222
+ <Pressable
223
+ onPress={() => onReportMessage(message)}
224
+ className="rounded p-1 opacity-60 hover:opacity-100 active:opacity-40"
225
+ accessibilityRole="button"
226
+ accessibilityLabel={translate('chatbot.report_message')}
227
+ accessibilityHint={translate('chatbot.report_hint')}
228
+ testID="report-message-button"
229
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
230
+ >
231
+ <Ionicons
232
+ name="flag-outline"
233
+ size={20}
234
+ color={theme.colors.mutedForeground}
235
+ />
236
+ </Pressable>
237
+ )}
238
+ </View>
239
+ )}
240
+ </View>
241
+ )}
242
+
243
+ {/* If both text and images are empty (e.g. streaming just started), omit the placeholder bubble */}
244
+ </View>
245
+ );
246
+ };
@@ -0,0 +1,161 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Modal, Select, Text, useModal } from '@/components/ui';
5
+ import type { OptionType } from '@/components/ui/core/select';
6
+ import { useEntitlement } from '@/features/payments/hooks/use-entitlement';
7
+
8
+ import {
9
+ getModelsByProvider,
10
+ PREMIUM_MODELS,
11
+ type Provider,
12
+ } from '../constants/models';
13
+ import type { SearchMode } from '../hooks/use-chatbot-settings';
14
+
15
+ const PROVIDER_OPTIONS: OptionType[] = [
16
+ { label: 'OpenAI', value: 'openai' },
17
+ { label: 'Gemini', value: 'gemini' },
18
+ ];
19
+
20
+ const SEARCH_MODE_OPTIONS: OptionType[] = [
21
+ { label: 'Provider-Optimized', value: 'provider' },
22
+ { label: 'Tavily Search', value: 'tavily' },
23
+ { label: 'None', value: 'none' },
24
+ ];
25
+
26
+ type ChatSettingsModalProps = {
27
+ selectedProvider: Provider;
28
+ selectedModel: string;
29
+ selectedSearchMode: SearchMode;
30
+ onProviderChange: (provider: Provider) => void;
31
+ onModelChange: (model: string) => void;
32
+ onSearchModeChange: (searchMode: SearchMode) => void;
33
+ };
34
+
35
+ export type ChatSettingsModalRef = {
36
+ present: () => void;
37
+ dismiss: () => void;
38
+ };
39
+
40
+ export const ChatSettingsModal = React.forwardRef<
41
+ ChatSettingsModalRef,
42
+ ChatSettingsModalProps
43
+ >(
44
+ (
45
+ {
46
+ selectedProvider,
47
+ selectedModel,
48
+ selectedSearchMode,
49
+ onProviderChange,
50
+ onModelChange,
51
+ onSearchModeChange,
52
+ },
53
+ ref,
54
+ ) => {
55
+ const modal = useModal();
56
+ const { isEntitled, isLoading: isEntitlementLoading } =
57
+ useEntitlement('premium_access');
58
+
59
+ const premiumModelIds = React.useMemo(() => new Set(PREMIUM_MODELS()), []);
60
+
61
+ const baseModelOptions = React.useMemo<OptionType[]>(
62
+ () =>
63
+ getModelsByProvider(selectedProvider).map((model) => ({
64
+ label: model.name,
65
+ value: model.id,
66
+ })),
67
+ [selectedProvider],
68
+ );
69
+
70
+ // Filter model options based on entitlement
71
+ const modelOptions = React.useMemo(
72
+ () =>
73
+ baseModelOptions
74
+ .filter((option) => {
75
+ const isPremium = premiumModelIds.has(option.value as string);
76
+ return isPremium ? isEntitled : true;
77
+ })
78
+ .map((option) => {
79
+ const isPremium = premiumModelIds.has(option.value as string);
80
+ if (isPremium && isEntitled) {
81
+ return {
82
+ ...option,
83
+ label: `${option.label} ⭐`,
84
+ };
85
+ }
86
+ return option;
87
+ }),
88
+ [baseModelOptions, isEntitled, premiumModelIds],
89
+ );
90
+
91
+ const handleProviderChange = (value: string | number) => {
92
+ const provider = value as Provider;
93
+ onProviderChange(provider);
94
+
95
+ // Reset to first model of new provider if current model doesn't exist
96
+ const newModelOptions = getModelsByProvider(provider);
97
+ const modelExists = newModelOptions.some(
98
+ (model) => model.id === selectedModel,
99
+ );
100
+ if (!modelExists && newModelOptions.length > 0) {
101
+ onModelChange(newModelOptions[0].id);
102
+ }
103
+ };
104
+
105
+ const handleModelChange = (value: string | number) => {
106
+ onModelChange(value as string);
107
+ };
108
+
109
+ const handleSearchModeChange = (value: string | number) => {
110
+ onSearchModeChange(value as SearchMode);
111
+ };
112
+
113
+ // Expose modal controls via imperative handle
114
+ React.useImperativeHandle(ref, () => ({
115
+ present: modal.present,
116
+ dismiss: modal.dismiss,
117
+ }));
118
+
119
+ return (
120
+ <Modal ref={modal.ref} title="Chat Settings">
121
+ <View className="gap-6 p-4">
122
+ <View className="gap-2">
123
+ <Text className="text-base font-medium">AI Provider</Text>
124
+ <Select
125
+ options={PROVIDER_OPTIONS}
126
+ value={selectedProvider}
127
+ onSelect={handleProviderChange}
128
+ placeholder="Select AI Provider"
129
+ testID="provider-select"
130
+ />
131
+ </View>
132
+
133
+ <View className="gap-2">
134
+ <Text className="text-base font-medium">Model</Text>
135
+ <Select
136
+ options={modelOptions}
137
+ value={selectedModel}
138
+ onSelect={handleModelChange}
139
+ placeholder="Select Model"
140
+ testID="model-select"
141
+ disabled={isEntitlementLoading}
142
+ />
143
+ </View>
144
+
145
+ <View className="gap-2">
146
+ <Text className="text-base font-medium">Search Mode</Text>
147
+ <Select
148
+ options={SEARCH_MODE_OPTIONS}
149
+ value={selectedSearchMode}
150
+ onSelect={handleSearchModeChange}
151
+ placeholder="Select Search Mode"
152
+ testID="search-mode-select"
153
+ />
154
+ </View>
155
+ </View>
156
+ </Modal>
157
+ );
158
+ },
159
+ );
160
+
161
+ ChatSettingsModal.displayName = 'ChatSettingsModal';
@@ -0,0 +1,115 @@
1
+ import { Feather } from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { ActivityIndicator, ScrollView, View } from 'react-native';
4
+
5
+ import { Image, Pressable, Text } from '@/components/ui';
6
+ import { useThemeConfig } from '@/lib/use-theme-config';
7
+
8
+ export type ImageUploadState =
9
+ | { status: 'pending'; progress: 0 }
10
+ | { status: 'uploading'; progress: number }
11
+ | { status: 'completed'; storageId: string; progress: 100 }
12
+ | { status: 'error'; error: string; progress: 0 };
13
+
14
+ export type SelectedImage = {
15
+ uri: string;
16
+ type: string;
17
+ fileName?: string;
18
+ id: string;
19
+ uploadState: ImageUploadState;
20
+ storageId?: string;
21
+ mimeType?: string;
22
+ };
23
+
24
+ type ImagePreviewListProps = {
25
+ images: SelectedImage[];
26
+ onRemoveImage: (id: string) => void;
27
+ onRetryUpload?: (id: string) => void;
28
+ };
29
+
30
+ /**
31
+ * Component for displaying and managing selected image previews with upload progress
32
+ */
33
+ export const ImagePreviewList: React.FC<ImagePreviewListProps> = ({
34
+ images,
35
+ onRemoveImage,
36
+ onRetryUpload,
37
+ }) => {
38
+ const theme = useThemeConfig();
39
+
40
+ if (images.length === 0) return null;
41
+
42
+ return (
43
+ <ScrollView
44
+ horizontal
45
+ showsHorizontalScrollIndicator={false}
46
+ className="mb-1"
47
+ contentContainerStyle={{ gap: 8 }}
48
+ >
49
+ {images.map((image, index) => {
50
+ const isUploading =
51
+ image.uploadState.status === 'uploading' ||
52
+ image.uploadState.status === 'pending';
53
+ const isError = image.uploadState.status === 'error';
54
+
55
+ return (
56
+ <View key={image.id} className="relative p-2">
57
+ <Image
58
+ source={{ uri: image.uri }}
59
+ className="size-20 rounded-xl border border-neutral-200 dark:border-neutral-800"
60
+ contentFit="cover"
61
+ accessibilityRole="image"
62
+ accessibilityLabel={`Selected image ${index + 1}`}
63
+ style={{ opacity: isUploading ? 0.5 : 1 }}
64
+ />
65
+
66
+ {/* Upload Progress Loader */}
67
+ {isUploading && (
68
+ <View className="absolute inset-2 items-center justify-center rounded-xl">
69
+ <ActivityIndicator
70
+ size="small"
71
+ color={theme.dark ? '#fff' : '#000'}
72
+ />
73
+ </View>
74
+ )}
75
+
76
+ {/* Error State Overlay */}
77
+ {isError && (
78
+ <View className="absolute inset-2 items-center justify-center rounded-xl bg-danger-500/90">
79
+ <Feather name="alert-circle" size={20} color="#fff" />
80
+ <Text className="mt-1 text-xs font-medium text-white">
81
+ Failed
82
+ </Text>
83
+ {onRetryUpload && (
84
+ <Pressable
85
+ onPress={() => onRetryUpload(image.id)}
86
+ className="mt-1 rounded bg-white/20 px-2 py-1"
87
+ accessibilityLabel="Retry upload"
88
+ accessibilityRole="button"
89
+ >
90
+ <Text className="text-xs font-medium text-white">
91
+ Retry
92
+ </Text>
93
+ </Pressable>
94
+ )}
95
+ </View>
96
+ )}
97
+
98
+ {/* Remove Button */}
99
+ <Pressable
100
+ onPress={() => onRemoveImage(image.id)}
101
+ className="absolute right-0 top-0 z-50 size-[26px] rounded-full bg-danger-500/70 dark:bg-danger-600/70"
102
+ accessibilityLabel={`Remove image ${index + 1}`}
103
+ accessibilityRole="button"
104
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
105
+ >
106
+ <Text className="mt-1 items-center justify-center text-center text-sm text-white">
107
+
108
+ </Text>
109
+ </Pressable>
110
+ </View>
111
+ );
112
+ })}
113
+ </ScrollView>
114
+ );
115
+ };
@@ -0,0 +1,165 @@
1
+ import { Feather } from '@expo/vector-icons';
2
+ import React, { useCallback, useState } from 'react';
3
+ import {
4
+ type ImageStyle,
5
+ Pressable,
6
+ type TextStyle,
7
+ View,
8
+ type ViewStyle,
9
+ } from 'react-native';
10
+ import SyntaxHighlighter from 'react-native-syntax-highlighter';
11
+
12
+ import { translate } from '@/lib';
13
+
14
+ type CodeBlockProps = {
15
+ containerStyle: ViewStyle | TextStyle | ImageStyle;
16
+ content: string;
17
+ language: string;
18
+ codeSyntaxTheme: any;
19
+ monospaceFontFamily: string;
20
+ subtleText: string;
21
+ codeContentStyle: ViewStyle;
22
+ codeActionsContainerStyle: ViewStyle;
23
+ codeActionButtonBaseStyle: ViewStyle;
24
+ codeToggleButtonSpacingStyle: ViewStyle;
25
+ codeContentContainerStyle: ViewStyle;
26
+ onCopyCode: (code: string) => Promise<void>;
27
+ };
28
+
29
+ /**
30
+ * CodeBlock component for rendering syntax-highlighted code with collapse/expand functionality.
31
+ *
32
+ * Features:
33
+ * - Syntax highlighting using react-native-syntax-highlighter
34
+ * - Copy to clipboard functionality
35
+ * - Collapse/expand animation with FlashList preparation
36
+ * - Smooth 220ms easeInEaseOut animation
37
+ * - Independent animation for multiple code blocks
38
+ *
39
+ * Requirements addressed:
40
+ * - 9.1: Prepare FlashList for layout animation before collapse
41
+ * - 9.2: Animate height change over 220ms with easeInEaseOut
42
+ * - 9.3: Animate each code block independently
43
+ * - 9.4: Restore full content with same animation timing
44
+ * - 9.5: Complete animation without interruption during scroll
45
+ *
46
+ * @param props - CodeBlock component props
47
+ * @returns Rendered code block with syntax highlighting and controls
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * <CodeBlock
52
+ * containerStyle={styles.code_block}
53
+ * content="const hello = 'world';"
54
+ * language="javascript"
55
+ * codeSyntaxTheme={atomOneDark}
56
+ * monospaceFontFamily="Menlo"
57
+ * subtleText="#888"
58
+ * codeContentStyle={styles.codeContent}
59
+ * codeActionsContainerStyle={styles.actions}
60
+ * codeActionButtonBaseStyle={styles.button}
61
+ * codeToggleButtonSpacingStyle={styles.spacing}
62
+ * codeContentContainerStyle={styles.container}
63
+ * onCopyCode={copyToClipboard}
64
+ * onPrepareLayoutAnimation={prepareFlashList}
65
+ * />
66
+ * ```
67
+ */
68
+ export const CodeBlock = React.memo(function CodeBlock({
69
+ containerStyle,
70
+ content,
71
+ language,
72
+ codeSyntaxTheme,
73
+ monospaceFontFamily,
74
+ subtleText,
75
+ codeContentStyle,
76
+ codeActionsContainerStyle,
77
+ codeActionButtonBaseStyle,
78
+ codeToggleButtonSpacingStyle,
79
+ codeContentContainerStyle,
80
+ onCopyCode,
81
+ }: CodeBlockProps) {
82
+ const [isCollapsed, setIsCollapsed] = useState(false);
83
+
84
+ /**
85
+ * Toggle collapsed state without animation.
86
+ * LayoutAnimation causes scroll jitter, so we disable it.
87
+ */
88
+ const toggleCollapsed = useCallback(() => {
89
+ setIsCollapsed((prev) => !prev);
90
+ }, []);
91
+
92
+ return (
93
+ <View
94
+ style={[
95
+ containerStyle as ViewStyle,
96
+ { position: 'relative' as ViewStyle['position'] },
97
+ ]}
98
+ >
99
+ <View style={codeActionsContainerStyle} pointerEvents="box-none">
100
+ <Pressable
101
+ onPress={() => onCopyCode(content)}
102
+ style={({ pressed }) => [
103
+ codeActionButtonBaseStyle,
104
+ pressed ? { opacity: 0.6 } : null,
105
+ ]}
106
+ accessibilityRole="button"
107
+ accessibilityLabel={translate('chatbot.copy_code')}
108
+ accessibilityHint={translate('chatbot.copy_code_hint')}
109
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
110
+ >
111
+ <Feather name="copy" size={16} color={subtleText} />
112
+ </Pressable>
113
+
114
+ <Pressable
115
+ onPress={toggleCollapsed}
116
+ style={({ pressed }) => [
117
+ codeActionButtonBaseStyle,
118
+ codeToggleButtonSpacingStyle,
119
+ pressed ? { opacity: 0.6 } : null,
120
+ ]}
121
+ accessibilityRole="button"
122
+ accessibilityLabel={
123
+ translate(
124
+ isCollapsed
125
+ ? 'chatbot.expand_code_block'
126
+ : 'chatbot.collapse_code_block',
127
+ ) ?? undefined
128
+ }
129
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
130
+ >
131
+ <View
132
+ style={{
133
+ transform: [{ rotate: isCollapsed ? '180deg' : '0deg' }],
134
+ }}
135
+ >
136
+ <Feather name="chevron-down" size={18} color={subtleText} />
137
+ </View>
138
+ </Pressable>
139
+ </View>
140
+
141
+ <View
142
+ style={[
143
+ codeContentContainerStyle,
144
+ isCollapsed ? { marginTop: 0 } : null,
145
+ ]}
146
+ pointerEvents={isCollapsed ? 'none' : 'auto'}
147
+ >
148
+ {isCollapsed ? null : (
149
+ <SyntaxHighlighter
150
+ language={language.length > 0 ? language : 'plaintext'}
151
+ style={codeSyntaxTheme}
152
+ highlighter="hljs"
153
+ fontFamily={monospaceFontFamily}
154
+ fontSize={15}
155
+ PreTag={View}
156
+ CodeTag={View}
157
+ customStyle={codeContentStyle}
158
+ >
159
+ {content}
160
+ </SyntaxHighlighter>
161
+ )}
162
+ </View>
163
+ </View>
164
+ );
165
+ });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Markdown rendering components for chatbot messages.
3
+ *
4
+ * This module exports specialized components for rendering markdown content
5
+ * in chat messages, including code blocks with syntax highlighting and
6
+ * tables with intelligent column sizing.
7
+ */
8
+
9
+ export { CodeBlock } from './code-block';
10
+ export { TableRenderer } from './table-renderer';