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,469 @@
1
+ import { AntDesign, Feather } from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { useCallback, useMemo, useRef, useState } from 'react';
4
+ import { ActivityIndicator, Alert, View } from 'react-native';
5
+
6
+ import { sharedApi } from '@/api-client/shared';
7
+ import { Input, Pressable } from '@/components/ui';
8
+ import type { SelectedImage } from '@/features/chatbot/components/image-preview-list';
9
+ import { ImagePreviewList } from '@/features/chatbot/components/image-preview-list';
10
+ import { ModelSelector } from '@/features/chatbot/components/model-selector';
11
+ import { SuggestedMessages } from '@/features/chatbot/components/suggested-messages';
12
+ import type { ModelType } from '@/features/chatbot/constants/models';
13
+ import { DEFAULT_MODELS } from '@/features/chatbot/constants/models';
14
+ import { useImagePicker } from '@/features/chatbot/hooks/use-image-picker';
15
+ import { useFileUploadService } from '@/features/chatbot/services/file-uploader';
16
+ import {
17
+ createChatTelemetryTracker,
18
+ logChatTelemetryEvent,
19
+ } from '@/features/chatbot/utils/chat-telemetry';
20
+ import { useEntitlement } from '@/features/payments/hooks/use-entitlement';
21
+ import { useThemeConfig } from '@/lib/use-theme-config';
22
+
23
+ // Helper to generate unique IDs
24
+ function generateId(): string {
25
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
26
+ }
27
+
28
+ type ChatInputBarProps = {
29
+ /** Current text input value */
30
+ input: string;
31
+ /** Handler for text input changes */
32
+ onInputChange: (text: string) => void;
33
+ /** Handler for sending message with text and/or images */
34
+ onSubmit: (
35
+ attachments?: {
36
+ type: 'image';
37
+ storageId: string;
38
+ fileName?: string;
39
+ mimeType: string;
40
+ }[],
41
+ ) => Promise<void>;
42
+ /** Whether the chat is currently loading/sending */
43
+ isLoading: boolean;
44
+ /** Whether to show suggested messages (only when conversation is empty) */
45
+ showSuggestedMessages?: boolean;
46
+ /** Currently selected AI model */
47
+ selectedModel?: ModelType;
48
+ /** Handler for model selection changes */
49
+ onModelChange?: (model: ModelType) => void;
50
+ /** Test ID for E2E testing */
51
+ testID?: string;
52
+ };
53
+
54
+ /**
55
+ * Chat input bar component with support for text input and multiple image attachments.
56
+ * Handles image selection, preview, removal, and upload progress.
57
+ */
58
+ export const ChatInputBar: React.FC<ChatInputBarProps> = ({
59
+ input,
60
+ onInputChange,
61
+ onSubmit,
62
+ isLoading,
63
+ showSuggestedMessages = false,
64
+ selectedModel = DEFAULT_MODELS.openai,
65
+ onModelChange,
66
+ testID = 'chat-input-bar',
67
+ }) => {
68
+ const theme = useThemeConfig();
69
+ const fileUploadService = useFileUploadService();
70
+ const { showImageSourceOptions } = useImagePicker();
71
+ const deleteFileMutation = sharedApi.useDeleteUploadedFile();
72
+
73
+ const [selectedImages, setSelectedImages] = useState<SelectedImage[]>([]);
74
+ const uploadAbortControllersRef = useRef<Map<string, AbortController>>(
75
+ new Map(),
76
+ );
77
+
78
+ // Entitlement-based attachment limits
79
+ const { isEntitled: hasPremium } = useEntitlement('premium_access');
80
+ const maxImages = hasPremium ? 5 : 1;
81
+
82
+ // Update image state helper
83
+ const updateImageState = useCallback(
84
+ (
85
+ id: string,
86
+ updates:
87
+ | Partial<SelectedImage>
88
+ | ((prev: SelectedImage) => SelectedImage),
89
+ ) => {
90
+ setSelectedImages((prev) =>
91
+ prev.map((img) => {
92
+ if (img.id !== id) return img;
93
+ return typeof updates === 'function'
94
+ ? updates(img)
95
+ : { ...img, ...updates };
96
+ }),
97
+ );
98
+ },
99
+ [],
100
+ );
101
+
102
+ // Upload image immediately
103
+ const uploadImageImmediately = useCallback(
104
+ async (image: SelectedImage) => {
105
+ const telemetry = createChatTelemetryTracker(
106
+ 'chat.imageUpload.immediate',
107
+ { imageId: image.id, fileName: image.fileName },
108
+ 0.25,
109
+ );
110
+
111
+ try {
112
+ // Update to uploading state
113
+ updateImageState(image.id, {
114
+ uploadState: { status: 'uploading', progress: 10 },
115
+ });
116
+ telemetry?.mark('upload.started');
117
+
118
+ // Optimize and upload
119
+ updateImageState(image.id, {
120
+ uploadState: { status: 'uploading', progress: 30 },
121
+ });
122
+
123
+ const storageId = await fileUploadService.uploadImage(image.uri);
124
+
125
+ if (!storageId) {
126
+ throw new Error('Upload failed - no storage ID returned');
127
+ }
128
+
129
+ updateImageState(image.id, {
130
+ uploadState: { status: 'uploading', progress: 90 },
131
+ });
132
+
133
+ // Update to completed state
134
+ updateImageState(image.id, {
135
+ uploadState: { status: 'completed', storageId, progress: 100 },
136
+ storageId,
137
+ mimeType: image.type,
138
+ });
139
+
140
+ telemetry?.mark('upload.completed', { storageId });
141
+ telemetry?.finalize('ok', { storageId });
142
+ } catch (error) {
143
+ const errorMessage =
144
+ error instanceof Error ? error.message : 'Unknown error';
145
+
146
+ updateImageState(image.id, {
147
+ uploadState: { status: 'error', error: errorMessage, progress: 0 },
148
+ });
149
+
150
+ telemetry?.mark('upload.failed', { error: errorMessage });
151
+ telemetry?.finalize('error', { error: errorMessage });
152
+
153
+ console.error('[CHATBOT_FRONTEND] Immediate upload failed:', {
154
+ error: errorMessage,
155
+ imageId: image.id,
156
+ });
157
+ }
158
+ },
159
+ [fileUploadService, updateImageState],
160
+ );
161
+
162
+ const handleImageSelection = useCallback(
163
+ (newImages: { uri: string; type: string; fileName?: string }[]) => {
164
+ setSelectedImages((prev) => {
165
+ const remaining = Math.max(0, maxImages - prev.length);
166
+ if (remaining <= 0) {
167
+ Alert.alert(
168
+ 'Limit reached',
169
+ hasPremium
170
+ ? 'You can attach up to 5 images per message.'
171
+ : 'Free plan allows 1 image per message.',
172
+ );
173
+ return prev;
174
+ }
175
+
176
+ const toAdd = newImages.slice(0, remaining);
177
+
178
+ if (newImages.length > toAdd.length) {
179
+ Alert.alert(
180
+ 'Some images not added',
181
+ hasPremium
182
+ ? 'You reached the 5-image limit for premium.'
183
+ : 'Free plan allows only 1 image.',
184
+ );
185
+ }
186
+
187
+ // Convert to SelectedImage with upload state
188
+ const imagesWithState: SelectedImage[] = toAdd.map((img) => ({
189
+ ...img,
190
+ id: generateId(),
191
+ uploadState: { status: 'pending', progress: 0 },
192
+ }));
193
+
194
+ // Start uploads immediately
195
+ imagesWithState.forEach((img) => {
196
+ uploadImageImmediately(img);
197
+ });
198
+
199
+ return [...prev, ...imagesWithState];
200
+ });
201
+ },
202
+ [maxImages, hasPremium, uploadImageImmediately],
203
+ );
204
+
205
+ const removeSelectedImage = useCallback(
206
+ async (id: string) => {
207
+ const image = selectedImages.find((img) => img.id === id);
208
+
209
+ if (!image) return;
210
+
211
+ // If upload completed, delete from storage
212
+ if (image.uploadState.status === 'completed' && image.storageId) {
213
+ try {
214
+ await deleteFileMutation({ storageId: image.storageId });
215
+ } catch (error) {
216
+ console.error('[CHATBOT_FRONTEND] Failed to delete file:', error);
217
+ // Continue with UI removal even if deletion fails
218
+ }
219
+ }
220
+
221
+ // Remove from state
222
+ setSelectedImages((prev) => prev.filter((img) => img.id !== id));
223
+
224
+ // Clean up abort controller if exists
225
+ uploadAbortControllersRef.current.delete(id);
226
+ },
227
+ [selectedImages, deleteFileMutation],
228
+ );
229
+
230
+ const retryUpload = useCallback(
231
+ (id: string) => {
232
+ const image = selectedImages.find((img) => img.id === id);
233
+ if (!image) return;
234
+
235
+ // Reset to pending state
236
+ updateImageState(id, {
237
+ uploadState: { status: 'pending', progress: 0 },
238
+ });
239
+
240
+ // Retry upload
241
+ uploadImageImmediately(image);
242
+ },
243
+ [selectedImages, updateImageState, uploadImageImmediately],
244
+ );
245
+
246
+ const handleSubmit = useCallback(async () => {
247
+ const hasText = input.trim();
248
+ const hasImages = selectedImages.length > 0;
249
+
250
+ if (!hasText && !hasImages) {
251
+ return;
252
+ }
253
+
254
+ // Check upload states
255
+ const hasErrors = selectedImages.some(
256
+ (img) => img.uploadState.status === 'error',
257
+ );
258
+ const isUploading = selectedImages.some(
259
+ (img) =>
260
+ img.uploadState.status === 'uploading' ||
261
+ img.uploadState.status === 'pending',
262
+ );
263
+
264
+ if (hasErrors) {
265
+ Alert.alert(
266
+ 'Upload Error',
267
+ 'Some images failed to upload. Please remove them or retry before sending.',
268
+ );
269
+ return;
270
+ }
271
+
272
+ if (isUploading) {
273
+ Alert.alert(
274
+ 'Upload in Progress',
275
+ 'Please wait for all images to finish uploading.',
276
+ );
277
+ return;
278
+ }
279
+
280
+ logChatTelemetryEvent(
281
+ 'chat.inputBar.submitTapped',
282
+ {
283
+ hasText: Boolean(hasText),
284
+ imageCount: selectedImages.length,
285
+ isLoading,
286
+ },
287
+ 0.05,
288
+ );
289
+
290
+ try {
291
+ // Extract completed attachments
292
+ const attachments = selectedImages
293
+ .filter((img) => img.uploadState.status === 'completed')
294
+ .map((img) => ({
295
+ type: 'image' as const,
296
+ storageId: img.storageId!,
297
+ fileName: img.fileName,
298
+ mimeType: img.mimeType || img.type,
299
+ }));
300
+
301
+ await onSubmit(attachments.length > 0 ? attachments : undefined);
302
+
303
+ logChatTelemetryEvent(
304
+ 'chat.inputBar.submitCompleted',
305
+ {
306
+ attachmentsCount: attachments.length,
307
+ hadText: Boolean(hasText),
308
+ },
309
+ 0.05,
310
+ );
311
+
312
+ // Clear selected images after successful submission
313
+ setSelectedImages([]);
314
+ } catch (error) {
315
+ console.error('[CHATBOT_FRONTEND] Message submission failed:', {
316
+ error: error instanceof Error ? error.message : 'Unknown error',
317
+ });
318
+ Alert.alert('Error', 'Failed to send message. Please try again.');
319
+ logChatTelemetryEvent(
320
+ 'chat.inputBar.submitFailed',
321
+ {
322
+ hasText: Boolean(hasText),
323
+ imageCount: selectedImages.length,
324
+ error:
325
+ error instanceof Error
326
+ ? error.message
327
+ : 'Unknown error encountered',
328
+ },
329
+ 0.05,
330
+ );
331
+ }
332
+ }, [input, isLoading, onSubmit, selectedImages]);
333
+
334
+ const canSend = useMemo(() => {
335
+ const hasText = input.trim().length > 0;
336
+ const hasImages = selectedImages.length > 0;
337
+ const allUploaded = selectedImages.every(
338
+ (img) => img.uploadState.status === 'completed',
339
+ );
340
+ const hasErrors = selectedImages.some(
341
+ (img) => img.uploadState.status === 'error',
342
+ );
343
+ const isUploading = selectedImages.some(
344
+ (img) =>
345
+ img.uploadState.status === 'uploading' ||
346
+ img.uploadState.status === 'pending',
347
+ );
348
+
349
+ return (
350
+ (hasText || (hasImages && allUploaded)) &&
351
+ !isLoading &&
352
+ !hasErrors &&
353
+ !isUploading
354
+ );
355
+ }, [input, selectedImages, isLoading]);
356
+
357
+ return (
358
+ <View className="">
359
+ <SuggestedMessages
360
+ visible={showSuggestedMessages}
361
+ onSelectMessage={onInputChange}
362
+ />
363
+
364
+ <View
365
+ className="m-2 mb-4 mt-0 rounded-[30px] border border-neutral-300/60 bg-neutral-50 p-4 dark:border-neutral-700/70 dark:bg-neutral-900"
366
+ testID={testID}
367
+ >
368
+ <ImagePreviewList
369
+ images={selectedImages}
370
+ onRemoveImage={removeSelectedImage}
371
+ onRetryUpload={retryUpload}
372
+ />
373
+
374
+ {/* Model Selector Row */}
375
+
376
+ <Input
377
+ value={input}
378
+ onChangeText={onInputChange}
379
+ placeholder="Message..."
380
+ multiline
381
+ showClearButton={false}
382
+ className="max-h-32 min-h-12 rounded-2xl border-none px-4 py-0 text-base dark:text-white"
383
+ placeholderTextColor={theme.dark ? '#94a3b8' : '#64748b'}
384
+ accessibilityLabel="Message input"
385
+ accessibilityHint="Type your message to the AI assistant"
386
+ testID="message-input"
387
+ />
388
+
389
+ {/* Input Row */}
390
+ <View className="w-full flex-row items-center justify-between gap-3">
391
+ <View className="flex-row gap-2">
392
+ <Pressable
393
+ onPress={() => {
394
+ if (selectedImages.length >= maxImages) {
395
+ Alert.alert(
396
+ 'Limit reached',
397
+ hasPremium
398
+ ? 'You can attach up to 5 images per message.'
399
+ : 'Free plan allows 1 image per message.',
400
+ );
401
+ return;
402
+ }
403
+ showImageSourceOptions(handleImageSelection);
404
+ }}
405
+ className="mb-2 rounded-full bg-neutral-100 p-3 dark:bg-neutral-800"
406
+ accessibilityLabel="Attach images"
407
+ accessibilityHint="Attach images from camera or gallery"
408
+ accessibilityRole="button"
409
+ testID="attach-image-button"
410
+ >
411
+ <Feather
412
+ name="paperclip"
413
+ size={20}
414
+ color={theme.dark ? '#94a3b8' : '#64748b'}
415
+ />
416
+ </Pressable>
417
+ <View className="flex-row items-center justify-between">
418
+ <ModelSelector
419
+ selectedModel={selectedModel}
420
+ onModelSelect={onModelChange || (() => {})}
421
+ testID="chat-model-selector"
422
+ />
423
+ </View>
424
+ </View>
425
+
426
+ {/* <View className="flex-1">
427
+ <Input
428
+ value={input}
429
+ onChangeText={onInputChange}
430
+ placeholder="Message..."
431
+ multiline
432
+ className="max-h-32 min-h-12 rounded-2xl border border-neutral-300 bg-neutral-50 px-4 py-3 text-base dark:border-neutral-600 dark:bg-neutral-800 dark:text-white"
433
+ placeholderTextColor={theme.dark ? '#94a3b8' : '#64748b'}
434
+ accessibilityLabel="Message input"
435
+ accessibilityHint="Type your message to the AI assistant"
436
+ testID="message-input"
437
+ />
438
+ </View> */}
439
+
440
+ <Pressable
441
+ onPress={handleSubmit}
442
+ disabled={!canSend}
443
+ className={`mb-2 size-12 items-center justify-center rounded-full ${
444
+ canSend
445
+ ? 'bg-neutral-900 dark:bg-white'
446
+ : 'bg-neutral-300 dark:bg-neutral-700'
447
+ }`}
448
+ accessibilityLabel="Send message"
449
+ accessibilityHint="Send your message to the AI assistant"
450
+ testID="send-button"
451
+ >
452
+ {isLoading ? (
453
+ <ActivityIndicator
454
+ size="small"
455
+ color={canSend ? (theme.dark ? '#000' : '#fff') : '#94a3b8'}
456
+ />
457
+ ) : (
458
+ <AntDesign
459
+ name="arrow-up"
460
+ size={20}
461
+ color={canSend ? (theme.dark ? '#000' : '#fff') : '#94a3b8'}
462
+ />
463
+ )}
464
+ </Pressable>
465
+ </View>
466
+ </View>
467
+ </View>
468
+ );
469
+ };