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,40 @@
1
+ export type Provider = 'claude' | 'openai' | 'gemini';
2
+
3
+ export type ModelCategory = 'free' | 'premium' | 'specialized';
4
+
5
+ export type ModelSpeed = 'fast' | 'medium' | 'slow';
6
+
7
+ export interface ModelConfig {
8
+ id: string;
9
+ name: string;
10
+ provider: Provider;
11
+ description: string;
12
+ category: ModelCategory;
13
+ capabilities: string[];
14
+ contextWindow?: number;
15
+ speed?: ModelSpeed;
16
+ released?: string;
17
+ tags?: string[];
18
+ }
19
+
20
+ export interface ProviderConfig {
21
+ id: Provider;
22
+ name: string;
23
+ displayName: string;
24
+ icon: React.ComponentType<any>;
25
+ defaultModel: string;
26
+ description: string;
27
+ website?: string;
28
+ color?: string;
29
+ }
30
+
31
+ export interface ModelOptions {
32
+ models: ModelConfig[];
33
+ providers: ProviderConfig[];
34
+ categories: Record<ModelCategory, ModelConfig[]>;
35
+ byProvider: Record<Provider, ModelConfig[]>;
36
+ premium: ModelConfig[];
37
+ free: ModelConfig[];
38
+ }
39
+
40
+ export type ModelId = string;
@@ -0,0 +1,238 @@
1
+ import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
2
+ import { Image } from 'react-native';
3
+
4
+ import { sharedApi } from '@/api-client/shared';
5
+ import { handleError } from '@/components/ui';
6
+ import { error as logError, info } from '@/core/logging';
7
+
8
+ // Configuration constants from Convex config
9
+ const CONFIG = {
10
+ MAX_IMAGE_DIMENSION: 1024,
11
+ COMPRESSION_QUALITY: 0.7,
12
+ MAX_FILE_SIZE_MB: 2,
13
+ MAX_FILE_SIZE_BYTES: 2 * 1024 * 1024, // 2MB
14
+ } as const;
15
+
16
+ const TARGET_ASPECT_RATIO = 16 / 9;
17
+
18
+ /**
19
+ * Service for uploading images to Convex File Storage
20
+ */
21
+ export class FileUploadService {
22
+ private generateUploadUrl: (args: any) => Promise<string>;
23
+
24
+ constructor(generateUploadUrlMutation: (args: any) => Promise<string>) {
25
+ this.generateUploadUrl = generateUploadUrlMutation;
26
+ }
27
+
28
+ /**
29
+ * Uploads an image file to Convex storage
30
+ */
31
+ async uploadImage(fileUri: string): Promise<string | null> {
32
+ try {
33
+ info('Starting image upload process', {
34
+ feature: 'chatbot.file_upload',
35
+ fileUri,
36
+ });
37
+
38
+ // Step 1: Compress and optimize the image
39
+ const optimizedUri = await this.optimizeImage(fileUri);
40
+ if (!optimizedUri) {
41
+ throw new Error('Failed to optimize image');
42
+ }
43
+
44
+ // Step 2: Validate file size
45
+ const response = await fetch(optimizedUri);
46
+ const blob = await response.blob();
47
+
48
+ if (blob.size > CONFIG.MAX_FILE_SIZE_BYTES) {
49
+ throw new Error(
50
+ `File size (${(blob.size / 1024 / 1024).toFixed(1)}MB) exceeds maximum allowed size of ${CONFIG.MAX_FILE_SIZE_MB}MB`,
51
+ );
52
+ }
53
+
54
+ info('Image optimized and validated', {
55
+ feature: 'chatbot.file_upload',
56
+ originalUri: fileUri,
57
+ optimizedUri,
58
+ fileSize: blob.size,
59
+ });
60
+
61
+ // Step 3: Get upload URL from Convex
62
+ const uploadUrl = await this.generateUploadUrl({});
63
+
64
+ // Step 4: Upload the file to Convex storage
65
+ const uploadResponse = await fetch(uploadUrl, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': blob.type },
68
+ body: blob,
69
+ });
70
+
71
+ if (!uploadResponse.ok) {
72
+ throw new Error(`Upload failed: ${uploadResponse.statusText}`);
73
+ }
74
+
75
+ // Step 5: Get the storage ID from the response
76
+ const { storageId } = await uploadResponse.json();
77
+
78
+ info('Image uploaded successfully', {
79
+ feature: 'chatbot.file_upload',
80
+ storageId,
81
+ fileSize: blob.size,
82
+ });
83
+
84
+ return storageId;
85
+ } catch (error) {
86
+ handleError(error, {
87
+ feature: 'chatbot.file_upload',
88
+ fallbackMessage: 'Failed to upload image',
89
+ });
90
+
91
+ return null;
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Optimizes an image by resizing and compressing it while preserving aspect ratio and alpha
97
+ */
98
+ private async optimizeImage(fileUri: string): Promise<string | null> {
99
+ try {
100
+ // Detect image format to preserve alpha channels
101
+ const response = await fetch(fileUri);
102
+ const blob = await response.blob();
103
+ const isPng = blob.type?.includes('png');
104
+ const isWebp = blob.type?.includes('webp');
105
+
106
+ const actions: Parameters<typeof manipulateAsync>[1][number][] = [];
107
+
108
+ try {
109
+ const { width, height } = await this.getImageDimensions(fileUri);
110
+ const cropRect = this.calculateCenterCrop(width, height);
111
+
112
+ if (cropRect) {
113
+ actions.push({ crop: cropRect });
114
+ }
115
+
116
+ const workingWidth = cropRect ? cropRect.width : width;
117
+ const workingHeight = cropRect ? cropRect.height : height;
118
+ const resize = this.calculateResizeDimensions(
119
+ workingWidth,
120
+ workingHeight,
121
+ );
122
+
123
+ if (resize) {
124
+ actions.push({ resize });
125
+ }
126
+ } catch (dimensionError) {
127
+ logError(
128
+ dimensionError instanceof Error
129
+ ? dimensionError
130
+ : new Error('Failed to read image dimensions'),
131
+ { feature: 'chatbot.file_upload', fileUri },
132
+ );
133
+
134
+ actions.push({
135
+ resize: {
136
+ width: CONFIG.MAX_IMAGE_DIMENSION,
137
+ },
138
+ });
139
+ }
140
+
141
+ const result = await manipulateAsync(fileUri, actions, {
142
+ compress: CONFIG.COMPRESSION_QUALITY,
143
+ format: isPng
144
+ ? SaveFormat.PNG
145
+ : isWebp
146
+ ? SaveFormat.WEBP
147
+ : SaveFormat.JPEG,
148
+ });
149
+
150
+ return result.uri;
151
+ } catch (error) {
152
+ logError(
153
+ error instanceof Error ? error : new Error('Failed to optimize image'),
154
+ { feature: 'chatbot.file_upload', fileUri },
155
+ );
156
+ return null;
157
+ }
158
+ }
159
+
160
+ private getImageDimensions(
161
+ uri: string,
162
+ ): Promise<{ width: number; height: number }> {
163
+ return new Promise((resolve, reject) => {
164
+ Image.getSize(
165
+ uri,
166
+ (width, height) => resolve({ width, height }),
167
+ (error) => reject(error),
168
+ );
169
+ });
170
+ }
171
+
172
+ private calculateCenterCrop(
173
+ width: number,
174
+ height: number,
175
+ ): {
176
+ originX: number;
177
+ originY: number;
178
+ width: number;
179
+ height: number;
180
+ } | null {
181
+ const aspectRatio = width / height;
182
+
183
+ if (Math.abs(aspectRatio - TARGET_ASPECT_RATIO) < 0.01) {
184
+ return null;
185
+ }
186
+
187
+ if (aspectRatio > TARGET_ASPECT_RATIO) {
188
+ const targetWidth = Math.round(height * TARGET_ASPECT_RATIO);
189
+ const originX = Math.round((width - targetWidth) / 2);
190
+
191
+ return {
192
+ originX,
193
+ originY: 0,
194
+ width: targetWidth,
195
+ height,
196
+ };
197
+ }
198
+
199
+ const targetHeight = Math.round(width / TARGET_ASPECT_RATIO);
200
+ const originY = Math.round((height - targetHeight) / 2);
201
+
202
+ return {
203
+ originX: 0,
204
+ originY,
205
+ width,
206
+ height: targetHeight,
207
+ };
208
+ }
209
+
210
+ private calculateResizeDimensions(
211
+ width: number,
212
+ height: number,
213
+ ): { width: number; height: number } | null {
214
+ const largestSide = Math.max(width, height);
215
+
216
+ if (largestSide <= CONFIG.MAX_IMAGE_DIMENSION) {
217
+ return null;
218
+ }
219
+
220
+ const scale = CONFIG.MAX_IMAGE_DIMENSION / largestSide;
221
+
222
+ return {
223
+ width: Math.round(width * scale),
224
+ height: Math.round(height * scale),
225
+ };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Hook to create a FileUploadService instance
231
+ *
232
+ * @returns FileUploadService instance with the mutation hook bound
233
+ */
234
+ export function useFileUploadService(): FileUploadService {
235
+ const generateUploadUrl = sharedApi.useGenerateUploadUrl();
236
+
237
+ return new FileUploadService(generateUploadUrl);
238
+ }
@@ -0,0 +1,180 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import { Alert } from 'react-native';
3
+
4
+ import { defaultCacheService } from '@/core/cache';
5
+ import type { AppMessage } from '@/features/chatbot/types';
6
+ import { translate } from '@/lib';
7
+
8
+ export type AppAttachment = {
9
+ storageId: Id<'_storage'>;
10
+ url?: string;
11
+ mimeType?: string;
12
+ type: 'image';
13
+ fileName?: string;
14
+ };
15
+
16
+ type MessageSubmissionOptions = {
17
+ currentInput: string;
18
+ conversationId: string;
19
+ attachmentsFromInputBar?: {
20
+ type: 'image';
21
+ storageId: string;
22
+ fileName?: string;
23
+ mimeType: string;
24
+ }[];
25
+ storeUserMessageMutation: (args: {
26
+ conversationId: Id<'conversations'>;
27
+ authorType: 'user';
28
+ text?: string;
29
+ attachments?: {
30
+ type: 'image';
31
+ storageId: Id<'_storage'>;
32
+ fileName?: string;
33
+ mimeType?: string;
34
+ }[];
35
+ }) => Promise<Id<'messages'>>;
36
+ };
37
+
38
+ type ClearChatOptions = {
39
+ conversationId: string;
40
+ clearConversationMutation: (args: {
41
+ conversationId: Id<'conversations'>;
42
+ }) => Promise<any>;
43
+ onCleared?: () => void;
44
+ };
45
+
46
+ /**
47
+ * Service for handling message submission and chat operations
48
+ */
49
+ export class MessageHandlerService {
50
+ /**
51
+ * Store a user message to the database before sending to AI
52
+ */
53
+ static async storeUserMessage(
54
+ options: MessageSubmissionOptions,
55
+ ): Promise<Id<'messages'> | null> {
56
+ const {
57
+ currentInput,
58
+ conversationId,
59
+ attachmentsFromInputBar,
60
+ storeUserMessageMutation,
61
+ } = options;
62
+
63
+ try {
64
+ console.log('[MessageHandlerService] Storing user message to DB...');
65
+ const messageId = await storeUserMessageMutation({
66
+ conversationId: conversationId as Id<'conversations'>,
67
+ authorType: 'user',
68
+ text: currentInput.trim() || undefined,
69
+ attachments: attachmentsFromInputBar?.map((att) => ({
70
+ type: 'image',
71
+ storageId: att.storageId as Id<'_storage'>,
72
+ fileName: att.fileName,
73
+ mimeType: att.mimeType,
74
+ })),
75
+ });
76
+ console.log('[MessageHandlerService] User message stored successfully.');
77
+ return messageId as Id<'messages'>;
78
+ } catch (dbError) {
79
+ console.error(
80
+ '[MessageHandlerService] Failed to store user message:',
81
+ dbError,
82
+ );
83
+ Alert.alert(translate('common.error'), translate('chatbot.send_failed'), [
84
+ {
85
+ text: translate('common.ok'),
86
+ onPress: () => {
87
+ // Reset the useChat hook by reloading the data
88
+ defaultCacheService.removeItem(`conversation_${conversationId}`);
89
+ },
90
+ },
91
+ ]);
92
+ return null;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Handle clearing a chat conversation
98
+ */
99
+ static clearChat(options: ClearChatOptions): void {
100
+ const { conversationId, clearConversationMutation } = options;
101
+
102
+ Alert.alert(
103
+ 'Clear Chat',
104
+ 'Are you sure you want to clear all messages? This action cannot be undone.',
105
+ [
106
+ {
107
+ text: 'Cancel',
108
+ style: 'cancel',
109
+ },
110
+ {
111
+ text: 'Clear',
112
+ style: 'destructive',
113
+ onPress: async () => {
114
+ try {
115
+ console.log(
116
+ '[MessageHandlerService] Clearing conversation:',
117
+ conversationId,
118
+ );
119
+ await clearConversationMutation({
120
+ conversationId: conversationId as Id<'conversations'>,
121
+ });
122
+ console.log(
123
+ '[MessageHandlerService] Conversation cleared successfully',
124
+ );
125
+
126
+ // Reset the useChat hook by reloading the data
127
+ defaultCacheService.removeItem(`conversation_${conversationId}`);
128
+ options.onCleared?.();
129
+ } catch (error) {
130
+ console.error(
131
+ '[MessageHandlerService] Failed to clear conversation:',
132
+ error,
133
+ );
134
+ Alert.alert('Error', 'Failed to clear chat. Please try again.');
135
+ }
136
+ },
137
+ },
138
+ ],
139
+ );
140
+ }
141
+
142
+ /**
143
+ * Transform display messages with stable attachment mapping
144
+ */
145
+ static transformDisplayMessages(
146
+ liveMessages: AppMessage[],
147
+ persistedMessages: AppMessage[],
148
+ lastUserAttachments?: AppAttachment[],
149
+ attachmentCache?: Record<string, AppAttachment[]>,
150
+ ): AppMessage[] {
151
+ const persisted = new Map(persistedMessages.map((m) => [m.id, m]));
152
+ const lastUserIdx = [...liveMessages]
153
+ .map((m, i) => ({ m, i }))
154
+ .reverse()
155
+ .find((x) => x.m.role === 'user')?.i;
156
+
157
+ return liveMessages.map((m, i) => {
158
+ const fromDb = persisted.get(m.id);
159
+ const isMostRecentUser = m.role === 'user' && i === lastUserIdx;
160
+ // Attachment precedence:
161
+ // 1) Cached attachments for this live message ID (persist across renders)
162
+ // 2) Most recent user's optimistic attachments (for immediate display)
163
+ // 3) Attachments from persisted DB messages when IDs align
164
+ const fromCache = attachmentCache?.[m.id];
165
+ const attachments =
166
+ fromCache && fromCache.length
167
+ ? fromCache
168
+ : ((isMostRecentUser && lastUserAttachments?.length
169
+ ? lastUserAttachments
170
+ : fromDb?.attachments) ?? undefined);
171
+
172
+ return {
173
+ ...m,
174
+ attachments,
175
+ toolCalls: m.toolCalls ?? fromDb?.toolCalls,
176
+ metadata: m.metadata ?? fromDb?.metadata,
177
+ };
178
+ });
179
+ }
180
+ }
@@ -0,0 +1,60 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import type { Message } from 'ai';
3
+
4
+ export type LocalMessageContentPart =
5
+ | { type: 'text'; text: string }
6
+ | { type: 'image'; image: string | URL };
7
+
8
+ export type ConvexMessageAttachment = {
9
+ type: 'image';
10
+ storageId: Id<'_storage'>;
11
+ url?: string;
12
+ fileName?: string;
13
+ mimeType?: string;
14
+ };
15
+
16
+ export type ToolCallRecord = {
17
+ toolName: string;
18
+ args: unknown;
19
+ result?: unknown;
20
+ };
21
+
22
+ export type MessageMetadata = {
23
+ streamId?: string;
24
+ status?: string;
25
+ usage?: {
26
+ promptTokens?: number;
27
+ completionTokens?: number;
28
+ totalTokens?: number;
29
+ };
30
+ };
31
+
32
+ export type AgentStatus =
33
+ | {
34
+ type: 'thinking';
35
+ label?: string | null;
36
+ startedAt: number;
37
+ }
38
+ | {
39
+ type: 'tool';
40
+ toolName: string;
41
+ label?: string | null;
42
+ startedAt: number;
43
+ };
44
+
45
+ export type AppMessage = Omit<Message, 'content'> & {
46
+ content: string | LocalMessageContentPart[];
47
+ attachments?: ConvexMessageAttachment[];
48
+ toolCalls?: ToolCallRecord[];
49
+ metadata?: MessageMetadata;
50
+ };
51
+
52
+ export type ConversationMessageFromDB = {
53
+ _id: string;
54
+ _creationTime: number;
55
+ authorType: 'user' | 'bot';
56
+ text?: string;
57
+ attachments?: ConvexMessageAttachment[];
58
+ toolCalls?: ToolCallRecord[];
59
+ metadata?: MessageMetadata;
60
+ };
@@ -0,0 +1,91 @@
1
+ import type { TelemetryMode } from '@/core/config';
2
+ import { getChatTelemetryMode } from '@/core/config/telemetry';
3
+
4
+ const DEFAULT_SAMPLE_PROBABILITY = 0.1;
5
+
6
+ type TimelineEntry = {
7
+ label: string;
8
+ msFromStart: number;
9
+ data?: Record<string, unknown>;
10
+ };
11
+
12
+ export type ChatTelemetryTracker = {
13
+ readonly event: string;
14
+ mark: (label: string, data?: Record<string, unknown>) => void;
15
+ finalize: (status: 'ok' | 'error', extra?: Record<string, unknown>) => void;
16
+ };
17
+
18
+ function shouldTrack(mode: TelemetryMode, probability: number): boolean {
19
+ if (mode === 'off') {
20
+ return false;
21
+ }
22
+ if (mode === 'debug') {
23
+ return true;
24
+ }
25
+
26
+ const clampedProbability = Math.min(Math.max(probability, 0), 1);
27
+ return Math.random() < clampedProbability;
28
+ }
29
+
30
+ export function createChatTelemetryTracker(
31
+ event: string,
32
+ baseContext: Record<string, unknown>,
33
+ sampleProbability = DEFAULT_SAMPLE_PROBABILITY,
34
+ ): ChatTelemetryTracker | null {
35
+ const mode = getChatTelemetryMode();
36
+ if (!shouldTrack(mode, sampleProbability)) {
37
+ return null;
38
+ }
39
+
40
+ const start = Date.now();
41
+ const timeline: TimelineEntry[] = [{ label: 'start', msFromStart: 0 }];
42
+ let finalized = false;
43
+
44
+ return {
45
+ event,
46
+ mark(label, data) {
47
+ if (finalized) {
48
+ return;
49
+ }
50
+ const elapsed = Date.now() - start;
51
+ timeline.push({
52
+ label,
53
+ msFromStart: elapsed,
54
+ data,
55
+ });
56
+ },
57
+ finalize(status, extra) {
58
+ if (finalized) {
59
+ return;
60
+ }
61
+ finalized = true;
62
+ const totalMs = Date.now() - start;
63
+ console.log('[chat][telemetry]', {
64
+ event,
65
+ mode,
66
+ status,
67
+ totalMs,
68
+ ...baseContext,
69
+ ...(extra ?? {}),
70
+ timeline,
71
+ });
72
+ },
73
+ };
74
+ }
75
+
76
+ export function logChatTelemetryEvent(
77
+ event: string,
78
+ payload: Record<string, unknown>,
79
+ sampleProbability = DEFAULT_SAMPLE_PROBABILITY,
80
+ ) {
81
+ const mode = getChatTelemetryMode();
82
+ if (!shouldTrack(mode, sampleProbability)) {
83
+ return;
84
+ }
85
+
86
+ console.log('[chat][telemetry]', {
87
+ event,
88
+ mode,
89
+ ...payload,
90
+ });
91
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "chatbot",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered chat assistant",
5
+ "copy": [
6
+ {
7
+ "from": "apps/native/src/app/chatbot",
8
+ "to": "apps/native/src/app/(root)/(protected)/chatbot"
9
+ },
10
+ {
11
+ "from": "apps/native/src/features/chatbot",
12
+ "to": "apps/native/src/features/chatbot"
13
+ }
14
+ ],
15
+ "nav": {
16
+ "href": "/(root)/(protected)/chatbot",
17
+ "label": "AI Chatbot",
18
+ "icon": "💬",
19
+ "color": "#F43F5E"
20
+ },
21
+ "target": "native"
22
+ }
Binary file
@@ -0,0 +1,3 @@
1
+ import ImageGalleryScreen from '@/features/image-generator/app/gallery';
2
+
3
+ export default ImageGalleryScreen;
@@ -0,0 +1,3 @@
1
+ import ImageGeneratorScreen from '@/features/image-generator/app';
2
+
3
+ export default ImageGeneratorScreen;
@@ -0,0 +1,25 @@
1
+ import { Stack } from 'expo-router';
2
+ import { translate } from '@/lib';
3
+
4
+ export default function ImageGeneratorLayout() {
5
+ return (
6
+ <Stack
7
+ screenOptions={{
8
+ headerShown: true,
9
+ }}
10
+ >
11
+ <Stack.Screen
12
+ name="index"
13
+ options={{
14
+ title: translate('image_generator.title'),
15
+ }}
16
+ />
17
+ <Stack.Screen
18
+ name="gallery"
19
+ options={{
20
+ title: `${translate('image_generator.gallery_title')} (0)`,
21
+ }}
22
+ />
23
+ </Stack>
24
+ );
25
+ }