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,129 @@
1
+ import React from 'react';
2
+ import { ScrollView, View, type ViewStyle } from 'react-native';
3
+
4
+ type TableRendererProps = {
5
+ node: any;
6
+ children: React.ReactNode;
7
+ styles: Record<string, any>;
8
+ tableOuterStyle: ViewStyle;
9
+ tableScrollContentStyle: ViewStyle;
10
+ tableScrollViewStyle: ViewStyle;
11
+ columnDividerWidth: number;
12
+ };
13
+
14
+ /**
15
+ * TableRenderer component for rendering markdown tables with intelligent column sizing.
16
+ *
17
+ * Features:
18
+ * - Horizontal scrolling for wide tables
19
+ * - Intelligent column width sizing based on content
20
+ * - Proper border handling for first/last cells
21
+ * - Flexible widths that fit screen without excessive scrolling
22
+ *
23
+ * Requirements addressed:
24
+ * - 10.1: Calculate optimal column widths based on content length
25
+ * - 10.2: Use flexible widths for tables that fit screen
26
+ * - 10.3: Set dynamic widths proportional to content length
27
+ * - 10.4: Enable horizontal scroll when table exceeds screen width
28
+ * - 10.5: Balance column widths to prevent single column domination
29
+ *
30
+ * @param props - TableRenderer component props
31
+ * @returns Rendered table with proper styling and scrolling
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * <TableRenderer
36
+ * node={markdownNode}
37
+ * children={tableChildren}
38
+ * styles={markdownStyles}
39
+ * tableOuterStyle={styles.tableOuter}
40
+ * tableScrollContentStyle={styles.scrollContent}
41
+ * tableScrollViewStyle={styles.scrollView}
42
+ * columnDividerWidth={0.5}
43
+ * />
44
+ * ```
45
+ */
46
+ export function TableRenderer({
47
+ node,
48
+ children,
49
+ styles,
50
+ tableOuterStyle,
51
+ tableScrollContentStyle,
52
+ tableScrollViewStyle,
53
+ columnDividerWidth,
54
+ }: TableRendererProps) {
55
+ const sections = React.Children.map(children, (section) => {
56
+ if (!React.isValidElement(section)) {
57
+ return section;
58
+ }
59
+
60
+ const sectionProps = section.props as any;
61
+ const rawRows = React.Children.toArray(sectionProps.children);
62
+ const processedRows = rawRows.map((rowChild, rowIndex) => {
63
+ if (!React.isValidElement(rowChild)) {
64
+ return rowChild;
65
+ }
66
+
67
+ const rowChildProps = rowChild.props as any;
68
+ const rowChildrenArray = React.Children.toArray(rowChildProps.children);
69
+ const processedCells = rowChildrenArray.map((cellChild, cellIndex) => {
70
+ if (!React.isValidElement(cellChild)) {
71
+ return cellChild;
72
+ }
73
+
74
+ // Type assertion for props with style
75
+ const cellProps = cellChild.props as any;
76
+ const baseStyle = Array.isArray(cellProps.style)
77
+ ? cellProps.style
78
+ : [cellProps.style];
79
+ const cellStyle = [
80
+ ...baseStyle,
81
+ cellIndex === 0
82
+ ? { borderLeftWidth: 0 }
83
+ : { borderLeftWidth: columnDividerWidth },
84
+ cellIndex === rowChildrenArray.length - 1
85
+ ? { borderRightWidth: 0 }
86
+ : null,
87
+ ].filter(Boolean);
88
+
89
+ return React.cloneElement(cellChild as any, {
90
+ style: cellStyle,
91
+ });
92
+ });
93
+
94
+ // Type assertion for props with style
95
+ const rowProps = rowChild.props as any;
96
+ const baseRowStyle = Array.isArray(rowProps.style)
97
+ ? rowProps.style
98
+ : [rowProps.style];
99
+ const rowStyle = [
100
+ ...baseRowStyle,
101
+ rowIndex === rawRows.length - 1 ? { borderBottomWidth: 0 } : null,
102
+ ].filter(Boolean);
103
+
104
+ return React.cloneElement(
105
+ rowChild as any,
106
+ {
107
+ style: rowStyle,
108
+ },
109
+ processedCells,
110
+ );
111
+ });
112
+
113
+ return React.cloneElement(section, undefined, processedRows);
114
+ });
115
+
116
+ return (
117
+ <View key={node.key} style={tableOuterStyle}>
118
+ <ScrollView
119
+ horizontal
120
+ showsHorizontalScrollIndicator={false}
121
+ contentContainerStyle={tableScrollContentStyle}
122
+ style={tableScrollViewStyle}
123
+ nestedScrollEnabled={true}
124
+ >
125
+ <View style={styles.table}>{sections}</View>
126
+ </ScrollView>
127
+ </View>
128
+ );
129
+ }
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Text } from '@/components/ui';
5
+
6
+ type MessageErrorBoundaryProps = {
7
+ children: React.ReactNode;
8
+ messageId: string;
9
+ };
10
+
11
+ type MessageErrorBoundaryState = {
12
+ hasError: boolean;
13
+ error?: Error;
14
+ };
15
+
16
+ /**
17
+ * Error boundary for individual chat messages.
18
+ * Prevents one broken message from crashing the entire chat.
19
+ *
20
+ * Features:
21
+ * - Catches rendering errors in message components
22
+ * - Shows fallback UI for broken messages
23
+ * - Logs error details for debugging
24
+ * - Resets when message changes
25
+ *
26
+ * @example
27
+ * ```tsx
28
+ * <MessageErrorBoundary messageId={message.id}>
29
+ * <ChatMessageBubble message={message} />
30
+ * </MessageErrorBoundary>
31
+ * ```
32
+ */
33
+ export class MessageErrorBoundary extends React.Component<
34
+ MessageErrorBoundaryProps,
35
+ MessageErrorBoundaryState
36
+ > {
37
+ constructor(props: MessageErrorBoundaryProps) {
38
+ super(props);
39
+ this.state = { hasError: false };
40
+ }
41
+
42
+ static getDerivedStateFromError(error: Error): MessageErrorBoundaryState {
43
+ return { hasError: true, error };
44
+ }
45
+
46
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
47
+ console.error('[MessageErrorBoundary] Message rendering error:', {
48
+ messageId: this.props.messageId,
49
+ error: error.message,
50
+ stack: error.stack,
51
+ componentStack: errorInfo.componentStack,
52
+ });
53
+ }
54
+
55
+ componentDidUpdate(prevProps: MessageErrorBoundaryProps) {
56
+ // Reset error state when message changes
57
+ if (prevProps.messageId !== this.props.messageId && this.state.hasError) {
58
+ this.setState({ hasError: false, error: undefined });
59
+ }
60
+ }
61
+
62
+ render() {
63
+ if (this.state.hasError) {
64
+ return (
65
+ <View className="my-2 rounded-lg border border-destructive/50 bg-destructive/10 p-4">
66
+ <Text className="text-sm font-medium text-destructive">
67
+ Failed to render message
68
+ </Text>
69
+ <Text className="mt-1 text-xs text-muted-foreground">
70
+ {this.state.error?.message || 'Unknown error'}
71
+ </Text>
72
+ </View>
73
+ );
74
+ }
75
+
76
+ return this.props.children;
77
+ }
78
+ }
@@ -0,0 +1,170 @@
1
+ import type { FlashListRef } from '@shopify/flash-list';
2
+ import { FlashList } from '@shopify/flash-list';
3
+ import React, { useCallback, useEffect, useRef } from 'react';
4
+ import { Keyboard, View } from 'react-native';
5
+
6
+ import { ChatMessageBubble } from '@/features/chatbot/components/chat-message-bubble';
7
+ import { MessageErrorBoundary } from '@/features/chatbot/components/message-error-boundary';
8
+ import { useSmartScrollManager } from '@/features/chatbot/hooks/use-smart-scroll-manager';
9
+ import type { AppMessage } from '@/features/chatbot/types';
10
+
11
+ type MessageListProps = {
12
+ messages: AppMessage[];
13
+ onReportMessage?: (message: AppMessage) => void;
14
+ testID?: string;
15
+ };
16
+
17
+ /**
18
+ * Component that renders the scrollable list of chat messages.
19
+ * Uses FlashList for optimal performance with large message lists.
20
+ * Auto-scrolls to bottom when new messages are added and on initial load.
21
+ */
22
+ export const MessageList: React.FC<MessageListProps> = ({
23
+ messages,
24
+ onReportMessage,
25
+ testID = 'message-list',
26
+ }) => {
27
+ const flashListRef = useRef<FlashListRef<AppMessage> | null>(null);
28
+ const previousMessageIdsRef = useRef<string[]>([]);
29
+
30
+ // Use smart scroll manager hook
31
+ const scrollManager = useSmartScrollManager(flashListRef);
32
+
33
+ const attachFlashListRef = useCallback(
34
+ (instance: FlashListRef<AppMessage> | null) => {
35
+ flashListRef.current = instance;
36
+ },
37
+ [],
38
+ );
39
+
40
+ const handleTouchStart = useCallback(() => {
41
+ Keyboard.dismiss();
42
+ }, []);
43
+
44
+ // Auto-scroll to bottom when appropriate
45
+ useEffect(() => {
46
+ if (messages.length === 0) {
47
+ scrollManager.markNearBottom();
48
+ previousMessageIdsRef.current = [];
49
+ return;
50
+ }
51
+
52
+ const lastMessage = messages[messages.length - 1];
53
+ const previousIds = [...previousMessageIdsRef.current];
54
+ const prevLastId = previousIds[previousIds.length - 1];
55
+ const newMessageAppended = lastMessage?.id && lastMessage.id !== prevLastId;
56
+ const userSentMessage = newMessageAppended && lastMessage?.role === 'user';
57
+
58
+ // Check if we should auto-scroll
59
+ // Always scroll on user message, otherwise only if near bottom and not dragging
60
+ const shouldStickToBottom = scrollManager.shouldAutoScroll(userSentMessage);
61
+
62
+ if (shouldStickToBottom) {
63
+ // Only schedule scroll if:
64
+ // 1. New message was appended (user or assistant)
65
+ // 2. OR we're streaming (last message is assistant and content is updating)
66
+ const isStreaming =
67
+ lastMessage?.role === 'assistant' &&
68
+ lastMessage.id.startsWith('stream-');
69
+
70
+ if (newMessageAppended || isStreaming) {
71
+ scrollManager.scheduleScroll();
72
+ }
73
+ }
74
+
75
+ previousMessageIdsRef.current = messages.map((message) => message.id);
76
+ }, [messages, scrollManager]);
77
+
78
+ // Render individual message with error boundary
79
+ const renderMessage = React.useCallback(
80
+ ({ item }: { item: AppMessage }) => {
81
+ return (
82
+ <MessageErrorBoundary messageId={item.id}>
83
+ <ChatMessageBubble message={item} onReportMessage={onReportMessage} />
84
+ </MessageErrorBoundary>
85
+ );
86
+ },
87
+ [onReportMessage],
88
+ );
89
+
90
+ // Get item type for better height estimation per message type
91
+ // FlashList will maintain separate height estimates for each type
92
+ const getItemType = useCallback((item: AppMessage) => {
93
+ const hasImages = (item.attachments?.length ?? 0) > 0;
94
+ const contentLength =
95
+ typeof item.content === 'string' ? item.content.length : 0;
96
+ const isLongMessage = contentLength > 500;
97
+ const role = item.role;
98
+
99
+ // Types: user-short, user-long, user-img, assistant-short, assistant-long, assistant-img
100
+ return `${role}-${hasImages ? 'img' : isLongMessage ? 'long' : 'short'}`;
101
+ }, []);
102
+
103
+ // Calculate better estimated item size based on message type
104
+ // Note: FlashList will use this for better height estimation
105
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
106
+ const estimatedItemSize = useCallback((item: AppMessage, index: number) => {
107
+ const hasImages = (item.attachments?.length ?? 0) > 0;
108
+ const contentLength =
109
+ typeof item.content === 'string' ? item.content.length : 0;
110
+
111
+ // Base height
112
+ let estimate = 80;
113
+
114
+ // Add height for images (fixed aspect ratio 1.5, width 192px = height ~128px)
115
+ if (hasImages) {
116
+ estimate += item.attachments!.length * 150; // 128px image + margins
117
+ }
118
+
119
+ // Add height for text content (rough estimate: ~20px per 50 chars)
120
+ if (contentLength > 0) {
121
+ const estimatedLines = Math.ceil(contentLength / 50);
122
+ estimate += estimatedLines * 20;
123
+ }
124
+
125
+ // Assistant messages with markdown tend to be taller
126
+ if (item.role === 'assistant' && contentLength > 200) {
127
+ estimate *= 1.3; // 30% buffer for markdown formatting
128
+ }
129
+
130
+ return estimate;
131
+ }, []);
132
+
133
+ return (
134
+ <View className="flex-1 px-4" testID={testID}>
135
+ {messages.length === 0 ? (
136
+ <View
137
+ className="flex-1 items-center justify-center px-6"
138
+ onTouchStart={handleTouchStart}
139
+ >
140
+ {/* <Text className="text-center text-base text-muted-foreground">
141
+ {translate('chatbot.start_conversation')}
142
+ </Text> */}
143
+ </View>
144
+ ) : (
145
+ <FlashList<AppMessage>
146
+ ref={attachFlashListRef}
147
+ data={messages}
148
+ renderItem={renderMessage}
149
+ keyExtractor={(item: AppMessage) => item.id}
150
+ getItemType={getItemType}
151
+ drawDistance={1000}
152
+ removeClippedSubviews={false}
153
+ onTouchStart={handleTouchStart}
154
+ onScroll={scrollManager.onScroll}
155
+ onScrollBeginDrag={scrollManager.onScrollBeginDrag}
156
+ onScrollEndDrag={scrollManager.onScrollEndDrag}
157
+ onMomentumScrollBegin={scrollManager.onMomentumScrollBegin}
158
+ onMomentumScrollEnd={scrollManager.onMomentumScrollEnd}
159
+ scrollEventThrottle={16}
160
+ keyboardShouldPersistTaps="handled"
161
+ keyboardDismissMode="on-drag"
162
+ contentContainerStyle={{ paddingTop: 50, paddingBottom: 8 }}
163
+ accessibilityLabel="Chat messages"
164
+ accessibilityRole="list"
165
+ testID="messages-flashlist"
166
+ />
167
+ )}
168
+ </View>
169
+ );
170
+ };
@@ -0,0 +1,283 @@
1
+ import { Feather } from '@expo/vector-icons';
2
+ import type React from 'react';
3
+ import { Alert, 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 type { ModelType, Provider } from '@/features/chatbot/models';
9
+ import {
10
+ getAllProviders,
11
+ getModelById,
12
+ getModelsByProvider,
13
+ getProviderDisplayName,
14
+ getProviderIcon,
15
+ isPremiumModel,
16
+ } from '@/features/chatbot/models';
17
+ import { useEntitlement } from '@/features/payments/hooks/use-entitlement';
18
+ import { useThemeConfig } from '@/lib/use-theme-config';
19
+
20
+ type ModelSelectorProps = {
21
+ selectedModel: ModelType;
22
+ onModelSelect: (model: ModelType) => void;
23
+ testID?: string;
24
+ };
25
+
26
+ /**
27
+ * Model selector component that displays available AI models in a beautiful dropdown
28
+ * Replicates the SuperGrok interface design with provider grouping
29
+ */
30
+ export const ModelSelector: React.FC<ModelSelectorProps> = ({
31
+ selectedModel,
32
+ onModelSelect,
33
+ testID = 'model-selector',
34
+ }) => {
35
+ const theme = useThemeConfig();
36
+ const modal = useModal();
37
+ const { isEntitled: hasPremium } = useEntitlement('premium_access');
38
+
39
+ // Get the current model's display info
40
+ const getCurrentModelInfo = () => {
41
+ const model = getModelById(selectedModel);
42
+ if (model) {
43
+ const IconComponent = getProviderIcon(model.provider);
44
+ return {
45
+ label: model.name,
46
+ provider: model.provider,
47
+ icon: <IconComponent />,
48
+ };
49
+ }
50
+
51
+ // Fallback to OpenAI
52
+ const DefaultIcon = getProviderIcon('openai');
53
+ return {
54
+ label: 'Select Model',
55
+ provider: 'openai' as Provider,
56
+ icon: <DefaultIcon />,
57
+ };
58
+ };
59
+
60
+ const currentModel = getCurrentModelInfo();
61
+
62
+ const handleModelSelect = (model: ModelType) => {
63
+ // Check if model requires premium access
64
+ if (isPremiumModel(model) && !hasPremium) {
65
+ Alert.alert(
66
+ 'Premium Required',
67
+ 'This model requires a premium subscription. Upgrade to access advanced AI models.',
68
+ [
69
+ { text: 'Cancel', style: 'cancel' },
70
+ {
71
+ text: 'Upgrade',
72
+ onPress: () => {
73
+ /* Navigate to upgrade */
74
+ },
75
+ },
76
+ ],
77
+ );
78
+ return;
79
+ }
80
+
81
+ onModelSelect(model);
82
+ modal.dismiss();
83
+ };
84
+
85
+ return (
86
+ <>
87
+ <Pressable
88
+ onPress={modal.present}
89
+ 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"
90
+ style={{ alignSelf: 'flex-start' }}
91
+ accessibilityLabel={`Current model: ${currentModel.label}`}
92
+ accessibilityHint="Tap to change AI model"
93
+ accessibilityRole="button"
94
+ testID={testID}
95
+ >
96
+ <View className="size-4">{currentModel.icon}</View>
97
+ <Text
98
+ className="max-w-[120px] text-xs font-medium text-neutral-800 dark:text-neutral-200"
99
+ numberOfLines={1}
100
+ >
101
+ {currentModel.label}
102
+ </Text>
103
+ <Feather
104
+ name="chevron-down"
105
+ size={14}
106
+ color={theme.dark ? colors.neutral[400] : colors.neutral[500]}
107
+ />
108
+ </Pressable>
109
+ <Modal
110
+ ref={modal.ref}
111
+ index={0}
112
+ snapPoints={['70%']}
113
+ // title="Choose Model"
114
+ >
115
+ <ScrollView
116
+ className="flex-1"
117
+ showsVerticalScrollIndicator={false}
118
+ contentContainerStyle={{
119
+ paddingHorizontal: 24,
120
+ // paddingVertical: 8,
121
+ }}
122
+ >
123
+ {/* Subtitle */}
124
+ <View className="mb-6">
125
+ <Text className="text-center text-xl font-bold">
126
+ Select the AI model
127
+ </Text>
128
+ </View>
129
+
130
+ {getAllProviders().map((provider) => {
131
+ const IconComponent = getProviderIcon(provider);
132
+ const models = getModelsByProvider(provider);
133
+
134
+ return (
135
+ <View key={provider} className="mb-6">
136
+ <View className="mb-4 flex-row items-center gap-3">
137
+ <View className="size-6">
138
+ <IconComponent />
139
+ </View>
140
+ <Text className="text-lg font-semibold text-neutral-900 dark:text-white">
141
+ {getProviderDisplayName(provider)}
142
+ </Text>
143
+ </View>
144
+ {models.map((model) => (
145
+ <ModelOption
146
+ key={model.id}
147
+ model={{
148
+ label: model.name,
149
+ value: model.id,
150
+ description: model.description,
151
+ }}
152
+ isSelected={selectedModel === model.id}
153
+ onSelect={() => handleModelSelect(model.id)}
154
+ isPremium={isPremiumModel(model.id)}
155
+ hasPremium={hasPremium}
156
+ testID={`${testID}-${model.id}`}
157
+ />
158
+ ))}
159
+ </View>
160
+ );
161
+ })}
162
+ </ScrollView>
163
+ </Modal>
164
+ </>
165
+ );
166
+ };
167
+
168
+ type ModelOptionProps = {
169
+ model: { label: string; value: string | number; description?: string };
170
+ isSelected: boolean;
171
+ onSelect: () => void;
172
+ isPremium: boolean;
173
+ hasPremium: boolean;
174
+ testID?: string;
175
+ };
176
+
177
+ const ModelOption: React.FC<ModelOptionProps> = ({
178
+ model,
179
+ isSelected,
180
+ onSelect,
181
+ isPremium,
182
+ hasPremium,
183
+ testID,
184
+ }) => {
185
+ const theme = useThemeConfig();
186
+ const isDisabled = isPremium && !hasPremium;
187
+
188
+ return (
189
+ <Pressable
190
+ onPress={onSelect}
191
+ disabled={isDisabled}
192
+ className="mb-3 rounded-2xl border p-4"
193
+ style={{
194
+ backgroundColor: isDisabled
195
+ ? theme.dark
196
+ ? 'rgba(51, 65, 85, 0.2)'
197
+ : 'rgba(248, 250, 252, 0.2)'
198
+ : isSelected
199
+ ? theme.dark
200
+ ? 'rgba(59, 130, 246, 0.25)'
201
+ : 'rgba(59, 130, 246, 0.15)'
202
+ : theme.dark
203
+ ? 'rgba(51, 65, 85, 0.2)'
204
+ : 'rgba(248, 250, 252, 0.2)', // 20% opacity for non-selected
205
+ borderColor: isDisabled
206
+ ? theme.dark
207
+ ? colors.neutral[600]
208
+ : colors.neutral[300]
209
+ : isSelected
210
+ ? colors.blue[500]
211
+ : theme.dark
212
+ ? colors.neutral[700]
213
+ : colors.neutral[200],
214
+ borderWidth: isSelected ? 2 : 1,
215
+ shadowColor: isSelected ? colors.blue[500] : colors.neutral[900],
216
+ shadowOffset: { width: 0, height: 2 },
217
+ shadowOpacity: isSelected ? 0.15 : 0.05,
218
+ shadowRadius: 8,
219
+ elevation: isSelected ? 4 : 2,
220
+ opacity: isDisabled ? 0.6 : 1,
221
+ }}
222
+ accessibilityLabel={`${model.label}: ${model.description || ''}${isPremium ? ' (Premium)' : ''}`}
223
+ accessibilityHint={
224
+ isDisabled
225
+ ? 'Premium required'
226
+ : isSelected
227
+ ? 'Currently selected'
228
+ : 'Tap to select'
229
+ }
230
+ accessibilityRole="button"
231
+ testID={testID}
232
+ >
233
+ <View className="flex-row items-center justify-between">
234
+ <View className="flex-1">
235
+ <View className="flex-row items-center gap-2">
236
+ <Text
237
+ className={`text-base font-bold ${
238
+ isDisabled
239
+ ? 'text-neutral-500 dark:text-neutral-500'
240
+ : isSelected
241
+ ? 'text-blue-700 dark:text-blue-300'
242
+ : 'text-neutral-900 dark:text-white'
243
+ }`}
244
+ >
245
+ {model.label}
246
+ </Text>
247
+ {isPremium && (
248
+ <View className="rounded-full bg-warning-500 px-2 py-0.5">
249
+ <Text className="text-xs font-bold text-white">PRO</Text>
250
+ </View>
251
+ )}
252
+ </View>
253
+ <Text
254
+ className={`mt-1 text-sm ${
255
+ isDisabled
256
+ ? 'text-neutral-500 dark:text-neutral-500'
257
+ : isSelected
258
+ ? 'text-blue-600 dark:text-blue-400'
259
+ : 'text-neutral-600 dark:text-neutral-400'
260
+ }`}
261
+ >
262
+ {model.description || 'AI model'}
263
+ </Text>
264
+ {isDisabled && (
265
+ <Text className="mt-1 text-xs text-warning-600 dark:text-warning-400">
266
+ Upgrade to Premium to access this model
267
+ </Text>
268
+ )}
269
+ </View>
270
+ {isSelected && !isDisabled && (
271
+ <View className="ml-3 rounded-full bg-blue-500 p-1">
272
+ <Feather name="check" size={16} color={colors.white} />
273
+ </View>
274
+ )}
275
+ {isDisabled && (
276
+ <View className="ml-3">
277
+ <Feather name="lock" size={20} color={colors.neutral[400]} />
278
+ </View>
279
+ )}
280
+ </View>
281
+ </Pressable>
282
+ );
283
+ };