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,98 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+
3
+ import { defaultCacheService } from '@/core/cache';
4
+
5
+ export type SavedImageMetadata = {
6
+ id: string;
7
+ prompt: string;
8
+ provider: string;
9
+ model: string;
10
+ savedAt: number;
11
+ localUri: string;
12
+ remoteImageId?: Id<'generatedImages'>;
13
+ storageId?: Id<'_storage'>;
14
+ };
15
+
16
+ const GALLERY_CACHE_KEY = 'image_gallery_v1';
17
+
18
+ /**
19
+ * Service for managing the local image gallery with MMKV persistence
20
+ */
21
+ export class ImageGalleryService {
22
+ /**
23
+ * Get all saved images from the gallery
24
+ */
25
+ static async getAllImages(): Promise<SavedImageMetadata[]> {
26
+ try {
27
+ const images =
28
+ await defaultCacheService.getItem<SavedImageMetadata[]>(
29
+ GALLERY_CACHE_KEY,
30
+ );
31
+ return images || [];
32
+ } catch (error) {
33
+ console.error('Failed to load gallery images:', error);
34
+ return [];
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Add a new image to the gallery
40
+ */
41
+ static async addImage(
42
+ metadata: Omit<SavedImageMetadata, 'id' | 'savedAt'>,
43
+ ): Promise<void> {
44
+ try {
45
+ const existingImages = await ImageGalleryService.getAllImages();
46
+ const newImage: SavedImageMetadata = {
47
+ ...metadata,
48
+ id: `img_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
49
+ savedAt: Date.now(),
50
+ };
51
+
52
+ const updatedImages = [newImage, ...existingImages];
53
+ await defaultCacheService.setItem(GALLERY_CACHE_KEY, updatedImages);
54
+ } catch (error) {
55
+ console.error('Failed to add image to gallery:', error);
56
+ throw error;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Remove an image from the gallery
62
+ */
63
+ static async removeImage(imageId: string): Promise<void> {
64
+ try {
65
+ const existingImages = await ImageGalleryService.getAllImages();
66
+ const updatedImages = existingImages.filter((img) => img.id !== imageId);
67
+ await defaultCacheService.setItem(GALLERY_CACHE_KEY, updatedImages);
68
+ } catch (error) {
69
+ console.error('Failed to remove image from gallery:', error);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Clear all images from the gallery
76
+ */
77
+ static async clearGallery(): Promise<void> {
78
+ try {
79
+ await defaultCacheService.setItem(GALLERY_CACHE_KEY, []);
80
+ } catch (error) {
81
+ console.error('Failed to clear gallery:', error);
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Get the total number of saved images
88
+ */
89
+ static async getImageCount(): Promise<number> {
90
+ try {
91
+ const images = await ImageGalleryService.getAllImages();
92
+ return images.length;
93
+ } catch (error) {
94
+ console.error('Failed to get image count:', error);
95
+ return 0;
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,121 @@
1
+ import type { Id } from '@vibefast/backend/_generated/dataModel';
2
+ import { File, Paths } from 'expo-file-system';
3
+ import * as MediaLibrary from 'expo-media-library';
4
+ import { Alert, Platform } from 'react-native';
5
+
6
+ import { ImageGalleryService } from './image-gallery-service';
7
+
8
+ export type ImageSaveMetadata = {
9
+ prompt: string;
10
+ provider: string;
11
+ model: string;
12
+ remoteImageId?: Id<'generatedImages'>;
13
+ storageId?: Id<'_storage'>;
14
+ };
15
+
16
+ /**
17
+ * Service for handling image saving operations to the device gallery and local storage
18
+ */
19
+ export class ImageSaveService {
20
+ /**
21
+ * Save a base64 image data URI to the device gallery and local gallery
22
+ * @param imageDataUri - The data URI containing the image (e.g., 'data:image/png;base64,iVBORw0KGgo...')
23
+ * @param metadata - Optional metadata about the image generation
24
+ * @returns Promise that resolves when save is complete
25
+ */
26
+ static async saveImageToGallery(
27
+ imageDataUri: string,
28
+ metadata?: ImageSaveMetadata,
29
+ ): Promise<void> {
30
+ try {
31
+ // Check if running on web - FileSystem is not available
32
+ if (Platform.OS === 'web') {
33
+ // On web, just save to local gallery
34
+ if (metadata) {
35
+ await ImageGalleryService.addImage({
36
+ prompt: metadata.prompt,
37
+ provider: metadata.provider,
38
+ model: metadata.model,
39
+ localUri: imageDataUri,
40
+ remoteImageId: metadata.remoteImageId,
41
+ storageId: metadata.storageId,
42
+ });
43
+ Alert.alert('Success!', 'Image saved to your gallery.');
44
+ }
45
+ return;
46
+ }
47
+
48
+ // Request permissions (native only)
49
+ const { status } = await MediaLibrary.requestPermissionsAsync();
50
+ if (status !== 'granted') {
51
+ Alert.alert(
52
+ 'Permission Required',
53
+ 'We need permission to save photos to your gallery.',
54
+ );
55
+ return;
56
+ }
57
+
58
+ // Extract base64 data from data URI (supports multiple formats)
59
+ const match = imageDataUri.match(/^data:image\/(\w+);base64,(.+)$/);
60
+ if (!match) {
61
+ throw new Error('Invalid image data format');
62
+ }
63
+ const [, imageFormat, base64Code] = match;
64
+ const fileExtension = imageFormat === 'jpeg' ? 'jpg' : imageFormat;
65
+
66
+ // Create temporary file using new File API
67
+ const filename = `generated-image-${Date.now()}.${fileExtension}`;
68
+ const file = new File(Paths.cache, filename);
69
+
70
+ // Create and write the file
71
+ file.create();
72
+ await file.write(base64Code, { encoding: 'base64' });
73
+
74
+ // Save to device gallery
75
+ const asset = await MediaLibrary.createAssetAsync(file.uri);
76
+ await MediaLibrary.createAlbumAsync('MyAppImages', asset, false);
77
+
78
+ // Save to local gallery if metadata is provided
79
+ if (metadata) {
80
+ await ImageGalleryService.addImage({
81
+ prompt: metadata.prompt,
82
+ provider: metadata.provider,
83
+ model: metadata.model,
84
+ localUri: imageDataUri,
85
+ remoteImageId: metadata.remoteImageId,
86
+ storageId: metadata.storageId,
87
+ });
88
+ }
89
+
90
+ Alert.alert('Success!', 'Image saved to your gallery.');
91
+ } catch (error) {
92
+ console.error('Failed to save image:', error);
93
+ Alert.alert('Error', 'Could not save the image.');
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Save image only to local gallery (without device gallery)
99
+ * @param imageDataUri - The data URI containing the image
100
+ * @param metadata - Metadata about the image generation
101
+ * @returns Promise that resolves when save is complete
102
+ */
103
+ static async saveToLocalGallery(
104
+ imageDataUri: string,
105
+ metadata: ImageSaveMetadata,
106
+ ): Promise<void> {
107
+ try {
108
+ await ImageGalleryService.addImage({
109
+ prompt: metadata.prompt,
110
+ provider: metadata.provider,
111
+ model: metadata.model,
112
+ localUri: imageDataUri,
113
+ remoteImageId: metadata.remoteImageId,
114
+ storageId: metadata.storageId,
115
+ });
116
+ } catch (error) {
117
+ console.error('Failed to save to local gallery:', error);
118
+ throw error;
119
+ }
120
+ }
121
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "image-generator",
3
+ "version": "1.0.0",
4
+ "description": "AI-powered image generation",
5
+ "copy": [
6
+ {
7
+ "from": "apps/native/src/app/image-generator",
8
+ "to": "apps/native/src/app/(root)/(protected)/image-generator"
9
+ },
10
+ {
11
+ "from": "apps/native/src/features/image-generator",
12
+ "to": "apps/native/src/features/image-generator"
13
+ }
14
+ ],
15
+ "nav": {
16
+ "href": "/(root)/(protected)/image-generator",
17
+ "label": "Image Generator",
18
+ "icon": "🎨",
19
+ "color": "#A855F7"
20
+ },
21
+ "target": "native"
22
+ }
@@ -0,0 +1,47 @@
1
+ import Quiz from '@/features/quiz';
2
+ import { QuestionMode } from '@/features/quiz/config';
3
+
4
+ const questions = [
5
+ {
6
+ text: 'What is your favorite color?',
7
+ mode: QuestionMode.AutoAdvance,
8
+ options: [
9
+ { id: 'red', text: 'Red', note: 'Red is the color of passion.' },
10
+ { id: 'blue', text: 'Blue', note: 'Blue is the color of calm.' },
11
+ { id: 'green', text: 'Green', note: 'Green is the color of nature.' },
12
+ ],
13
+ },
14
+ {
15
+ text: 'What is your favorite animal?',
16
+ mode: QuestionMode.ManualAdvance,
17
+ options: [
18
+ { id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
19
+ { id: 'cat', text: 'Cat', note: 'Cats are independent.' },
20
+ { id: 'bird', text: 'Bird', note: 'Birds can fly.' },
21
+ ],
22
+ },
23
+ {
24
+ text: 'What is your favorite animal?',
25
+ mode: QuestionMode.ManualAdvance,
26
+ options: [
27
+ { id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
28
+ { id: 'cat', text: 'Cat', note: 'Cats are independent.' },
29
+ { id: 'bird', text: 'Bird', note: 'Birds can fly.' },
30
+ ],
31
+ },
32
+ {
33
+ text: 'What is your favorite animal?',
34
+ mode: QuestionMode.ManualAdvance,
35
+ options: [
36
+ { id: 'dog', text: 'Dog', note: 'Dogs are loyal.' },
37
+ { id: 'cat', text: 'Cat', note: 'Cats are independent.' },
38
+ { id: 'bird', text: 'Bird', note: 'Birds can fly.' },
39
+ ],
40
+ },
41
+ ];
42
+
43
+ const QuizScreen = () => {
44
+ return <Quiz questions={questions} />;
45
+ };
46
+
47
+ export default QuizScreen;
@@ -0,0 +1,67 @@
1
+ import { MotiView } from 'moti';
2
+ import React from 'react';
3
+ import { TouchableOpacity } from 'react-native';
4
+ import Animated, { Layout } from 'react-native-reanimated';
5
+
6
+ import { Text } from '@/components/ui';
7
+
8
+ import { QuestionMode, quizConfig } from '../config';
9
+
10
+ const Question = ({ question, onOptionSelect, selectedOption }) => {
11
+ return (
12
+ <MotiView
13
+ from={{ opacity: 0, translateY: 50 }}
14
+ animate={{ opacity: 1, translateY: 0 }}
15
+ exit={{ opacity: 0, translateY: -50 }}
16
+ transition={{
17
+ type: 'timing',
18
+ duration: quizConfig.animation.duration,
19
+ }}
20
+ className="flex-1"
21
+ >
22
+ <Text className="my-8 text-center text-2xl font-bold">
23
+ {question.text}
24
+ </Text>
25
+ <Animated.View
26
+ className="flex-1 justify-center gap-y-4"
27
+ layout={Layout.springify().duration(quizConfig.animation.duration)}
28
+ >
29
+ {question.options.map((option) => (
30
+ <TouchableOpacity
31
+ key={option.id}
32
+ onPress={() => onOptionSelect(option)}
33
+ >
34
+ <MotiView
35
+ from={{ scale: 1 }}
36
+ animate={{ scale: selectedOption === option ? 1.02 : 1 }}
37
+ transition={{
38
+ type: 'timing',
39
+ duration: quizConfig.animation.duration,
40
+ }}
41
+ className={`rounded-lg border p-4 ${selectedOption === option ? 'border-primary-500 bg-primary-500/20 dark:border-primary-500 dark:bg-primary-900/20' : 'border-neutral-300'}`}
42
+ >
43
+ <Text className="text-lg">{option.text}</Text>
44
+ {selectedOption === option &&
45
+ question.mode === QuestionMode.ManualAdvance && (
46
+ <MotiView
47
+ from={{ opacity: 0, height: 0 }}
48
+ animate={{ opacity: 1, height: 'auto' }}
49
+ transition={{
50
+ type: 'timing',
51
+ duration: quizConfig.animation.duration,
52
+ }}
53
+ >
54
+ <Text className="mt-2 text-sm text-neutral-600">
55
+ {option.note}
56
+ </Text>
57
+ </MotiView>
58
+ )}
59
+ </MotiView>
60
+ </TouchableOpacity>
61
+ ))}
62
+ </Animated.View>
63
+ </MotiView>
64
+ );
65
+ };
66
+
67
+ export default Question;
@@ -0,0 +1,11 @@
1
+ export enum QuestionMode {
2
+ AutoAdvance = 'auto-advance',
3
+ ManualAdvance = 'manual-advance',
4
+ }
5
+
6
+ export const quizConfig = {
7
+ showContinueButton: true, // This can be configured based on the desired behavior
8
+ animation: {
9
+ duration: 300,
10
+ },
11
+ };
@@ -0,0 +1,133 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import { useRouter } from 'expo-router';
3
+ import { AnimatePresence, MotiView } from 'moti';
4
+ import { useState } from 'react';
5
+ import { View } from 'react-native';
6
+ import { SafeAreaView } from 'react-native-safe-area-context';
7
+
8
+ import { Button, Text } from '@/components/ui';
9
+
10
+ import Question from './components/question';
11
+ import { QuestionMode, quizConfig } from './config';
12
+
13
+ const ProgressBar = ({ progress, handlePreviousQuestion }) => (
14
+ <View className="mr-3 flex flex-row items-center">
15
+ <Button
16
+ size="icon"
17
+ onPress={handlePreviousQuestion}
18
+ className="mb-5 mr-3 mt-0 shrink-0 !bg-transparent"
19
+ >
20
+ <Ionicons name="arrow-back" size={24} color="#666" />
21
+ </Button>
22
+ <View className="mb-5 h-3 flex-1 overflow-hidden rounded-xl bg-neutral-200 dark:bg-neutral-800">
23
+ <MotiView
24
+ from={{ width: '0%' }}
25
+ animate={{ width: `${progress * 100}%` }}
26
+ transition={{
27
+ type: 'timing',
28
+ duration: quizConfig.animation.duration,
29
+ }}
30
+ className="h-full bg-primary-500"
31
+ />
32
+ </View>
33
+ </View>
34
+ );
35
+
36
+ const Quiz = ({ questions }) => {
37
+ const router = useRouter();
38
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
39
+ const [selectedOption, setSelectedOption] = useState(null);
40
+ const [showContinue, setShowContinue] = useState(false);
41
+
42
+ const handleOptionSelect = (option) => {
43
+ setSelectedOption(option);
44
+ const currentQuestionMode = questions[currentQuestionIndex].mode;
45
+ if (currentQuestionMode === QuestionMode.AutoAdvance) {
46
+ setTimeout(() => {
47
+ handleNextQuestion();
48
+ }, quizConfig.animation.duration + 200);
49
+ } else {
50
+ setShowContinue(true);
51
+ }
52
+ };
53
+
54
+ const handleNextQuestion = () => {
55
+ setSelectedOption(null);
56
+ setShowContinue(false);
57
+ setCurrentQuestionIndex(currentQuestionIndex + 1);
58
+ };
59
+
60
+ const handlePreviousQuestion = () => {
61
+ if (currentQuestionIndex === 0) {
62
+ router.back();
63
+ return;
64
+ }
65
+ setSelectedOption(null);
66
+ setShowContinue(false);
67
+ setCurrentQuestionIndex(currentQuestionIndex - 1);
68
+ };
69
+
70
+ const progress = currentQuestionIndex / questions.length;
71
+
72
+ if (currentQuestionIndex >= questions.length) {
73
+ return (
74
+ <SafeAreaView className="flex-1 p-5">
75
+ <Text className="mb-5 text-xl font-bold">Quiz Complete!</Text>
76
+ </SafeAreaView>
77
+ );
78
+ }
79
+
80
+ const currentQuestion = questions[currentQuestionIndex];
81
+
82
+ return (
83
+ <SafeAreaView className="flex-1 p-5">
84
+ <ProgressBar
85
+ progress={progress}
86
+ handlePreviousQuestion={handlePreviousQuestion}
87
+ />
88
+ <AnimatePresence exitBeforeEnter>
89
+ <Question
90
+ key={currentQuestionIndex}
91
+ question={currentQuestion}
92
+ onOptionSelect={handleOptionSelect}
93
+ selectedOption={selectedOption}
94
+ />
95
+ </AnimatePresence>
96
+
97
+ {quizConfig.showContinueButton &&
98
+ showContinue &&
99
+ currentQuestion.mode === QuestionMode.ManualAdvance && (
100
+ <MotiView
101
+ from={{ opacity: 0, translateY: 50 }}
102
+ animate={{ opacity: 1, translateY: 0 }}
103
+ transition={{
104
+ type: 'timing',
105
+ duration: quizConfig.animation.duration,
106
+ }}
107
+ className="mt-8"
108
+ >
109
+ <View className="flex-row gap-3">
110
+ {/* {currentQuestionIndex > 0 && (
111
+ <Button
112
+ variant="outline"
113
+ onPress={handlePreviousQuestion}
114
+ className="flex-shrink-0"
115
+ >
116
+ <Ionicons name="arrow-back" size={20} color="#666" />
117
+ </Button>
118
+ )} */}
119
+ <Button
120
+ variant="secondary"
121
+ onPress={handleNextQuestion}
122
+ label="Continue"
123
+ className="flex-1"
124
+ textClassName="text-white"
125
+ />
126
+ </View>
127
+ </MotiView>
128
+ )}
129
+ </SafeAreaView>
130
+ );
131
+ };
132
+
133
+ export default Quiz;
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "quiz",
3
+ "version": "1.0.0",
4
+ "description": "Interactive quiz feature",
5
+ "copy": [
6
+ {
7
+ "from": "apps/native/src/app/quiz",
8
+ "to": "apps/native/src/app/(root)/(protected)/quiz"
9
+ },
10
+ {
11
+ "from": "apps/native/src/features/quiz",
12
+ "to": "apps/native/src/features/quiz"
13
+ }
14
+ ],
15
+ "nav": {
16
+ "href": "/(root)/(protected)/quiz",
17
+ "label": "Quiz",
18
+ "icon": "📱",
19
+ "color": "#F97316"
20
+ },
21
+ "target": "native"
22
+ }
Binary file
@@ -0,0 +1 @@
1
+ export { default } from '@/features/tracker-app/app';
@@ -0,0 +1,108 @@
1
+ import { Ionicons, MaterialIcons } from '@expo/vector-icons';
2
+ import React from 'react';
3
+ import { ScrollView, View } from 'react-native';
4
+ import { SafeAreaView } from 'react-native-safe-area-context';
5
+
6
+ import { Text } from '@/components/ui';
7
+ import { useThemeConfig } from '@/lib/use-theme-config';
8
+
9
+ import CalorieCard from '../components/calorie-card';
10
+ import MacroCard from '../components/macro-card';
11
+ import PromoBanner from '../components/promo-banner';
12
+ import RecentlyLogged from '../components/recently-logged';
13
+ import WeekCalendar from '../components/week-calendar';
14
+
15
+ export default function HomeScreen() {
16
+ const theme = useThemeConfig();
17
+
18
+ return (
19
+ <SafeAreaView
20
+ className="flex-1"
21
+ style={{ backgroundColor: theme.colors.background }}
22
+ >
23
+ <ScrollView className="flex-1 px-5" showsVerticalScrollIndicator={false}>
24
+ <View className="flex-row items-center justify-between pb-6 pt-4">
25
+ <View className="flex-row items-center">
26
+ <MaterialIcons
27
+ name="restaurant-menu"
28
+ size={28}
29
+ color={theme.colors.foreground}
30
+ />
31
+ <Text
32
+ className="ml-2 text-2xl font-extrabold"
33
+ style={{ color: theme.colors.foreground }}
34
+ >
35
+ CalAI
36
+ </Text>
37
+ </View>
38
+ <View
39
+ className="flex-row items-center rounded-2xl px-3 py-1.5"
40
+ style={{ backgroundColor: theme.colors.card }}
41
+ >
42
+ <Ionicons name="flame" size={16} color={theme.colors.warning} />
43
+ <Text
44
+ className="ml-1 text-base font-bold"
45
+ style={{ color: theme.colors.foreground }}
46
+ >
47
+ 1
48
+ </Text>
49
+ </View>
50
+ </View>
51
+
52
+ <WeekCalendar />
53
+
54
+ <CalorieCard calories={1852} progress={25} />
55
+
56
+ <View className="mb-2 flex-row gap-3">
57
+ <MacroCard
58
+ value={122}
59
+ unit="g"
60
+ label="Protein left"
61
+ progress={35}
62
+ color={theme.colors.destructive}
63
+ icon={
64
+ <MaterialIcons
65
+ name="restaurant-menu"
66
+ size={20}
67
+ color={theme.colors.destructive}
68
+ />
69
+ }
70
+ delay={600}
71
+ />
72
+ <MacroCard
73
+ value={222}
74
+ unit="g"
75
+ label="Carbs left"
76
+ progress={60}
77
+ color={theme.colors.warning}
78
+ icon={
79
+ <Ionicons
80
+ name="nutrition"
81
+ size={20}
82
+ color={theme.colors.warning}
83
+ />
84
+ }
85
+ delay={800}
86
+ />
87
+ <MacroCard
88
+ value={49}
89
+ unit="g"
90
+ label="Fat left"
91
+ progress={45}
92
+ color={theme.colors.primary}
93
+ icon={
94
+ <Ionicons name="water" size={20} color={theme.colors.primary} />
95
+ }
96
+ delay={1000}
97
+ />
98
+ </View>
99
+
100
+ <PromoBanner />
101
+
102
+ <RecentlyLogged />
103
+
104
+ <View className="h-25" />
105
+ </ScrollView>
106
+ </SafeAreaView>
107
+ );
108
+ }