vibefast-cli 1.1.5 → 1.3.0

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 (301) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +63 -169
  3. package/dist/__tests__/recipes.test.js +89 -85
  4. package/dist/__tests__/recipes.test.js.map +1 -1
  5. package/dist/commands/add.d.ts +1 -1
  6. package/dist/commands/add.d.ts.map +1 -1
  7. package/dist/commands/add.js +576 -588
  8. package/dist/commands/add.js.map +1 -1
  9. package/dist/commands/checklist.d.ts +1 -1
  10. package/dist/commands/checklist.d.ts.map +1 -1
  11. package/dist/commands/checklist.js +40 -39
  12. package/dist/commands/checklist.js.map +1 -1
  13. package/dist/commands/doctor.d.ts +1 -1
  14. package/dist/commands/doctor.js +22 -22
  15. package/dist/commands/doctor.js.map +1 -1
  16. package/dist/commands/env.d.ts +1 -1
  17. package/dist/commands/env.d.ts.map +1 -1
  18. package/dist/commands/env.js +58 -53
  19. package/dist/commands/env.js.map +1 -1
  20. package/dist/commands/health.d.ts +1 -1
  21. package/dist/commands/health.d.ts.map +1 -1
  22. package/dist/commands/health.js +101 -93
  23. package/dist/commands/health.js.map +1 -1
  24. package/dist/commands/init.d.ts +1 -1
  25. package/dist/commands/init.d.ts.map +1 -1
  26. package/dist/commands/init.js +416 -296
  27. package/dist/commands/init.js.map +1 -1
  28. package/dist/commands/remove.d.ts +1 -1
  29. package/dist/commands/remove.d.ts.map +1 -1
  30. package/dist/commands/remove.js +77 -64
  31. package/dist/commands/remove.js.map +1 -1
  32. package/dist/commands/status.d.ts +1 -1
  33. package/dist/commands/status.d.ts.map +1 -1
  34. package/dist/commands/status.js +15 -14
  35. package/dist/commands/status.js.map +1 -1
  36. package/dist/core/__tests__/detect.test.js +68 -34
  37. package/dist/core/__tests__/detect.test.js.map +1 -1
  38. package/dist/core/ast.d.ts +14 -0
  39. package/dist/core/ast.d.ts.map +1 -0
  40. package/dist/core/ast.js +239 -0
  41. package/dist/core/ast.js.map +1 -0
  42. package/dist/core/codemod.d.ts.map +1 -1
  43. package/dist/core/codemod.js +62 -44
  44. package/dist/core/codemod.js.map +1 -1
  45. package/dist/core/config.d.ts +10 -0
  46. package/dist/core/config.d.ts.map +1 -0
  47. package/dist/core/config.js +51 -0
  48. package/dist/core/config.js.map +1 -0
  49. package/dist/core/detect.d.ts +8 -2
  50. package/dist/core/detect.d.ts.map +1 -1
  51. package/dist/core/detect.js +52 -21
  52. package/dist/core/detect.js.map +1 -1
  53. package/dist/core/errors.d.ts.map +1 -1
  54. package/dist/core/errors.js +9 -8
  55. package/dist/core/errors.js.map +1 -1
  56. package/dist/core/exec.d.ts +16 -0
  57. package/dist/core/exec.d.ts.map +1 -0
  58. package/dist/core/exec.js +48 -0
  59. package/dist/core/exec.js.map +1 -0
  60. package/dist/core/manualSteps.d.ts +7 -0
  61. package/dist/core/manualSteps.d.ts.map +1 -0
  62. package/dist/core/manualSteps.js +59 -0
  63. package/dist/core/manualSteps.js.map +1 -0
  64. package/dist/core/paths.d.ts +3 -1
  65. package/dist/core/paths.d.ts.map +1 -1
  66. package/dist/core/paths.js +14 -10
  67. package/dist/core/paths.js.map +1 -1
  68. package/dist/core/spinner.d.ts +1 -1
  69. package/dist/core/spinner.d.ts.map +1 -1
  70. package/dist/core/spinner.js +38 -8
  71. package/dist/core/spinner.js.map +1 -1
  72. package/dist/core/vosk.d.ts.map +1 -1
  73. package/dist/core/vosk.js +50 -39
  74. package/dist/core/vosk.js.map +1 -1
  75. package/docs/manual-testing.md +91 -0
  76. package/package.json +6 -3
  77. package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
  78. package/recipes/audio-recorder/recipe.json +3 -3
  79. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
  80. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
  81. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
  82. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
  83. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
  84. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
  85. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
  86. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
  87. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
  88. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
  89. package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
  90. package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
  91. package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
  92. package/recipes/audio-recorder-supabase/recipe.json +52 -0
  93. package/recipes/audio-recorder-supabase@latest.zip +0 -0
  94. package/recipes/audio-recorder@latest.zip +0 -0
  95. package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
  96. package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
  97. package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
  98. package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
  99. package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
  100. package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
  101. package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
  102. package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
  103. package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
  104. package/recipes/charts/recipe.json +4 -13
  105. package/recipes/charts@latest.zip +0 -0
  106. package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
  107. package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
  108. package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
  109. package/recipes/chatbot/recipe.json +26 -92
  110. package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
  111. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
  112. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
  113. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
  114. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
  115. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
  116. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
  117. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
  118. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
  119. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
  120. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
  121. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
  122. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
  123. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
  124. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
  125. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
  126. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
  127. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
  128. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
  129. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
  130. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
  131. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
  132. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
  133. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
  134. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
  135. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
  136. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
  137. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
  138. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
  139. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
  140. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
  141. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
  142. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
  143. package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
  144. package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
  145. package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
  146. package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
  147. package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
  148. package/recipes/chatbot-supabase/recipe.json +106 -0
  149. package/recipes/chatbot-supabase@latest.zip +0 -0
  150. package/recipes/chatbot.zip +0 -0
  151. package/recipes/chatbot@latest.zip +0 -0
  152. package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
  153. package/recipes/image-analysis/packages/backend/convex/imageAnalysis.ts +0 -1
  154. package/recipes/image-analysis/recipe.json +15 -55
  155. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
  156. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
  157. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
  158. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
  159. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
  160. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
  161. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
  162. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
  163. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
  164. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
  165. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
  166. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
  167. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
  168. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
  169. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
  170. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
  171. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
  172. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
  173. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
  174. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
  175. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
  176. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
  177. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
  178. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
  179. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
  180. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
  181. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
  182. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
  183. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
  184. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
  185. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
  186. package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
  187. package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
  188. package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
  189. package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
  190. package/recipes/image-analysis-supabase/recipe.json +90 -0
  191. package/recipes/image-analysis-supabase@latest.zip +0 -0
  192. package/recipes/image-analysis@latest.zip +0 -0
  193. package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
  194. package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
  195. package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
  196. package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
  197. package/recipes/image-generator/recipe.json +16 -39
  198. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
  199. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
  200. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
  201. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
  202. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
  203. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
  204. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
  205. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
  206. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
  207. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
  208. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
  209. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
  210. package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
  211. package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
  212. package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
  213. package/recipes/image-generator-supabase/recipe.json +86 -0
  214. package/recipes/image-generator-supabase@latest.zip +0 -0
  215. package/recipes/image-generator@latest.zip +0 -0
  216. package/recipes/ios-widget/recipe.json +15 -24
  217. package/recipes/ios-widget@latest.zip +0 -0
  218. package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
  219. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
  220. package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
  221. package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
  222. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
  223. package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
  224. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
  225. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
  226. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
  227. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
  228. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
  229. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
  230. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
  231. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
  232. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
  233. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
  234. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
  235. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
  236. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
  237. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
  238. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
  239. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
  240. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
  241. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
  242. package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
  243. package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
  244. package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
  245. package/recipes/onboarding/recipe.json +15 -0
  246. package/recipes/onboarding@latest.zip +0 -0
  247. package/recipes/payments/recipe.json +28 -61
  248. package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
  249. package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
  250. package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
  251. package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
  252. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
  253. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
  254. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
  255. package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
  256. package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
  257. package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
  258. package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
  259. package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
  260. package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
  261. package/recipes/payments-supabase/recipe.json +72 -0
  262. package/recipes/payments-supabase@latest.zip +0 -0
  263. package/recipes/payments@latest.zip +0 -0
  264. package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
  265. package/recipes/quiz/recipe.json +6 -9
  266. package/recipes/quiz@latest.zip +0 -0
  267. package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
  268. package/recipes/tracker-app/recipe.json +7 -10
  269. package/recipes/tracker-app@latest.zip +0 -0
  270. package/recipes/voice-bot/recipe.json +8 -68
  271. package/recipes/voice-bot.zip +0 -0
  272. package/recipes/voice-bot@latest.zip +0 -0
  273. package/recipes/wake-word/recipe.json +10 -9
  274. package/recipes/wake-word.zip +0 -0
  275. package/recipes/wake-word@latest.zip +0 -0
  276. package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
  277. package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
  278. package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
  279. package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
  280. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
  281. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
  282. package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
  283. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
  284. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
  285. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
  286. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
  287. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
  288. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
  289. package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
  290. package/recipes/image-analysis/packages/backend/convex/imageAnalysisFunctions.ts +0 -325
  291. package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
  292. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
  293. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
  294. package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
  295. package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
  296. package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
  297. package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
  298. package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
  299. /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
  300. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
  301. /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/index.tsx +0 -0
