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,217 @@
1
+ import { Stack, useRouter } from 'expo-router';
2
+ import React from 'react';
3
+ import {
4
+ ActivityIndicator,
5
+ Alert,
6
+ Platform,
7
+ ScrollView,
8
+ View,
9
+ } from 'react-native';
10
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
11
+
12
+ import {
13
+ Button,
14
+ FocusAwareStatusBar,
15
+ Gallery,
16
+ Pressable,
17
+ Text,
18
+ } from '@/components/ui';
19
+ import type { GalleryItem } from '@/components/ui/gallery';
20
+ import { ImageDetailModal } from '@/features/image-generator/components/image-detail-modal';
21
+ import { useImageGallery } from '@/features/image-generator/hooks/use-image-gallery';
22
+ import type { SavedImageMetadata } from '@/features/image-generator/services/image-gallery-service';
23
+ import { translate } from '@/lib';
24
+ import { useScreenOptions } from '@/lib/hooks/use-navigation-options';
25
+ import { Trash2 } from '@/components/ui/icons';
26
+
27
+ // Set static options to prevent initial flash
28
+ export const unstable_settings = {
29
+ initialRouteName: 'gallery',
30
+ };
31
+
32
+ /**
33
+ * Screen component for displaying the local image gallery
34
+ */
35
+ export default function ImageGalleryScreen() {
36
+ const router = useRouter();
37
+ const insets = useSafeAreaInsets();
38
+ const { images, isLoading, error, removeImage, clearGallery } =
39
+ useImageGallery();
40
+
41
+ const [selectedImage, setSelectedImage] =
42
+ React.useState<SavedImageMetadata | null>(null);
43
+
44
+ const screenOptions = useScreenOptions(
45
+ {
46
+ title: `${translate('image_generator.gallery_title')} (${images.length})`,
47
+ headerRight: () =>
48
+ images.length > 0 ? (
49
+ <Pressable
50
+ onPress={handleClearGallery}
51
+ className="pl-2"
52
+ style={{ marginRight: 8 }}
53
+ testID="clear-gallery-button"
54
+ accessibilityLabel="Clear all images from gallery"
55
+ accessibilityRole="button"
56
+ >
57
+ <Text className="font-medium text-danger-500">
58
+ <Trash2 color="#ef4444" />
59
+ </Text>
60
+ </Pressable>
61
+ ) : null,
62
+ },
63
+ 'scrollable',
64
+ );
65
+
66
+ const handleImagePress = (item: GalleryItem) => {
67
+ const image = images.find((img) => img.id === item.id);
68
+ if (image) {
69
+ setSelectedImage(image);
70
+ }
71
+ };
72
+
73
+ const handleImageLongPress = (item: GalleryItem) => {
74
+ const image = images.find((img) => img.id === item.id);
75
+ if (!image) return;
76
+
77
+ Alert.alert(
78
+ translate('image_generator.image_options'),
79
+ `${translate('image_generator.image_options_description')}\n\nPrompt: ${image.prompt.substring(0, 100)}...\n\nModel:${image.model}`,
80
+ [
81
+ { text: translate('common.cancel'), style: 'cancel' },
82
+ {
83
+ text: translate('common.delete'),
84
+ style: 'destructive',
85
+ onPress: () => {
86
+ Alert.alert(
87
+ translate('image_detail.delete_image'),
88
+ translate('image_detail.delete_image_confirmation'),
89
+ [
90
+ { text: translate('common.cancel'), style: 'cancel' },
91
+ {
92
+ text: translate('common.delete'),
93
+ style: 'destructive',
94
+ onPress: () => removeImage(image.id),
95
+ },
96
+ ],
97
+ );
98
+ },
99
+ },
100
+ ],
101
+ );
102
+ };
103
+
104
+ // Convert SavedImageMetadata to GalleryItem format
105
+ const galleryItems: GalleryItem[] = images.map((img) => ({
106
+ id: img.id,
107
+ uri: img.localUri,
108
+ title: img.prompt,
109
+ }));
110
+
111
+ const handleCloseModal = () => {
112
+ setSelectedImage(null);
113
+ };
114
+
115
+ const handleDeleteFromModal = (imageId: string) => {
116
+ removeImage(imageId);
117
+ };
118
+
119
+ const handleClearGallery = () => {
120
+ if (images.length === 0) return;
121
+
122
+ Alert.alert(
123
+ translate('image_generator.clear_gallery'),
124
+ translate('image_generator.clear_gallery_confirmation'),
125
+ [
126
+ { text: translate('common.cancel'), style: 'cancel' },
127
+ {
128
+ text: translate('image_generator.clear_all'),
129
+ style: 'destructive',
130
+ onPress: clearGallery,
131
+ },
132
+ ],
133
+ );
134
+ };
135
+
136
+ if (isLoading) {
137
+ return (
138
+ <View className="flex-1 items-center justify-center bg-white dark:bg-neutral-950">
139
+ <FocusAwareStatusBar />
140
+ <ActivityIndicator size="large" />
141
+ <Text className="mt-4 text-neutral-900 dark:text-neutral-100">
142
+ {translate('common.loading')}...
143
+ </Text>
144
+ </View>
145
+ );
146
+ }
147
+
148
+ if (error) {
149
+ return (
150
+ <View className="flex-1 items-center justify-center bg-white p-4 dark:bg-neutral-950">
151
+ <FocusAwareStatusBar />
152
+ <Text className="mb-4 text-center text-neutral-900 dark:text-neutral-100">
153
+ {translate('common.error')}: {error}
154
+ </Text>
155
+ <Button
156
+ onPress={() => router.back()}
157
+ label={translate('image_generator.go_back')}
158
+ />
159
+ </View>
160
+ );
161
+ }
162
+
163
+ return (
164
+ <View className="flex-1">
165
+ <FocusAwareStatusBar />
166
+ <Stack.Screen
167
+ options={{
168
+ ...screenOptions,
169
+ title: `${translate('image_generator.gallery_title')} (${images.length})`,
170
+ }}
171
+ />
172
+
173
+ {images.length === 0 ? (
174
+ <View
175
+ className="flex-1 items-center justify-center p-8"
176
+ style={{ paddingBottom: insets.bottom + 32 }}
177
+ >
178
+ <Text className="mb-4 text-center text-xl font-semibold text-neutral-900 dark:text-neutral-100">
179
+ {translate('image_generator.no_images_title')}
180
+ </Text>
181
+ <Text className="mb-6 text-center text-neutral-500 dark:text-neutral-400">
182
+ {translate('image_generator.no_images_description')}
183
+ </Text>
184
+ <Button
185
+ onPress={() => router.back()}
186
+ label={translate('image_generator.generate_images_button')}
187
+ />
188
+ </View>
189
+ ) : (
190
+ <ScrollView
191
+ contentContainerStyle={{
192
+ padding: 16,
193
+ paddingTop: Platform.OS === 'ios' ? 84 : 16,
194
+ paddingBottom: insets.bottom + 16,
195
+ }}
196
+ >
197
+ <Gallery
198
+ items={galleryItems}
199
+ columns={2}
200
+ columnsTablet={4}
201
+ spacing={8}
202
+ borderRadius={12}
203
+ aspectRatio={1}
204
+ onPressItem={handleImagePress}
205
+ onLongPressItem={handleImageLongPress}
206
+ />
207
+ </ScrollView>
208
+ )}
209
+
210
+ <ImageDetailModal
211
+ image={selectedImage}
212
+ onDelete={handleDeleteFromModal}
213
+ onClose={handleCloseModal}
214
+ />
215
+ </View>
216
+ );
217
+ }
@@ -0,0 +1,237 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
2
+ import { router, Stack } from 'expo-router';
3
+ import React, { useState } from 'react';
4
+ import {
5
+ Keyboard,
6
+ ScrollView,
7
+ TouchableWithoutFeedback,
8
+ View,
9
+ } from 'react-native';
10
+
11
+ import { reportingApi } from '@/api-client/reporting';
12
+ import {
13
+ Button,
14
+ FocusAwareStatusBar,
15
+ Image,
16
+ Input,
17
+ Pressable,
18
+ Text,
19
+ } from '@/components/ui';
20
+ import { ReportContentModal } from '@/features/chatbot/components/report-content-modal';
21
+ import type { ReportReason } from '@/features/chatbot/constants/report-reasons';
22
+ import { ImageModelSelector } from '@/features/image-generator/components/image-model-selector';
23
+ import { ImagePlaceholder } from '@/features/image-generator/components/image-placeholder';
24
+ import { useImageGenerator } from '@/features/image-generator/hooks/use-image-generator';
25
+ import { ImageSaveService } from '@/features/image-generator/services/image-save-service';
26
+ import { translate } from '@/lib';
27
+ import { useThemeConfig } from '@/lib/use-theme-config';
28
+
29
+ export default function ImageGeneratorScreen() {
30
+ const {
31
+ prompt,
32
+ setPrompt,
33
+ isLoading,
34
+ generatedImageData,
35
+ generatedImageUri,
36
+ handleGenerateImage,
37
+ } = useImageGenerator();
38
+
39
+ const [isReportModalVisible, setIsReportModalVisible] = useState(false);
40
+ const theme = useThemeConfig();
41
+ // Use a ref to store a stable ID for the report modal
42
+ const reportIdRef = React.useRef<string>(`generated_image_${Date.now()}`);
43
+
44
+ const reportImageMutation = reportingApi.useReportAIGeneratedImage();
45
+
46
+ const handleSaveImage = async () => {
47
+ if (!generatedImageData) {
48
+ return;
49
+ }
50
+ await ImageSaveService.saveImageToGallery(generatedImageData.imageUri, {
51
+ prompt: generatedImageData.prompt,
52
+ provider: generatedImageData.provider,
53
+ model: generatedImageData.model,
54
+ remoteImageId: generatedImageData.imageId,
55
+ storageId: generatedImageData.storageId,
56
+ });
57
+ };
58
+
59
+ const handleReportImage = () => {
60
+ setIsReportModalVisible(true);
61
+ };
62
+
63
+ const handleReportSubmit = async (reportData: {
64
+ reason: ReportReason;
65
+ details?: string;
66
+ }) => {
67
+ if (!generatedImageData) {
68
+ return false;
69
+ }
70
+
71
+ try {
72
+ await reportImageMutation({
73
+ prompt: generatedImageData.prompt,
74
+ provider: generatedImageData.provider,
75
+ model: generatedImageData.model,
76
+ reason: reportData.reason,
77
+ details: reportData.details,
78
+ imageId: generatedImageData.imageId,
79
+ storageId: generatedImageData.storageId,
80
+ });
81
+
82
+ setIsReportModalVisible(false);
83
+ return true;
84
+ } catch (error) {
85
+ console.error('Failed to report image:', error);
86
+ return false;
87
+ }
88
+ };
89
+
90
+ const handleReportCancel = () => {
91
+ setIsReportModalVisible(false);
92
+ };
93
+
94
+ const isGenerateDisabled = isLoading || !prompt.trim();
95
+
96
+ // Create a mock message object for the ReportContentModal
97
+ const mockMessage = React.useMemo(() => {
98
+ const result =
99
+ isReportModalVisible && generatedImageData
100
+ ? {
101
+ id: generatedImageData.imageId ?? reportIdRef.current,
102
+ content: `Generated image with prompt: ${generatedImageData.prompt}`,
103
+ role: 'assistant' as const,
104
+ createdAt: new Date(),
105
+ }
106
+ : null;
107
+
108
+ console.log('🚩 [ImageGenerator] mockMessage created:', {
109
+ id: result?.id,
110
+ hasContent: !!result?.content,
111
+ isVisible: isReportModalVisible,
112
+ hasImageData: !!generatedImageData,
113
+ });
114
+
115
+ return result;
116
+ }, [isReportModalVisible, generatedImageData]);
117
+
118
+ const galleryButton = (
119
+ <Pressable
120
+ onPress={() => router.push('/image-generator/gallery')}
121
+ className="pl-2"
122
+ style={{ marginRight: 8 }}
123
+ testID="gallery-button"
124
+ accessibilityLabel="Open image gallery"
125
+ accessibilityRole="button"
126
+ >
127
+ <MaterialIcons
128
+ name="photo-library"
129
+ size={22}
130
+ color={theme.colors.foreground}
131
+ />
132
+ </Pressable>
133
+ );
134
+
135
+ return (
136
+ <>
137
+ <FocusAwareStatusBar />
138
+ <Stack.Screen
139
+ options={{
140
+ title: translate('image_generator.title'),
141
+ headerRight: () => galleryButton,
142
+ }}
143
+ />
144
+ <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
145
+ <View className="flex-1 px-4 pt-4">
146
+ <ScrollView
147
+ keyboardShouldPersistTaps="handled"
148
+ showsVerticalScrollIndicator={false}
149
+ automaticallyAdjustKeyboardInsets={true}
150
+ >
151
+ {/* Image Display Area */}
152
+ <View className="flex-1">
153
+ <View className="mb-4 flex-row items-center justify-between">
154
+ <Text className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
155
+ Generated Image
156
+ </Text>
157
+ <ImageModelSelector testID="image-model-selector-inline" />
158
+ </View>
159
+ {generatedImageUri ? (
160
+ <Image
161
+ source={{ uri: generatedImageUri }}
162
+ className="aspect-square w-full rounded-lg"
163
+ accessibilityRole="image"
164
+ accessibilityLabel="Generated AI image"
165
+ testID="generated-image"
166
+ contentFit="contain"
167
+ />
168
+ ) : (
169
+ <ImagePlaceholder />
170
+ )}
171
+ </View>
172
+
173
+ {/* Input and Generate Section */}
174
+ <View className="gap-2 pt-8">
175
+ <View>
176
+ <Text className="mb-2 text-base font-semibold text-neutral-900 dark:text-neutral-100">
177
+ Image Prompt
178
+ </Text>
179
+ <Input
180
+ value={prompt}
181
+ onChangeText={setPrompt}
182
+ placeholder="Describe the image you want to generate..."
183
+ multiline
184
+ numberOfLines={3}
185
+ textAlignVertical="top"
186
+ testID="prompt-input"
187
+ editable={!isLoading}
188
+ accessibilityLabel="Image generation prompt input"
189
+ accessibilityHint="Enter a description for the AI to generate an image"
190
+ />
191
+ </View>
192
+
193
+ <Button
194
+ onPress={handleGenerateImage}
195
+ disabled={isGenerateDisabled}
196
+ testID="generate-button"
197
+ accessibilityLabel="Generate image"
198
+ accessibilityHint="Generate an AI image based on your prompt"
199
+ accessibilityState={{ disabled: isGenerateDisabled }}
200
+ label={isLoading ? 'Generating...' : 'Generate Image'}
201
+ loading={isLoading}
202
+ />
203
+
204
+ {/* Action buttons for generated image */}
205
+ {generatedImageUri && !isLoading && (
206
+ <View className="flex-row gap-2">
207
+ <Button
208
+ onPress={handleSaveImage}
209
+ variant="outline"
210
+ testID="save-gallery-button"
211
+ className="flex-1"
212
+ label="Save to Gallery"
213
+ />
214
+ <Button
215
+ onPress={handleReportImage}
216
+ variant="outline"
217
+ testID="report-image-button"
218
+ className="flex-1"
219
+ label="Report Image"
220
+ />
221
+ </View>
222
+ )}
223
+ </View>
224
+ </ScrollView>
225
+ <ReportContentModal
226
+ message={mockMessage}
227
+ onSubmit={async (reportData) => {
228
+ const success = await handleReportSubmit(reportData);
229
+ return success;
230
+ }}
231
+ onCancel={handleReportCancel}
232
+ />
233
+ </View>
234
+ </TouchableWithoutFeedback>
235
+ </>
236
+ );
237
+ }
@@ -0,0 +1,26 @@
1
+ import type React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Image, Pressable, Text } from '@/components/ui';
5
+ import type { SavedImageMetadata } from '@/features/image-generator/services/image-gallery-service';
6
+
7
+ export const GalleryImageItem: React.FC<{
8
+ image: SavedImageMetadata;
9
+ onPress(): void;
10
+ onLongPress(): void;
11
+ }> = ({ image, onPress, onLongPress }) => (
12
+ <Pressable
13
+ onPress={onPress}
14
+ onLongPress={onLongPress}
15
+ className="w-full"
16
+ testID={`gallery-image-${image.id}`}
17
+ accessibilityRole="button"
18
+ accessibilityLabel={`Generated image from ${image.provider} using ${image.model}`}
19
+ >
20
+ <Image
21
+ source={{ uri: image.localUri }}
22
+ className="w-full aspect-square rounded-lg"
23
+ contentFit="cover"
24
+ />
25
+ </Pressable>
26
+ );
@@ -0,0 +1,215 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import React, { useMemo, useState } from 'react';
3
+ import { Alert, ScrollView, View } from 'react-native';
4
+
5
+ import { reportingApi } from '@/api-client/reporting';
6
+ import { Button, Image, Modal, Text, useModal } from '@/components/ui';
7
+ import { ReportContentModal } from '@/features/chatbot/components/report-content-modal';
8
+ import type { ReportReason } from '@/features/chatbot/constants/report-reasons';
9
+ import type { AppMessage } from '@/features/chatbot/types';
10
+ import type { SavedImageMetadata } from '@/features/image-generator/services/image-gallery-service';
11
+
12
+ type ImageDetailModalProps = {
13
+ image: SavedImageMetadata | null;
14
+ onDelete: (imageId: string) => void;
15
+ onClose: () => void;
16
+ };
17
+
18
+ /**
19
+ * Modal component for displaying detailed image information
20
+ */
21
+ export const ImageDetailModal: React.FC<ImageDetailModalProps> = ({
22
+ image,
23
+ onDelete,
24
+ onClose,
25
+ }) => {
26
+ const { ref, dismiss } = useModal();
27
+ const [isReportModalVisible, setIsReportModalVisible] = useState(false);
28
+
29
+ const reportImageMutation = reportingApi.useReportAIGeneratedImage();
30
+
31
+ React.useEffect(() => {
32
+ if (image) {
33
+ ref.current?.present();
34
+ } else {
35
+ ref.current?.dismiss();
36
+ }
37
+ }, [image, ref]);
38
+
39
+ const handleDelete = () => {
40
+ if (!image) return;
41
+
42
+ Alert.alert('Delete Image', 'Are you sure you want to delete this image?', [
43
+ { text: 'Cancel', style: 'cancel' },
44
+ {
45
+ text: 'Delete',
46
+ style: 'destructive',
47
+ onPress: () => {
48
+ onDelete(image.id);
49
+ dismiss();
50
+ onClose();
51
+ },
52
+ },
53
+ ]);
54
+ };
55
+
56
+ const handleReport = () => {
57
+ console.log('🚩 [ImageDetailModal] Report button pressed!');
58
+ setIsReportModalVisible(true);
59
+ };
60
+
61
+ const handleReportSubmit = async (reportData: {
62
+ reason: ReportReason;
63
+ details?: string;
64
+ }) => {
65
+ console.log('🚩 [ImageDetailModal] Report submit called with:', reportData);
66
+ if (!image) {
67
+ console.log('🚩 [ImageDetailModal] No image available for reporting');
68
+ return false;
69
+ }
70
+
71
+ try {
72
+ console.log('🚩 [ImageDetailModal] Submitting report for image:', {
73
+ prompt: image.prompt,
74
+ provider: image.provider,
75
+ model: image.model,
76
+ });
77
+
78
+ await reportImageMutation({
79
+ prompt: image.prompt,
80
+ provider: image.provider,
81
+ model: image.model,
82
+ reason: reportData.reason,
83
+ details: reportData.details,
84
+ imageId: image.remoteImageId,
85
+ storageId: image.storageId,
86
+ });
87
+
88
+ console.log('🚩 [ImageDetailModal] Report submitted successfully');
89
+ setIsReportModalVisible(false);
90
+ Alert.alert(
91
+ 'Report Submitted',
92
+ 'Thank you for your report. It has been submitted for review.',
93
+ );
94
+ return true;
95
+ } catch (error) {
96
+ console.error('🚩 [ImageDetailModal] Failed to report image:', error);
97
+ return false;
98
+ }
99
+ };
100
+
101
+ const handleReportCancel = () => {
102
+ console.log('🚩 [ImageDetailModal] Report cancelled');
103
+ setIsReportModalVisible(false);
104
+ };
105
+
106
+ const handleModalDismiss = () => {
107
+ onClose();
108
+ };
109
+
110
+ const formatDate = (timestamp: number): string => {
111
+ return new Date(timestamp).toLocaleString();
112
+ };
113
+
114
+ const reportMessage = useMemo<AppMessage | null>(() => {
115
+ if (!isReportModalVisible || !image) {
116
+ return null;
117
+ }
118
+ return {
119
+ id: image.remoteImageId ?? `gallery_image_${image.id}`,
120
+ role: 'assistant',
121
+ content: `Image generated with prompt: ${image.prompt}`,
122
+ createdAt: new Date(image.savedAt),
123
+ };
124
+ }, [image, isReportModalVisible]);
125
+
126
+ if (!image) return null;
127
+
128
+ return (
129
+ <>
130
+ <Modal
131
+ ref={ref}
132
+ snapPoints={['90%']}
133
+ title="Image Details"
134
+ onDismiss={handleModalDismiss}
135
+ >
136
+ <ScrollView className="flex-1 px-4 pb-4">
137
+ <View className="mb-6">
138
+ <View className="aspect-square w-full overflow-hidden rounded-lg bg-neutral-100 dark:bg-neutral-800">
139
+ <Image
140
+ source={{ uri: image.localUri }}
141
+ className="size-full"
142
+ contentFit="contain"
143
+ />
144
+ </View>
145
+ </View>
146
+
147
+ <View className="mb-4">
148
+ <Text className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
149
+ Prompt
150
+ </Text>
151
+ <Text className="text-base text-neutral-700 dark:text-neutral-300">
152
+ {image.prompt}
153
+ </Text>
154
+ </View>
155
+
156
+ <View className="mb-4">
157
+ <Text className="mb-2 text-lg font-semibold text-neutral-900 dark:text-neutral-100">
158
+ Details
159
+ </Text>
160
+ <View className="gap-2">
161
+ <View className="flex-row justify-between">
162
+ <Text className="text-neutral-600 dark:text-neutral-400">
163
+ Provider:
164
+ </Text>
165
+ <Text className="font-medium text-neutral-900 dark:text-neutral-100">
166
+ {image.provider}
167
+ </Text>
168
+ </View>
169
+ <View className="flex-row justify-between">
170
+ <Text className="text-neutral-600 dark:text-neutral-400">
171
+ Model:
172
+ </Text>
173
+ <Text className="font-medium text-neutral-900 dark:text-neutral-100">
174
+ {image.model}
175
+ </Text>
176
+ </View>
177
+ <View className="flex-row justify-between">
178
+ <Text className="text-neutral-600 dark:text-neutral-400">
179
+ Created:
180
+ </Text>
181
+ <Text className="font-medium text-neutral-900 dark:text-neutral-100">
182
+ {formatDate(image.savedAt)}
183
+ </Text>
184
+ </View>
185
+ </View>
186
+ </View>
187
+
188
+ <View className="gap-3 pt-4">
189
+ <Button
190
+ variant="outline"
191
+ label="Report Image"
192
+ leftIcon={<Ionicons name="flag" size={16} color="#000000" />}
193
+ onPress={handleReport}
194
+ testID="report-image-button"
195
+ accessibilityLabel="Report this image for inappropriate content"
196
+ />
197
+ <Button
198
+ variant="destructive"
199
+ label="Delete Image"
200
+ onPress={handleDelete}
201
+ testID="delete-image-button"
202
+ accessibilityLabel="Delete this image"
203
+ />
204
+ </View>
205
+ </ScrollView>
206
+ </Modal>
207
+
208
+ <ReportContentModal
209
+ message={reportMessage}
210
+ onSubmit={handleReportSubmit}
211
+ onCancel={handleReportCancel}
212
+ />
213
+ </>
214
+ );
215
+ };