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,359 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import React, { useCallback, useMemo, useState } from 'react';
3
+ import { Alert, Keyboard } from 'react-native';
4
+
5
+ import { chatbotApi } from '@/api-client/chatbot';
6
+ import { reportingApi } from '@/api-client/reporting';
7
+ import { useToast } from '@/components/ui/utils';
8
+ import type { ReportReason } from '@/features/chatbot/constants/report-reasons';
9
+ import { useChatConfig } from '@/features/chatbot/hooks/use-chat-config';
10
+ import {
11
+ type AppAttachment,
12
+ MessageHandlerService,
13
+ } from '@/features/chatbot/services/message-handler-service';
14
+ import type { AppMessage } from '@/features/chatbot/types';
15
+
16
+ type UseChatHandlersOptions = {
17
+ conversationId: string | null;
18
+ authToken?: string;
19
+ initialMessages?: AppMessage[];
20
+ preferredProvider?: string;
21
+ preferredModel?: string;
22
+ searchMode?: string;
23
+ };
24
+
25
+ /**
26
+ * Hook to manage chat interactions, message handling, and reporting
27
+ */
28
+ export const useChatHandlers = (options: UseChatHandlersOptions) => {
29
+ const {
30
+ conversationId,
31
+ authToken,
32
+ initialMessages = [],
33
+ preferredProvider = 'openai',
34
+ preferredModel = 'gpt-5-nano',
35
+ searchMode = 'provider',
36
+ } = options;
37
+
38
+ // console.log('[CHATBOT_FRONTEND] 🎣 useChatHandlers hook 1:', {
39
+ // conversationId,
40
+ // hasAuthToken: !!authToken,
41
+ // initialMessagesCount: initialMessages.length,
42
+ // preferredProvider,
43
+ // preferredModel,
44
+ // searchMode,
45
+ // file: 'src/features/chatbot/hooks/use-chat-handlers.ts',
46
+ // function: 'useChatHandlers',
47
+ // });
48
+
49
+ const [messageToReport, setMessageToReport] = useState<AppMessage | null>(
50
+ null,
51
+ );
52
+ const lastUserAttachments = React.useRef<AppAttachment[] | undefined>(
53
+ undefined,
54
+ );
55
+
56
+ // Persist attachments per live message ID to avoid disappearing images
57
+ const attachmentCacheRef = React.useRef<Record<string, AppAttachment[]>>({});
58
+
59
+ const storeUserMessageMutation = chatbotApi.useStoreMessage();
60
+ const clearConversationMutation = chatbotApi.useClearConversation();
61
+ const deleteUserMessageMutation = chatbotApi.useDeleteUserMessage();
62
+ const reportChatMessageMutation = reportingApi.useReportChatMessage();
63
+ const toast = useToast();
64
+ const handleInputChangeRef = React.useRef<(text: string) => void>(() => {});
65
+
66
+ // Define onStreamFailure BEFORE passing it to useChatConfig
67
+ const onStreamFailure = useCallback(
68
+ async (details: {
69
+ userMessageId: string | null;
70
+ text: string;
71
+ attachments?: {
72
+ type: 'image';
73
+ storageId: string;
74
+ fileName?: string;
75
+ mimeType?: string;
76
+ }[];
77
+ errorMessage?: string;
78
+ }) => {
79
+ const message =
80
+ details.errorMessage ?? 'Message failed to send. Please try again.';
81
+
82
+ // Show error toast to user
83
+ toast.error(message);
84
+
85
+ // Restore user's input text so they can retry
86
+ handleInputChangeRef.current(details.text ?? '');
87
+
88
+ // Delete the failed user message from backend
89
+ if (details.userMessageId) {
90
+ try {
91
+ await deleteUserMessageMutation({
92
+ messageId: details.userMessageId as Id<'messages'>,
93
+ });
94
+ } catch (error) {
95
+ console.warn('[useChatHandlers] Failed to remove failed message', {
96
+ messageId: details.userMessageId,
97
+ error,
98
+ });
99
+ }
100
+ delete attachmentCacheRef.current[details.userMessageId];
101
+ }
102
+
103
+ // Clear attachment cache
104
+ lastUserAttachments.current = undefined;
105
+ },
106
+ [deleteUserMessageMutation, toast],
107
+ );
108
+
109
+ const chatConfig = useChatConfig({
110
+ conversationId,
111
+ authToken,
112
+ initialMessages,
113
+ preferredProvider,
114
+ preferredModel,
115
+ searchMode,
116
+ onStreamFailure,
117
+ });
118
+
119
+ handleInputChangeRef.current = chatConfig.handleInputChange;
120
+
121
+ const displayMessages = useMemo((): AppMessage[] => {
122
+ return MessageHandlerService.transformDisplayMessages(
123
+ chatConfig.messages,
124
+ initialMessages,
125
+ lastUserAttachments.current,
126
+ attachmentCacheRef.current,
127
+ ) as AppMessage[];
128
+ }, [initialMessages, chatConfig.messages]);
129
+
130
+ // After we compute display messages, cache any attachments by live message ID
131
+ React.useEffect(() => {
132
+ if (!displayMessages?.length) return;
133
+ const cache = attachmentCacheRef.current;
134
+ let changed = false;
135
+ for (const m of displayMessages) {
136
+ if (m.attachments && m.attachments.length) {
137
+ const existing = cache[m.id];
138
+ if (!existing || existing.length !== m.attachments.length) {
139
+ cache[m.id] = m.attachments as AppAttachment[];
140
+ changed = true;
141
+ }
142
+ }
143
+ }
144
+ if (changed) {
145
+ // noop: we mutate ref; next memo pass will read updated cache
146
+ }
147
+ }, [displayMessages]);
148
+
149
+ // useEffect(() => {
150
+ // console.log(
151
+ // '[Client useChatHandlers] DisplayMessages updated count:',
152
+ // displayMessages.length
153
+ // );
154
+ // if (displayMessages.length > 0) {
155
+ // const lastMsg = displayMessages[displayMessages.length - 1];
156
+ // if (lastMsg) {
157
+ // console.log(
158
+ // `[Client useChatHandlers] Last display message (ID: ${lastMsg.id}, Role: ${lastMsg.role}, Content Type: ${typeof lastMsg.content}):`,
159
+ // JSON.stringify(lastMsg.content)
160
+ // );
161
+ // }
162
+ // }
163
+ // }, [displayMessages]);
164
+
165
+ const handleReportMessage = useCallback((message: AppMessage) => {
166
+ setMessageToReport(message);
167
+ }, []);
168
+
169
+ const handleSubmitReport = useCallback(
170
+ async (reportData: {
171
+ messageId?: string;
172
+ reason: ReportReason;
173
+ details?: string;
174
+ }): Promise<boolean> => {
175
+ if (!reportData.messageId) {
176
+ console.warn(
177
+ '[Client useChatHandlers] Report dismissed: missing messageId',
178
+ );
179
+ return false;
180
+ }
181
+ try {
182
+ await reportChatMessageMutation({
183
+ messageId: reportData.messageId,
184
+ reason: reportData.reason,
185
+ details: reportData.details,
186
+ });
187
+ Alert.alert('Report Submitted', 'Thank you for your report.', [
188
+ { text: 'OK' },
189
+ ]);
190
+ return true;
191
+ } catch (error) {
192
+ console.error(
193
+ '[Client useChatHandlers] Failed to submit report:',
194
+ error,
195
+ );
196
+ Alert.alert(
197
+ 'Report Failed',
198
+ "We couldn't submit your report. Please try again.",
199
+ [{ text: 'OK' }],
200
+ );
201
+ return false;
202
+ }
203
+ },
204
+ [reportChatMessageMutation],
205
+ );
206
+
207
+ const handleCancelReport = useCallback(() => {
208
+ setMessageToReport(null);
209
+ }, []);
210
+
211
+ const handleTextInputChange = useCallback(
212
+ (text: string) => {
213
+ chatConfig.handleInputChange(text);
214
+ },
215
+ [chatConfig],
216
+ );
217
+
218
+ const handleMessageSubmit = useCallback(
219
+ async (
220
+ attachmentsFromInputBar?: {
221
+ type: 'image';
222
+ storageId: string;
223
+ fileName?: string;
224
+ mimeType: string;
225
+ }[],
226
+ ) => {
227
+ // Dismiss keyboard when sending a message
228
+ Keyboard.dismiss();
229
+
230
+ // Read the current input value just before submission to prevent stale closure bugs
231
+ const currentInput = chatConfig.input;
232
+ const trimmedInput = currentInput.trim();
233
+
234
+ // console.log(
235
+ // '[Client useChatHandlers handleMessageSubmit] Attempting to submit. Input:',
236
+ // currentInput,
237
+ // 'Attachments from InputBar:',
238
+ // attachmentsFromInputBar,
239
+ // 'ConvID:',
240
+ // conversationId
241
+ // );
242
+
243
+ if (!conversationId) {
244
+ console.error(
245
+ '[Client useChatHandlers handleMessageSubmit] Missing conversationId.',
246
+ );
247
+ Alert.alert(
248
+ 'Error',
249
+ 'Chat session not fully initialized. Cannot send message.',
250
+ );
251
+ return;
252
+ }
253
+
254
+ if (
255
+ !trimmedInput &&
256
+ (!attachmentsFromInputBar || attachmentsFromInputBar.length === 0)
257
+ ) {
258
+ console.log(
259
+ '[Client useChatHandlers handleMessageSubmit] Empty message and no attachments. Not submitting.',
260
+ );
261
+ return;
262
+ }
263
+
264
+ // Store attachments in ref for optimistic UI display
265
+ lastUserAttachments.current = attachmentsFromInputBar?.map((att) => ({
266
+ type: 'image' as const,
267
+ storageId: att.storageId as Id<'_storage'>,
268
+ fileName: att.fileName,
269
+ mimeType: att.mimeType,
270
+ }));
271
+
272
+ // Store user message and retrieve the persisted ID before triggering streaming
273
+ const storedMessageId = await MessageHandlerService.storeUserMessage({
274
+ currentInput: trimmedInput,
275
+ conversationId,
276
+ attachmentsFromInputBar,
277
+ storeUserMessageMutation,
278
+ });
279
+
280
+ if (!storedMessageId) {
281
+ console.error(
282
+ '[Client useChatHandlers] Unable to store user message; aborting streaming request.',
283
+ );
284
+ return;
285
+ }
286
+
287
+ // Submit to AI
288
+ const dummyFormEvent = {
289
+ preventDefault: () => {},
290
+ } as React.FormEvent<HTMLFormElement>;
291
+
292
+ try {
293
+ await chatConfig.handleSubmit(dummyFormEvent, {
294
+ body: {
295
+ conversationId: conversationId,
296
+ preferredModel,
297
+ userMessageId: storedMessageId,
298
+ text: trimmedInput,
299
+ attachments:
300
+ attachmentsFromInputBar && attachmentsFromInputBar.length > 0
301
+ ? attachmentsFromInputBar
302
+ : undefined,
303
+ },
304
+ });
305
+ } catch (submitError) {
306
+ console.error(
307
+ '[Client useChatHandlers] Streaming submission failed:',
308
+ submitError,
309
+ );
310
+ return;
311
+ }
312
+
313
+ // console.log(
314
+ // '[Client useChatHandlers handleMessageSubmit] chatConfig.handleSubmit called for bot response.'
315
+ // );
316
+
317
+ // Note: We don't clear lastUserAttachments here anymore.
318
+ // Once initialMessages contains the stored user message (with attachments),
319
+ // the initialEquivalent?.attachments path takes over naturally.
320
+ },
321
+ [conversationId, storeUserMessageMutation, chatConfig, preferredModel],
322
+ );
323
+
324
+ const handleClearChat = useCallback(() => {
325
+ if (!conversationId) {
326
+ console.error('[handleClearChat] Missing conversationId');
327
+ return;
328
+ }
329
+
330
+ MessageHandlerService.clearChat({
331
+ conversationId,
332
+ clearConversationMutation,
333
+ onCleared: () => {
334
+ chatConfig.resetMessages();
335
+ },
336
+ });
337
+ // Reset local caches immediately for a clean slate
338
+ lastUserAttachments.current = undefined;
339
+ attachmentCacheRef.current = {};
340
+ }, [conversationId, clearConversationMutation, chatConfig]);
341
+
342
+ // When conversation changes, reset transient caches to prevent leakage
343
+ React.useEffect(() => {
344
+ lastUserAttachments.current = undefined;
345
+ attachmentCacheRef.current = {};
346
+ }, [conversationId]);
347
+
348
+ return {
349
+ ...chatConfig,
350
+ messages: displayMessages,
351
+ messageToReport,
352
+ handleReportMessage,
353
+ handleSubmitReport,
354
+ handleCancelReport,
355
+ handleTextInputChange,
356
+ handleMessageSubmit,
357
+ handleClearChat,
358
+ };
359
+ };
@@ -0,0 +1,89 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import { defaultCacheService } from '@/core/cache';
4
+
5
+ import type { Provider } from '../constants/models';
6
+
7
+ export type SearchMode = 'provider' | 'tavily' | 'none';
8
+
9
+ type ChatbotSettings = {
10
+ provider: Provider;
11
+ model: string;
12
+ searchMode: SearchMode;
13
+ };
14
+
15
+ const CACHE_KEY = 'chatbot_settings';
16
+
17
+ const DEFAULT_SETTINGS: ChatbotSettings = {
18
+ provider: 'openai',
19
+ model: 'gpt-4.1-nano',
20
+ searchMode: 'tavily',
21
+ };
22
+
23
+ export function useChatbotSettings() {
24
+ const [settings, setSettings] = useState<ChatbotSettings>(DEFAULT_SETTINGS);
25
+ const [isLoaded, setIsLoaded] = useState(false);
26
+
27
+ // Load settings from cache on mount
28
+ useEffect(() => {
29
+ const loadSettings = async () => {
30
+ try {
31
+ const cachedSettings =
32
+ await defaultCacheService.getItem<ChatbotSettings>(CACHE_KEY);
33
+ if (cachedSettings) {
34
+ setSettings(cachedSettings);
35
+ }
36
+ } catch (error) {
37
+ console.error('[useChatbotSettings] Failed to load settings:', error);
38
+ } finally {
39
+ setIsLoaded(true);
40
+ }
41
+ };
42
+
43
+ loadSettings();
44
+ }, []);
45
+
46
+ // Save settings to cache whenever they change
47
+ const updateSettings = useCallback(
48
+ async (newSettings: Partial<ChatbotSettings>) => {
49
+ const updatedSettings = { ...settings, ...newSettings };
50
+ setSettings(updatedSettings);
51
+
52
+ try {
53
+ await defaultCacheService.setItem(CACHE_KEY, updatedSettings);
54
+ } catch (error) {
55
+ console.error('[useChatbotSettings] Failed to save settings:', error);
56
+ }
57
+ },
58
+ [settings],
59
+ );
60
+
61
+ const updateProvider = useCallback(
62
+ (provider: Provider) => {
63
+ updateSettings({ provider });
64
+ },
65
+ [updateSettings],
66
+ );
67
+
68
+ const updateModel = useCallback(
69
+ (model: string) => {
70
+ updateSettings({ model });
71
+ },
72
+ [updateSettings],
73
+ );
74
+
75
+ const updateSearchMode = useCallback(
76
+ (searchMode: SearchMode) => {
77
+ updateSettings({ searchMode });
78
+ },
79
+ [updateSettings],
80
+ );
81
+
82
+ return {
83
+ settings,
84
+ isLoaded,
85
+ updateProvider,
86
+ updateModel,
87
+ updateSearchMode,
88
+ };
89
+ }
@@ -0,0 +1,79 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import { useEffect, useMemo, useState } from 'react';
3
+
4
+ import { chatbotApi } from '@/api-client/chatbot';
5
+ import type {
6
+ AppMessage,
7
+ ConversationMessageFromDB,
8
+ } from '@/features/chatbot/types';
9
+
10
+ /**
11
+ * Hook to manage conversation state and load historical messages
12
+ */
13
+ export const useConversation = (userId?: Id<'users'>) => {
14
+ const getOrCreateConversation = chatbotApi.useGetOrCreateConversation();
15
+ const [conversationId, setConversationId] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ const initializeConversation = async () => {
19
+ if (userId && !conversationId) {
20
+ console.log(
21
+ '[Client useConversation] Initializing conversation for user:',
22
+ userId,
23
+ );
24
+ try {
25
+ const defaultConversationId = await getOrCreateConversation({});
26
+ setConversationId(defaultConversationId);
27
+ console.log(
28
+ '[Client useConversation] Conversation ID set:',
29
+ defaultConversationId,
30
+ );
31
+ } catch (error) {
32
+ console.error(
33
+ '[Client useConversation] Failed to get/create conversation:',
34
+ error,
35
+ );
36
+ }
37
+ }
38
+ };
39
+ initializeConversation();
40
+ }, [userId, conversationId, getOrCreateConversation]);
41
+
42
+ const historicalMessagesResult = chatbotApi.useListMessages(
43
+ conversationId ? (conversationId as Id<'conversations'>) : undefined,
44
+ { numItems: 50, cursor: null },
45
+ );
46
+
47
+ const initialMessages = useMemo((): AppMessage[] => {
48
+ if (!historicalMessagesResult?.page) {
49
+ console.log('[Client useConversation] No historical messages.');
50
+ return [];
51
+ }
52
+ console.log(
53
+ '[Client useConversation] Processing historical messages count:',
54
+ historicalMessagesResult.page.length,
55
+ );
56
+ return historicalMessagesResult.page.reverse().map(
57
+ (msg: ConversationMessageFromDB): AppMessage => ({
58
+ id: msg._id,
59
+ role: msg.authorType === 'user' ? 'user' : 'assistant',
60
+ content: msg.text || '',
61
+ createdAt: new Date(msg._creationTime),
62
+ attachments: msg.attachments,
63
+ toolCalls: msg.toolCalls,
64
+ metadata: msg.metadata,
65
+ }),
66
+ );
67
+ }, [historicalMessagesResult]);
68
+
69
+ useEffect(() => {
70
+ if (initialMessages.length > 0) {
71
+ console.log(
72
+ '[Client useConversation] Initial AppMessages loaded for useChat:',
73
+ initialMessages.length,
74
+ );
75
+ }
76
+ }, [initialMessages]);
77
+
78
+ return { conversationId, initialMessages };
79
+ };
@@ -0,0 +1,122 @@
1
+ import * as ImagePicker from 'expo-image-picker';
2
+ import { useCallback } from 'react';
3
+ import { Alert } from 'react-native';
4
+
5
+ // Basic image type without upload state (added later in chat-input-bar)
6
+ export type BasicSelectedImage = {
7
+ uri: string;
8
+ type: string;
9
+ fileName?: string;
10
+ };
11
+
12
+ export const useImagePicker = () => {
13
+ const handleImagePicker = useCallback(async (): Promise<
14
+ BasicSelectedImage[]
15
+ > => {
16
+ try {
17
+ // Request permissions
18
+ const { status } =
19
+ await ImagePicker.requestMediaLibraryPermissionsAsync();
20
+ if (status !== 'granted') {
21
+ Alert.alert(
22
+ 'Permission Required',
23
+ 'Sorry, we need camera roll permissions to upload images.',
24
+ );
25
+ return [];
26
+ }
27
+
28
+ // Launch image picker with multiple selection enabled
29
+ const result = await ImagePicker.launchImageLibraryAsync({
30
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
31
+ allowsMultipleSelection: true,
32
+ allowsEditing: false,
33
+ aspect: [4, 3],
34
+ quality: 0.8,
35
+ });
36
+
37
+ if (!result.canceled && result.assets && result.assets.length > 0) {
38
+ return result.assets.map((asset) => ({
39
+ uri: asset.uri,
40
+ type: asset.mimeType || 'image/jpeg',
41
+ fileName: asset.fileName || undefined,
42
+ }));
43
+ }
44
+
45
+ return [];
46
+ } catch (error) {
47
+ console.error('Error picking images:', error);
48
+ Alert.alert('Error', 'Failed to pick images. Please try again.');
49
+ return [];
50
+ }
51
+ }, []);
52
+
53
+ const handleCamera =
54
+ useCallback(async (): Promise<BasicSelectedImage | null> => {
55
+ try {
56
+ // Request permissions
57
+ const { status } = await ImagePicker.requestCameraPermissionsAsync();
58
+ if (status !== 'granted') {
59
+ Alert.alert(
60
+ 'Permission Required',
61
+ 'Sorry, we need camera permissions to take photos.',
62
+ );
63
+ return null;
64
+ }
65
+
66
+ // Launch camera
67
+ const result = await ImagePicker.launchCameraAsync({
68
+ allowsEditing: true,
69
+ aspect: [4, 3],
70
+ quality: 0.8,
71
+ });
72
+
73
+ if (!result.canceled && result.assets[0]) {
74
+ const asset = result.assets[0];
75
+ return {
76
+ uri: asset.uri,
77
+ type: asset.mimeType || 'image/jpeg',
78
+ fileName: asset.fileName || undefined,
79
+ };
80
+ }
81
+
82
+ return null;
83
+ } catch (error) {
84
+ console.error('Error taking photo:', error);
85
+ Alert.alert('Error', 'Failed to take photo. Please try again.');
86
+ return null;
87
+ }
88
+ }, []);
89
+
90
+ const showImageSourceOptions = useCallback(
91
+ (onImagesSelected: (images: BasicSelectedImage[]) => void) => {
92
+ Alert.alert('Select Image', 'Choose an image source:', [
93
+ {
94
+ text: 'Camera',
95
+ onPress: async () => {
96
+ const image = await handleCamera();
97
+ if (image) {
98
+ onImagesSelected([image]);
99
+ }
100
+ },
101
+ },
102
+ {
103
+ text: 'Photo Library',
104
+ onPress: async () => {
105
+ const images = await handleImagePicker();
106
+ if (images.length > 0) {
107
+ onImagesSelected(images);
108
+ }
109
+ },
110
+ },
111
+ { text: 'Cancel', style: 'cancel' },
112
+ ]);
113
+ },
114
+ [handleCamera, handleImagePicker],
115
+ );
116
+
117
+ return {
118
+ handleImagePicker,
119
+ handleCamera,
120
+ showImageSourceOptions,
121
+ };
122
+ };