@@ -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
+ };
@@ -0,0 +1,216 @@
1
+ import { Feather } from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { ScrollView, View } from 'react-native';
4
+
5
+ import { Pressable, Text } from '@/components/ui';
6
+ import colors from '@/components/ui/colors';
7
+ import { Modal, useModal } from '@/components/ui/core/overlays/modal';
8
+ import { GeminiIcon } from '@/components/ui/icons/gemini';
9
+ import { OpenaiIcon } from '@/components/ui/icons/openai';
10
+ import type { ImageGeneratorSettings } from '@/features/image-generator/hooks/use-image-generator-settings';
11
+ import {
12
+ DEFAULT_IMAGE_MODEL_ID,
13
+ getImageModelById,
14
+ getImageModelsByProvider,
15
+ getProviderDisplayName,
16
+ IMAGE_MODEL_PROVIDERS,
17
+ type ImageModel,
18
+ type ImageModelId,
19
+ type ImageProvider,
20
+ } from '@/features/image-generator/models/models';
21
+ import { useThemeConfig } from '@/lib/use-theme-config';
22
+
23
+ const getProviderIcon = (provider: ImageProvider): React.ComponentType<any> => {
24
+ switch (provider) {
25
+ case 'openai':
26
+ return OpenaiIcon;
27
+ case 'gemini':
28
+ return GeminiIcon;
29
+ default:
30
+ return OpenaiIcon;
31
+ }
32
+ };
33
+
34
+ type ModelSelectorProps = {
35
+ testID?: string;
36
+ settings: ImageGeneratorSettings;
37
+ onModelChange: (modelId: ImageModelId) => Promise<void> | void;
38
+ };
39
+
40
+ /**
41
+ * Image model selector component that mirrors the chatbot model selector UX.
42
+ */
43
+ export const ImageModelSelector: React.FC<ModelSelectorProps> = ({
44
+ testID = 'image-model-selector',
45
+ settings,
46
+ onModelChange,
47
+ }) => {
48
+ const theme = useThemeConfig();
49
+ const modal = useModal();
50
+
51
+ const currentModel =
52
+ getImageModelById(settings.modelId) ??
53
+ getImageModelById(DEFAULT_IMAGE_MODEL_ID);
54
+
55
+ const getCurrentModelIcon = () => {
56
+ if (!currentModel) return null;
57
+ const IconComponent = getProviderIcon(currentModel.provider);
58
+ return <IconComponent />;
59
+ };
60
+
61
+ const handleModelSelect = async (modelId: ImageModelId) => {
62
+ try {
63
+ await onModelChange(modelId);
64
+ } finally {
65
+ modal.dismiss();
66
+ }
67
+ };
68
+
69
+ return (
70
+ <>
71
+ <Pressable
72
+ onPress={modal.present}
73
+ className="flex-row items-center gap-2.5 rounded-full border border-neutral-300/60 bg-neutral-50 px-4 py-3.5 dark:border-neutral-800 dark:bg-neutral-800"
74
+ style={{ alignSelf: 'flex-start' }}
75
+ accessibilityLabel={`Current model: ${currentModel?.name ?? 'Select image model'}`}
76
+ accessibilityHint="Tap to change image generation model"
77
+ accessibilityRole="button"
78
+ testID={testID}
79
+ >
80
+ <View className="size-4">{getCurrentModelIcon()}</View>
81
+ <View className="max-w-[140px]">
82
+ <Text
83
+ className="text-xs font-medium text-neutral-800 dark:text-neutral-200"
84
+ numberOfLines={1}
85
+ >
86
+ {currentModel?.name ?? 'Select image model'}
87
+ </Text>
88
+ {/* <Text className="text-[10px] text-neutral-500 dark:text-neutral-400">
89
+ {currentModel?.provider
90
+ ? getProviderDisplayName(currentModel.provider)
91
+ : getProviderDisplayName("gemini")}
92
+ </Text> */}
93
+ </View>
94
+ <Feather
95
+ name="chevron-down"
96
+ size={14}
97
+ color={theme.dark ? colors.neutral[400] : colors.neutral[500]}
98
+ />
99
+ </Pressable>
100
+
101
+ <Modal ref={modal.ref} index={0} snapPoints={['70%']}>
102
+ <ScrollView
103
+ className="flex-1"
104
+ showsVerticalScrollIndicator={false}
105
+ contentContainerStyle={{ paddingHorizontal: 24 }}
106
+ >
107
+ <View className="mb-6 mt-2 items-center">
108
+ <Text className="text-xl font-semibold text-neutral-900 dark:text-neutral-50">
109
+ Choose an image model
110
+ </Text>
111
+ <Text className="mt-1 text-center text-xs text-neutral-500 dark:text-neutral-400">
112
+ Different models balance quality, speed, and style. Pick the one
113
+ that fits your workflow.
114
+ </Text>
115
+ </View>
116
+
117
+ {IMAGE_MODEL_PROVIDERS.map((provider) => {
118
+ const providerModels = getImageModelsByProvider(provider);
119
+ if (!providerModels.length) {
120
+ return null;
121
+ }
122
+
123
+ const ProviderIcon = getProviderIcon(provider);
124
+
125
+ return (
126
+ <View key={provider} className="mb-6">
127
+ <View className="mb-4 flex-row items-center gap-3">
128
+ <View className="size-6">
129
+ <ProviderIcon />
130
+ </View>
131
+ <Text className="text-lg font-semibold text-neutral-900 dark:text-neutral-100">
132
+ {getProviderDisplayName(provider)}
133
+ </Text>
134
+ </View>
135
+
136
+ {providerModels.map((model) => (
137
+ <ModelOption
138
+ key={model.id}
139
+ model={model}
140
+ isSelected={settings.modelId === model.id}
141
+ onSelect={() => handleModelSelect(model.id)}
142
+ testID={`${testID}-${model.id}`}
143
+ />
144
+ ))}
145
+ </View>
146
+ );
147
+ })}
148
+ </ScrollView>
149
+ </Modal>
150
+ </>
151
+ );
152
+ };
153
+
154
+ type ModelOptionProps = {
155
+ model: ImageModel;
156
+ isSelected: boolean;
157
+ onSelect: () => void;
158
+ testID?: string;
159
+ };
160
+
161
+ const ModelOption: React.FC<ModelOptionProps> = ({
162
+ model,
163
+ isSelected,
164
+ onSelect,
165
+ testID,
166
+ }) => {
167
+ const theme = useThemeConfig();
168
+
169
+ return (
170
+ <Pressable
171
+ onPress={onSelect}
172
+ className="mb-3 rounded-2xl border p-4"
173
+ style={{
174
+ borderColor: isSelected
175
+ ? theme.dark
176
+ ? 'rgba(96, 165, 250, 0.7)'
177
+ : 'rgba(37, 99, 235, 0.7)'
178
+ : theme.dark
179
+ ? 'rgba(71, 85, 105, 0.6)'
180
+ : 'rgba(203, 213, 225, 0.7)',
181
+ backgroundColor: isSelected
182
+ ? theme.dark
183
+ ? 'rgba(37, 99, 235, 0.14)'
184
+ : 'rgba(219, 234, 254, 0.6)'
185
+ : theme.dark
186
+ ? 'rgba(51, 65, 85, 0.35)'
187
+ : 'rgba(248, 250, 252, 0.5)',
188
+ }}
189
+ accessibilityLabel={`Select ${model.name}`}
190
+ accessibilityHint="Applies this model for future image generations"
191
+ accessibilityState={{ selected: isSelected }}
192
+ testID={testID}
193
+ >
194
+ <View className="flex-row items-center justify-between">
195
+ <Text className="text-base font-semibold text-neutral-900 dark:text-neutral-100">
196
+ {model.name}
197
+ </Text>
198
+ <View className="flex-row items-center gap-2">
199
+ {model.isPremium ? (
200
+ <Text className="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-400/20 dark:text-amber-200">
201
+ Premium
202
+ </Text>
203
+ ) : null}
204
+ {model.defaultSize || model.defaultAspectRatio ? (
205
+ <Text className="text-[10px] font-medium uppercase tracking-wide text-neutral-500 dark:text-neutral-400">
206
+ {model.defaultSize ?? model.defaultAspectRatio}
207
+ </Text>
208
+ ) : null}
209
+ </View>
210
+ </View>
211
+ <Text className="mt-2 text-xs text-neutral-600 dark:text-neutral-300">
212
+ {model.description}
213
+ </Text>
214
+ </Pressable>
215
+ );
216
+ };
@@ -0,0 +1,26 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { View } from 'react-native';
4
+
5
+ import { Text } from '@/components/ui';
6
+ /**
7
+ * Placeholder component to show before first generation
8
+ */
9
+ export const ImagePlaceholder: React.FC = () => (
10
+ <View
11
+ className="aspect-square w-full items-center justify-center rounded-lg border-2 border-dashed border-neutral-300 bg-neutral-50 dark:border-neutral-600 dark:bg-neutral-800"
12
+ testID="image-placeholder"
13
+ >
14
+ <View className="items-center justify-center">
15
+ <MaterialIcons
16
+ name="brush"
17
+ size={32}
18
+ color="#9CA3AF"
19
+ style={{ marginBottom: 8 }}
20
+ />
21
+ <Text className="text-center text-neutral-500 dark:text-neutral-400">
22
+ Generated image will appear here
23
+ </Text>
24
+ </View>
25
+ </View>
26
+ );
@@ -0,0 +1,71 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import {
4
+ ImageGalleryService,
5
+ type SavedImageMetadata,
6
+ } from '@/features/image-generator/services/image-gallery-service';
7
+
8
+ /**
9
+ * Hook for managing the local image gallery
10
+ */
11
+ export const useImageGallery = () => {
12
+ const [images, setImages] = useState<SavedImageMetadata[]>([]);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ // Load images from storage
17
+ const loadImages = useCallback(async () => {
18
+ try {
19
+ setIsLoading(true);
20
+ setError(null);
21
+ const galleryImages = await ImageGalleryService.getAllImages();
22
+ setImages(galleryImages);
23
+ } catch (_err) {
24
+ console.error('Failed to load gallery images:', _err);
25
+ setError('Failed to load images');
26
+ } finally {
27
+ setIsLoading(false);
28
+ }
29
+ }, []);
30
+
31
+ // Remove an image from the gallery
32
+ const removeImage = useCallback(async (imageId: string) => {
33
+ try {
34
+ await ImageGalleryService.removeImage(imageId);
35
+ setImages((prev) => prev.filter((img) => img.id !== imageId));
36
+ } catch (_err) {
37
+ console.error('Failed to remove image:', _err);
38
+ setError('Failed to remove image');
39
+ }
40
+ }, []);
41
+
42
+ // Clear all images from the gallery
43
+ const clearGallery = useCallback(async () => {
44
+ try {
45
+ await ImageGalleryService.clearGallery();
46
+ setImages([]);
47
+ } catch (_err) {
48
+ console.error('Failed to clear gallery:', _err);
49
+ setError('Failed to clear gallery');
50
+ }
51
+ }, []);
52
+
53
+ // Refresh the gallery
54
+ const refreshGallery = useCallback(() => {
55
+ loadImages();
56
+ }, [loadImages]);
57
+
58
+ // Load images on mount
59
+ useEffect(() => {
60
+ loadImages();
61
+ }, [loadImages]);
62
+
63
+ return {
64
+ images,
65
+ isLoading,
66
+ error,
67
+ removeImage,
68
+ clearGallery,
69
+ refreshGallery,
70
+ };
71
+ };
@@ -0,0 +1,152 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ import { defaultCacheService } from '@/core/cache';
4
+ import {
5
+ DEFAULT_IMAGE_MODEL_ID,
6
+ getImageModelById,
7
+ getImageModelsByProvider,
8
+ IMAGE_MODELS,
9
+ type ImageModelId,
10
+ type ImageProvider,
11
+ } from '@/features/image-generator/models/models';
12
+
13
+ export type ImageGeneratorSettings = {
14
+ provider: ImageProvider;
15
+ modelId: ImageModelId;
16
+ };
17
+
18
+ const CACHE_KEY = 'image_generator_settings_v3';
19
+
20
+ const DEFAULT_SETTINGS: ImageGeneratorSettings = {
21
+ provider: 'gemini',
22
+ modelId: DEFAULT_IMAGE_MODEL_ID,
23
+ };
24
+
25
+ type RawSettings = Partial<ImageGeneratorSettings> & {
26
+ provider?: unknown;
27
+ modelId?: unknown;
28
+ };
29
+
30
+ const providerHasModel = (
31
+ provider: ImageProvider,
32
+ modelId: ImageModelId,
33
+ ): boolean => {
34
+ return getImageModelsByProvider(provider).some(
35
+ (model) => model.id === modelId,
36
+ );
37
+ };
38
+
39
+ const normalizeSettings = (
40
+ rawSettings: RawSettings | null | undefined,
41
+ ): ImageGeneratorSettings => {
42
+ if (!rawSettings) {
43
+ return DEFAULT_SETTINGS;
44
+ }
45
+
46
+ const provider =
47
+ rawSettings.provider === 'openai' || rawSettings.provider === 'gemini'
48
+ ? rawSettings.provider
49
+ : DEFAULT_SETTINGS.provider;
50
+
51
+ const modelId =
52
+ typeof rawSettings.modelId === 'string' &&
53
+ IMAGE_MODELS.some((model) => model.id === rawSettings.modelId) &&
54
+ providerHasModel(provider, rawSettings.modelId)
55
+ ? (rawSettings.modelId as ImageModelId)
56
+ : DEFAULT_IMAGE_MODEL_ID;
57
+
58
+ return {
59
+ provider,
60
+ modelId,
61
+ };
62
+ };
63
+
64
+ /**
65
+ * Hook for managing image generator settings with persistent storage
66
+ */
67
+ export const useImageGeneratorSettings = () => {
68
+ const [settings, setSettings] =
69
+ useState<ImageGeneratorSettings>(DEFAULT_SETTINGS);
70
+ const [isLoaded, setIsLoaded] = useState(false);
71
+
72
+ // Load settings from cache on mount
73
+ useEffect(() => {
74
+ const loadSettings = async () => {
75
+ try {
76
+ const cached =
77
+ await defaultCacheService.getItem<RawSettings>(CACHE_KEY);
78
+ setSettings(normalizeSettings(cached));
79
+ } catch (error) {
80
+ console.error('Failed to load image generator settings:', error);
81
+ } finally {
82
+ setIsLoaded(true);
83
+ }
84
+ };
85
+
86
+ loadSettings();
87
+ }, []);
88
+
89
+ // Save settings to cache
90
+ const saveSettings = useCallback(
91
+ async (newSettings: ImageGeneratorSettings) => {
92
+ try {
93
+ const normalized = normalizeSettings(newSettings);
94
+ await defaultCacheService.setItem(CACHE_KEY, normalized);
95
+ setSettings(normalized);
96
+ } catch (error) {
97
+ console.error('Failed to save image generator settings:', error);
98
+ }
99
+ },
100
+ [],
101
+ );
102
+
103
+ const updateProvider = useCallback(
104
+ async (provider: ImageProvider) => {
105
+ const providerModels = getImageModelsByProvider(provider);
106
+ const defaultModelForProvider =
107
+ providerModels[0]?.id ?? DEFAULT_IMAGE_MODEL_ID;
108
+ const newSettings: ImageGeneratorSettings = {
109
+ provider,
110
+ modelId: defaultModelForProvider,
111
+ };
112
+ await saveSettings(newSettings);
113
+ },
114
+ [saveSettings],
115
+ );
116
+
117
+ const updateModel = useCallback(
118
+ async (model: string) => {
119
+ if (typeof model !== 'string') {
120
+ return;
121
+ }
122
+
123
+ const selectedModel = getImageModelById(model as ImageModelId);
124
+ if (!selectedModel) {
125
+ console.warn(
126
+ `[useImageGeneratorSettings] Unsupported model "${model}" received. Reverting to default.`,
127
+ );
128
+ await saveSettings(DEFAULT_SETTINGS);
129
+ return;
130
+ }
131
+
132
+ const provider =
133
+ settings.provider === selectedModel.provider
134
+ ? settings.provider
135
+ : selectedModel.provider;
136
+
137
+ const newSettings: ImageGeneratorSettings = {
138
+ provider,
139
+ modelId: selectedModel.id,
140
+ };
141
+ await saveSettings(newSettings);
142
+ },
143
+ [settings.provider, saveSettings],
144
+ );
145
+
146
+ return {
147
+ settings,
148
+ isLoaded,
149
+ updateProvider,
150
+ updateModel,
151
+ };
152
+ };