vibefast-cli 1.1.5 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +63 -169
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +547 -589
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/checklist.d.ts +1 -1
- package/dist/commands/checklist.d.ts.map +1 -1
- package/dist/commands/checklist.js +40 -39
- package/dist/commands/checklist.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +22 -22
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/env.d.ts +1 -1
- package/dist/commands/env.d.ts.map +1 -1
- package/dist/commands/env.js +58 -53
- package/dist/commands/env.js.map +1 -1
- package/dist/commands/health.d.ts +1 -1
- package/dist/commands/health.d.ts.map +1 -1
- package/dist/commands/health.js +101 -93
- package/dist/commands/health.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +416 -296
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/remove.d.ts +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +77 -64
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/status.d.ts +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +15 -14
- package/dist/commands/status.js.map +1 -1
- package/dist/core/__tests__/detect.test.js +68 -34
- package/dist/core/__tests__/detect.test.js.map +1 -1
- package/dist/core/ast.d.ts +14 -0
- package/dist/core/ast.d.ts.map +1 -0
- package/dist/core/ast.js +239 -0
- package/dist/core/ast.js.map +1 -0
- package/dist/core/codemod.d.ts.map +1 -1
- package/dist/core/codemod.js +62 -44
- package/dist/core/codemod.js.map +1 -1
- package/dist/core/config.d.ts +10 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +51 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/detect.d.ts +8 -2
- package/dist/core/detect.d.ts.map +1 -1
- package/dist/core/detect.js +52 -21
- package/dist/core/detect.js.map +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -8
- package/dist/core/errors.js.map +1 -1
- package/dist/core/exec.d.ts +16 -0
- package/dist/core/exec.d.ts.map +1 -0
- package/dist/core/exec.js +48 -0
- package/dist/core/exec.js.map +1 -0
- package/dist/core/manualSteps.d.ts +7 -0
- package/dist/core/manualSteps.d.ts.map +1 -0
- package/dist/core/manualSteps.js +59 -0
- package/dist/core/manualSteps.js.map +1 -0
- package/dist/core/paths.d.ts +3 -1
- package/dist/core/paths.d.ts.map +1 -1
- package/dist/core/paths.js +14 -10
- package/dist/core/paths.js.map +1 -1
- package/dist/core/spinner.d.ts +1 -1
- package/dist/core/spinner.d.ts.map +1 -1
- package/dist/core/spinner.js +38 -8
- package/dist/core/spinner.js.map +1 -1
- package/dist/core/vosk.d.ts.map +1 -1
- package/dist/core/vosk.js +50 -39
- package/dist/core/vosk.js.map +1 -1
- package/docs/manual-testing.md +91 -0
- package/package.json +6 -3
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/recipe.json +3 -3
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder-supabase/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder-supabase/packages/backend/src/services/recordings.ts +369 -0
- package/recipes/audio-recorder-supabase/packages/backend/supabase/migrations/recordings.sql +70 -0
- package/recipes/audio-recorder-supabase/recipe.json +35 -0
- package/recipes/audio-recorder-supabase@latest.zip +0 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +2 -2
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +5 -5
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +3 -3
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +20 -4
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +7 -6
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +6 -4
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +1 -1
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +5 -4
- package/recipes/charts/recipe.json +4 -13
- package/recipes/charts@latest.zip +0 -0
- package/recipes/chatbot/apps/native/src/app/chatbot/index.tsx +1 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +86 -86
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +86 -53
- package/recipes/chatbot/recipe.json +26 -92
- package/recipes/chatbot-supabase/apps/native/src/api-client/supabase/chatbot.ts +515 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/app/index.tsx +257 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-input-bar.tsx +485 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +223 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/image-preview-list.tsx +116 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/message-list.tsx +170 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +142 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-config.ts +458 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +429 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-conversation.ts +90 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +213 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/file-uploader.ts +287 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/services/message-handler-service.ts +189 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/types/index.ts +70 -0
- package/recipes/chatbot-supabase/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/conversations.ts +243 -0
- package/recipes/chatbot-supabase/packages/backend/src/services/messages.ts +327 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/functions/chat-stream/index.ts +347 -0
- package/recipes/chatbot-supabase/packages/backend/supabase/migrations/chatbot.sql +104 -0
- package/recipes/chatbot-supabase/recipe.json +79 -0
- package/recipes/chatbot-supabase@latest.zip +0 -0
- package/recipes/chatbot.zip +0 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis/index.ts +2 -2
- package/recipes/image-analysis/packages/backend/convex/imageAnalysis.ts +0 -1
- package/recipes/image-analysis/recipe.json +15 -55
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/analysis-options-screen.tsx +304 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/camera.tsx +221 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/image-capture-screen.tsx +333 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading-screen.tsx +214 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/loading.tsx +191 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/results.tsx +137 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/trait-details.tsx +172 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-analysis-data.ts +160 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/app/use-results-screen.ts +151 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-badge.tsx +77 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-card.tsx +75 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievement-unlocked-modal.tsx +162 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/achievements-section.tsx +44 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/advice-list.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/circular-progress.tsx +233 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/content-card.tsx +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/error-state.tsx +42 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/index.ts +9 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/loading-state.tsx +26 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/profile-image.tsx +60 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/results-header.tsx +62 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/score-display.tsx +54 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/share-options-modal.tsx +110 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/components/results/traits-grid.tsx +74 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/analysis-config.ts +80 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/config/master-analysis-config.ts +157 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/index.ts +1 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-analysis.ts +38 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/hooks/use-image-analysis.ts +208 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/analysis-service.ts +262 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/share-service.ts +176 -0
- package/recipes/image-analysis-supabase/apps/native/src/features/image-analyzer/services/trait-details-service.ts +289 -0
- package/recipes/image-analysis-supabase/packages/backend/src/services/image-analyses.ts +132 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/functions/analyze-image/index.ts +312 -0
- package/recipes/image-analysis-supabase/packages/backend/supabase/migrations/image_analysis.sql +42 -0
- package/recipes/image-analysis-supabase/recipe.json +57 -0
- package/recipes/image-analysis-supabase@latest.zip +0 -0
- package/recipes/image-analysis@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +16 -2
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +11 -5
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +11 -5
- package/recipes/image-generator/packages/backend/convex/imageGeneration/index.ts +2 -2
- package/recipes/image-generator/recipe.json +16 -39
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/_layout.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/app/index.tsx +251 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/gallery-image.tsx +25 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-model-selector.tsx +216 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/hooks/use-image-generator.ts +103 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-gallery-service.ts +96 -0
- package/recipes/image-generator-supabase/apps/native/src/features/image-generator/services/image-save-service.ts +120 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/functions/generate-image/index.ts +291 -0
- package/recipes/image-generator-supabase/packages/backend/supabase/migrations/image_generator.sql +71 -0
- package/recipes/image-generator-supabase/recipe.json +59 -0
- package/recipes/image-generator-supabase@latest.zip +0 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/ios-widget/recipe.json +15 -24
- package/recipes/ios-widget@latest.zip +0 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/analytics/index.ts +9 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding-with-analytics.tsx +141 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/components/onboarding.tsx +173 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/config/onboarding-flow-config.ts +189 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/app/index.tsx +42 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/demo-one/data.ts +32 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/app/index.tsx +43 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/interactive-onboarding.tsx +222 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/ai-tone-step.tsx +133 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/currency-step.tsx +165 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-ai-step.tsx +199 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-chatbot-step.tsx +154 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-manual-step.tsx +156 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/feature-scan-step.tsx +158 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/main-reason-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/notification-step.tsx +129 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/overspend-step.tsx +138 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/personalizing-step.tsx +190 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/rating-step.tsx +98 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/reminder-step.tsx +181 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/safety-step.tsx +110 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/struggle-step.tsx +139 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/steps/welcome-step.tsx +217 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/components/ui/onboarding-header.tsx +58 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/expense-tracker/constants.ts +179 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/hooks/use-onboarding-analytics.ts +323 -0
- package/recipes/onboarding/apps/native/src/features/onboarding/services/onboarding-analytics.ts +432 -0
- package/recipes/onboarding/recipe.json +15 -0
- package/recipes/onboarding@latest.zip +0 -0
- package/recipes/payments/recipe.json +28 -61
- package/recipes/payments-supabase/apps/native/src/features/payments/README.md +200 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/local-paywall.tsx +194 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/app/remote-paywall.tsx +79 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/payment-initializer.tsx +95 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-error-state.tsx +60 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-local-mode.tsx +116 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-product-card.tsx +133 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/components/paywall-remote-mode.tsx +146 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/hooks/use-entitlement.ts +63 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/index.ts +8 -0
- package/recipes/payments-supabase/apps/native/src/features/payments/services/revenuecat-adapter.ts +407 -0
- package/recipes/payments-supabase/packages/backend/src/services/payments.ts +201 -0
- package/recipes/payments-supabase/packages/backend/supabase/migrations/payments.sql +35 -0
- package/recipes/payments-supabase/recipe.json +51 -0
- package/recipes/payments-supabase@latest.zip +0 -0
- package/recipes/payments@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +1 -2
- package/recipes/quiz/recipe.json +6 -9
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +1 -2
- package/recipes/tracker-app/recipe.json +7 -10
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/voice-bot/recipe.json +8 -68
- package/recipes/voice-bot.zip +0 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/recipes/wake-word/recipe.json +10 -9
- package/recipes/wake-word.zip +0 -0
- package/recipes/wake-word@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/(root)/(protected)/charts/index.tsx +0 -3
- package/recipes/chatbot/packages/backend/convex/lib/rateLimit.ts +0 -100
- package/recipes/chatbot/packages/backend/convex/lib/telemetry.ts +0 -29
- package/recipes/chatbot/packages/backend/convex/ragKnowledge.ts +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/front.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/side.jpg +0 -0
- package/recipes/image-analysis/apps/native/assets/features/image-analyzer/threeQuarter.jpg +0 -0
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/_layout.tsx +0 -5
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/analysis-options.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/camera.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/index.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/loading.tsx +0 -50
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/results.tsx +0 -2
- package/recipes/image-analysis/apps/native/src/app/(root)/(protected)/analysis/[type]/trait-details.tsx +0 -3
- package/recipes/image-analysis/packages/backend/convex/imageAnalysisFunctions.ts +0 -325
- package/recipes/image-analysis/packages/backend/convex/lib/ai/imageAnalysisAdapter.ts +0 -200
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/index.tsx +0 -74
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/local.tsx +0 -25
- package/recipes/payments/apps/native/src/app/(root)/(protected)/paywall/remote.tsx +0 -23
- package/recipes/quiz/apps/native/src/app/(root)/(protected)/quiz/index.tsx +0 -47
- package/recipes/tracker-app/apps/native/src/app/(root)/(protected)/tracker-app/index.tsx +0 -1
- package/recipes/voice-bot/apps/native/src/app/(root)/(protected)/voice-bot/index.tsx +0 -27
- package/recipes/voice-bot/packages/backend/convex/router.ts +0 -81
- /package/recipes/{chatbot/apps/native/src/app/(root)/(protected) → chatbot-supabase/apps/native/src/app}/chatbot/index.tsx +0 -0
- /package/recipes/{image-generator/apps/native/src/app/(root)/(protected) → image-generator-supabase/apps/native/src/app}/image-generator/gallery.tsx +0 -0
- /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,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messages Service
|
|
3
|
+
*
|
|
4
|
+
* Handles message operations including storeMessage, listMessages,
|
|
5
|
+
* deleteUserMessage, and clearConversationMessages.
|
|
6
|
+
*
|
|
7
|
+
* Requirements: 4.3, 4.4, 4.5, 4.6, 4.7
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { SupabaseClientType } from '../lib/supabase';
|
|
11
|
+
import type { Message } from '../types';
|
|
12
|
+
import type { Json } from '../types/database';
|
|
13
|
+
|
|
14
|
+
export interface StoreMessageInput {
|
|
15
|
+
conversationId: string;
|
|
16
|
+
authorType: 'user' | 'bot';
|
|
17
|
+
text?: string | null;
|
|
18
|
+
attachments?: Json | null;
|
|
19
|
+
aiProvider?: string | null;
|
|
20
|
+
aiModel?: string | null;
|
|
21
|
+
toolCalls?: Json | null;
|
|
22
|
+
metadata?: Json | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface StoreMessageResult {
|
|
26
|
+
success: boolean;
|
|
27
|
+
message?: Message;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ListMessagesResult {
|
|
32
|
+
success: boolean;
|
|
33
|
+
messages?: Message[];
|
|
34
|
+
hasMore?: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeleteMessageResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ClearMessagesResult {
|
|
44
|
+
success: boolean;
|
|
45
|
+
deletedCount?: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
50
|
+
const BATCH_DELETE_SIZE = 100;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Store a new message in a conversation
|
|
54
|
+
*
|
|
55
|
+
* Validates that the message has either text or attachments (not both empty).
|
|
56
|
+
*
|
|
57
|
+
* @param supabase - Supabase client (authenticated)
|
|
58
|
+
* @param input - Message data to store
|
|
59
|
+
* @returns The created message
|
|
60
|
+
*
|
|
61
|
+
* Requirements: 4.3, 4.4
|
|
62
|
+
*/
|
|
63
|
+
export async function storeMessage(
|
|
64
|
+
supabase: SupabaseClientType,
|
|
65
|
+
input: StoreMessageInput
|
|
66
|
+
): Promise<StoreMessageResult> {
|
|
67
|
+
// Get the current auth user
|
|
68
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
69
|
+
|
|
70
|
+
if (authError || !authData.user) {
|
|
71
|
+
return { success: false, error: 'Not authenticated' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const userId = authData.user.id;
|
|
75
|
+
|
|
76
|
+
// Validate input
|
|
77
|
+
if (!input.conversationId) {
|
|
78
|
+
return { success: false, error: 'Conversation ID is required' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check if text is empty/whitespace and no attachments
|
|
82
|
+
const hasText = input.text && input.text.trim().length > 0;
|
|
83
|
+
const hasAttachments = input.attachments &&
|
|
84
|
+
(Array.isArray(input.attachments) ? input.attachments.length > 0 : true);
|
|
85
|
+
|
|
86
|
+
if (!hasText && !hasAttachments) {
|
|
87
|
+
return { success: false, error: 'Message must have text or attachments' };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Verify user owns the conversation
|
|
91
|
+
const { data: conversation, error: convError } = await supabase
|
|
92
|
+
.from('conversations')
|
|
93
|
+
.select('id')
|
|
94
|
+
.eq('id', input.conversationId)
|
|
95
|
+
.eq('user_id', userId)
|
|
96
|
+
.single();
|
|
97
|
+
|
|
98
|
+
if (convError || !conversation) {
|
|
99
|
+
return { success: false, error: 'Conversation not found or access denied' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Create the message
|
|
103
|
+
const { data: message, error: createError } = await supabase
|
|
104
|
+
.from('messages')
|
|
105
|
+
.insert({
|
|
106
|
+
conversation_id: input.conversationId,
|
|
107
|
+
user_id: userId,
|
|
108
|
+
author_type: input.authorType,
|
|
109
|
+
text: input.text || null,
|
|
110
|
+
attachments: input.attachments || null,
|
|
111
|
+
ai_provider: input.aiProvider || null,
|
|
112
|
+
ai_model: input.aiModel || null,
|
|
113
|
+
tool_calls: input.toolCalls || null,
|
|
114
|
+
metadata: input.metadata || null,
|
|
115
|
+
})
|
|
116
|
+
.select()
|
|
117
|
+
.single();
|
|
118
|
+
|
|
119
|
+
if (createError) {
|
|
120
|
+
return { success: false, error: `Failed to store message: ${createError.message}` };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: true, message };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* List messages for a conversation with pagination
|
|
129
|
+
*
|
|
130
|
+
* Returns messages ordered by creation time (oldest first for chat display).
|
|
131
|
+
*
|
|
132
|
+
* @param supabase - Supabase client (authenticated)
|
|
133
|
+
* @param conversationId - The conversation to list messages from
|
|
134
|
+
* @param options - Pagination options
|
|
135
|
+
* @returns Paginated list of messages
|
|
136
|
+
*
|
|
137
|
+
* Requirements: 4.5
|
|
138
|
+
*/
|
|
139
|
+
export async function listMessages(
|
|
140
|
+
supabase: SupabaseClientType,
|
|
141
|
+
conversationId: string,
|
|
142
|
+
options?: {
|
|
143
|
+
limit?: number;
|
|
144
|
+
cursor?: string; // created_at timestamp for cursor-based pagination
|
|
145
|
+
}
|
|
146
|
+
): Promise<ListMessagesResult> {
|
|
147
|
+
// Get the current auth user
|
|
148
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
149
|
+
|
|
150
|
+
if (authError || !authData.user) {
|
|
151
|
+
return { success: false, error: 'Not authenticated' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!conversationId) {
|
|
155
|
+
return { success: false, error: 'Conversation ID is required' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const limit = options?.limit || DEFAULT_PAGE_SIZE;
|
|
159
|
+
|
|
160
|
+
// Build query
|
|
161
|
+
let query = supabase
|
|
162
|
+
.from('messages')
|
|
163
|
+
.select('*')
|
|
164
|
+
.eq('conversation_id', conversationId)
|
|
165
|
+
.order('created_at', { ascending: true })
|
|
166
|
+
.limit(limit + 1); // Fetch one extra to check if there are more
|
|
167
|
+
|
|
168
|
+
// Apply cursor if provided
|
|
169
|
+
if (options?.cursor) {
|
|
170
|
+
query = query.gt('created_at', options.cursor);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const { data: messages, error: listError } = await query;
|
|
174
|
+
|
|
175
|
+
if (listError) {
|
|
176
|
+
return { success: false, error: `Failed to list messages: ${listError.message}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if there are more results
|
|
180
|
+
const hasMore = messages && messages.length > limit;
|
|
181
|
+
const resultMessages = hasMore ? messages.slice(0, limit) : (messages || []);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
success: true,
|
|
185
|
+
messages: resultMessages,
|
|
186
|
+
hasMore,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Delete a single user message
|
|
192
|
+
*
|
|
193
|
+
* Only allows deletion if the user owns the message.
|
|
194
|
+
*
|
|
195
|
+
* @param supabase - Supabase client (authenticated)
|
|
196
|
+
* @param messageId - The ID of the message to delete
|
|
197
|
+
* @returns Success or error
|
|
198
|
+
*
|
|
199
|
+
* Requirements: 4.7
|
|
200
|
+
*/
|
|
201
|
+
export async function deleteUserMessage(
|
|
202
|
+
supabase: SupabaseClientType,
|
|
203
|
+
messageId: string
|
|
204
|
+
): Promise<DeleteMessageResult> {
|
|
205
|
+
// Get the current auth user
|
|
206
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
207
|
+
|
|
208
|
+
if (authError || !authData.user) {
|
|
209
|
+
return { success: false, error: 'Not authenticated' };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!messageId) {
|
|
213
|
+
return { success: false, error: 'Message ID is required' };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Delete the message (RLS + user_id check ensures ownership)
|
|
217
|
+
const { data, error: deleteError } = await supabase
|
|
218
|
+
.from('messages')
|
|
219
|
+
.delete()
|
|
220
|
+
.eq('id', messageId)
|
|
221
|
+
.eq('user_id', authData.user.id)
|
|
222
|
+
.select('id')
|
|
223
|
+
.single();
|
|
224
|
+
|
|
225
|
+
if (deleteError) {
|
|
226
|
+
if (deleteError.code === 'PGRST116') {
|
|
227
|
+
return { success: false, error: 'Message not found or access denied' };
|
|
228
|
+
}
|
|
229
|
+
return { success: false, error: `Failed to delete message: ${deleteError.message}` };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!data) {
|
|
233
|
+
return { success: false, error: 'Message not found or access denied' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return { success: true };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Clear all messages in a conversation
|
|
241
|
+
*
|
|
242
|
+
* Deletes messages in batches to handle large conversations.
|
|
243
|
+
* The conversation itself is preserved.
|
|
244
|
+
*
|
|
245
|
+
* @param supabase - Supabase client (authenticated)
|
|
246
|
+
* @param conversationId - The conversation to clear
|
|
247
|
+
* @returns Success with count of deleted messages
|
|
248
|
+
*
|
|
249
|
+
* Requirements: 4.6
|
|
250
|
+
*/
|
|
251
|
+
export async function clearConversationMessages(
|
|
252
|
+
supabase: SupabaseClientType,
|
|
253
|
+
conversationId: string
|
|
254
|
+
): Promise<ClearMessagesResult> {
|
|
255
|
+
// Get the current auth user
|
|
256
|
+
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
257
|
+
|
|
258
|
+
if (authError || !authData.user) {
|
|
259
|
+
return { success: false, error: 'Not authenticated' };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!conversationId) {
|
|
263
|
+
return { success: false, error: 'Conversation ID is required' };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const userId = authData.user.id;
|
|
267
|
+
|
|
268
|
+
// Verify user owns the conversation
|
|
269
|
+
const { data: conversation, error: convError } = await supabase
|
|
270
|
+
.from('conversations')
|
|
271
|
+
.select('id')
|
|
272
|
+
.eq('id', conversationId)
|
|
273
|
+
.eq('user_id', userId)
|
|
274
|
+
.single();
|
|
275
|
+
|
|
276
|
+
if (convError || !conversation) {
|
|
277
|
+
return { success: false, error: 'Conversation not found or access denied' };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let totalDeleted = 0;
|
|
281
|
+
let hasMore = true;
|
|
282
|
+
|
|
283
|
+
// Delete in batches
|
|
284
|
+
while (hasMore) {
|
|
285
|
+
// Get batch of message IDs
|
|
286
|
+
const { data: messagesToDelete, error: fetchError } = await supabase
|
|
287
|
+
.from('messages')
|
|
288
|
+
.select('id')
|
|
289
|
+
.eq('conversation_id', conversationId)
|
|
290
|
+
.limit(BATCH_DELETE_SIZE);
|
|
291
|
+
|
|
292
|
+
if (fetchError) {
|
|
293
|
+
return { success: false, error: `Failed to fetch messages: ${fetchError.message}` };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!messagesToDelete || messagesToDelete.length === 0) {
|
|
297
|
+
hasMore = false;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const idsToDelete = messagesToDelete.map((m) => m.id);
|
|
302
|
+
|
|
303
|
+
// Delete the batch
|
|
304
|
+
const { error: deleteError } = await supabase
|
|
305
|
+
.from('messages')
|
|
306
|
+
.delete()
|
|
307
|
+
.in('id', idsToDelete);
|
|
308
|
+
|
|
309
|
+
if (deleteError) {
|
|
310
|
+
return { success: false, error: `Failed to delete messages: ${deleteError.message}` };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
totalDeleted += idsToDelete.length;
|
|
314
|
+
|
|
315
|
+
// Check if we got a full batch (might be more)
|
|
316
|
+
hasMore = messagesToDelete.length === BATCH_DELETE_SIZE;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { success: true, deletedCount: totalDeleted };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const messagesService = {
|
|
323
|
+
storeMessage,
|
|
324
|
+
listMessages,
|
|
325
|
+
deleteUserMessage,
|
|
326
|
+
clearConversationMessages,
|
|
327
|
+
};
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat Stream Edge Function
|
|
3
|
+
*
|
|
4
|
+
* Handles AI chat with real-time streaming via Supabase Realtime Broadcast.
|
|
5
|
+
* Broadcasts chunks as they arrive, client subscribes to channel.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import 'jsr:@supabase/functions-js/edge-runtime.d.ts';
|
|
9
|
+
import { createClient } from 'npm:@supabase/supabase-js@2';
|
|
10
|
+
import { streamText } from 'npm:ai@5.0.98';
|
|
11
|
+
import { createOpenAI } from 'npm:@ai-sdk/openai@2.0.71';
|
|
12
|
+
import { createGoogleGenerativeAI } from 'npm:@ai-sdk/google@2.0.40';
|
|
13
|
+
|
|
14
|
+
const corsHeaders = {
|
|
15
|
+
'Access-Control-Allow-Origin': '*',
|
|
16
|
+
'Access-Control-Allow-Headers':
|
|
17
|
+
'authorization, x-client-info, apikey, content-type',
|
|
18
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
interface ChatStreamRequest {
|
|
22
|
+
conversationId: string;
|
|
23
|
+
message: string;
|
|
24
|
+
attachments?: Array<{
|
|
25
|
+
type: string;
|
|
26
|
+
url?: string;
|
|
27
|
+
data?: string;
|
|
28
|
+
mimeType?: string;
|
|
29
|
+
}>;
|
|
30
|
+
preferredModel?: string;
|
|
31
|
+
preferredProvider?: 'openai' | 'google';
|
|
32
|
+
streamId?: string; // Client-provided channel ID for broadcast
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Message {
|
|
36
|
+
id: string;
|
|
37
|
+
author_type: 'user' | 'bot';
|
|
38
|
+
text: string | null;
|
|
39
|
+
created_at: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Deno.serve(async (req: Request) => {
|
|
43
|
+
if (req.method === 'OPTIONS') {
|
|
44
|
+
return new Response('ok', { headers: corsHeaders });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (req.method !== 'POST') {
|
|
48
|
+
return new Response(JSON.stringify({ error: 'Method not allowed' }), {
|
|
49
|
+
status: 405,
|
|
50
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const authHeader = req.headers.get('Authorization');
|
|
56
|
+
if (!authHeader) {
|
|
57
|
+
return new Response(JSON.stringify({ error: 'Missing authorization header' }), {
|
|
58
|
+
status: 401,
|
|
59
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const token = authHeader.replace('Bearer ', '');
|
|
64
|
+
const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
|
|
65
|
+
const supabaseAnonKey = Deno.env.get('SUPABASE_ANON_KEY')!;
|
|
66
|
+
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
67
|
+
|
|
68
|
+
// User client for auth verification
|
|
69
|
+
const supabaseUser = createClient(supabaseUrl, supabaseAnonKey, {
|
|
70
|
+
global: { headers: { Authorization: `Bearer ${token}` } },
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Service client for DB operations (bypasses RLS)
|
|
74
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
75
|
+
|
|
76
|
+
const { data: { user }, error: authError } = await supabaseUser.auth.getUser();
|
|
77
|
+
if (authError || !user) {
|
|
78
|
+
return new Response(JSON.stringify({ error: 'Invalid or expired token' }), {
|
|
79
|
+
status: 401,
|
|
80
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const body: ChatStreamRequest = await req.json();
|
|
85
|
+
const { conversationId, message, attachments, preferredModel, preferredProvider, streamId } = body;
|
|
86
|
+
|
|
87
|
+
if (!conversationId || !message) {
|
|
88
|
+
return new Response(
|
|
89
|
+
JSON.stringify({ error: 'conversationId and message are required' }),
|
|
90
|
+
{
|
|
91
|
+
status: 400,
|
|
92
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify user owns the conversation
|
|
98
|
+
const { data: conversation, error: convError } = await supabase
|
|
99
|
+
.from('conversations')
|
|
100
|
+
.select('id, user_id')
|
|
101
|
+
.eq('id', conversationId)
|
|
102
|
+
.single();
|
|
103
|
+
|
|
104
|
+
if (convError || !conversation) {
|
|
105
|
+
return new Response(JSON.stringify({ error: 'Conversation not found' }), {
|
|
106
|
+
status: 404,
|
|
107
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (conversation.user_id !== user.id) {
|
|
112
|
+
return new Response(JSON.stringify({ error: 'Access denied to conversation' }), {
|
|
113
|
+
status: 403,
|
|
114
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Retrieve conversation history
|
|
119
|
+
const { data: messages, error: msgError } = await supabase
|
|
120
|
+
.from('messages')
|
|
121
|
+
.select('id, author_type, text, created_at')
|
|
122
|
+
.eq('conversation_id', conversationId)
|
|
123
|
+
.order('created_at', { ascending: true })
|
|
124
|
+
.limit(50);
|
|
125
|
+
|
|
126
|
+
if (msgError) {
|
|
127
|
+
console.error('Error fetching messages:', msgError);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Build conversation history for AI - filter out empty messages
|
|
131
|
+
const conversationHistory = (messages || [])
|
|
132
|
+
.filter((msg: Message) => msg.text && msg.text.trim().length > 0)
|
|
133
|
+
.map((msg: Message) => ({
|
|
134
|
+
role: msg.author_type === 'user' ? 'user' : 'assistant',
|
|
135
|
+
content: msg.text!,
|
|
136
|
+
}));
|
|
137
|
+
|
|
138
|
+
// Add the current user message (include attachments as multimodal parts)
|
|
139
|
+
const userParts: Array<
|
|
140
|
+
| { type: 'text'; text: string }
|
|
141
|
+
| { type: 'image'; image: string; mimeType?: string }
|
|
142
|
+
> = [{ type: 'text', text: message }];
|
|
143
|
+
|
|
144
|
+
(attachments || [])
|
|
145
|
+
.filter((att) => att?.url)
|
|
146
|
+
.forEach((att) => {
|
|
147
|
+
userParts.push({
|
|
148
|
+
type: 'image',
|
|
149
|
+
image: att.url!,
|
|
150
|
+
mimeType: att.mimeType || 'image/jpeg',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
conversationHistory.push({
|
|
155
|
+
role: 'user',
|
|
156
|
+
content: userParts,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log('Filtered conversation history:', conversationHistory.length, 'messages');
|
|
160
|
+
|
|
161
|
+
// Model mapping
|
|
162
|
+
const modelMapping: Record<string, { provider: 'openai' | 'google'; model: string }> = {
|
|
163
|
+
'gpt-5': { provider: 'openai', model: 'gpt-4o' },
|
|
164
|
+
'gpt-5-mini': { provider: 'openai', model: 'gpt-4o-mini' },
|
|
165
|
+
'gpt-5-nano': { provider: 'openai', model: 'gpt-4o-mini' },
|
|
166
|
+
'gpt-4o': { provider: 'openai', model: 'gpt-4o' },
|
|
167
|
+
'gpt-4o-mini': { provider: 'openai', model: 'gpt-4o-mini' },
|
|
168
|
+
'gemini-2.0-flash': { provider: 'google', model: 'gemini-2.0-flash' },
|
|
169
|
+
'gemini-2.5-flash': { provider: 'google', model: 'gemini-2.0-flash' },
|
|
170
|
+
'gemini-2.5-pro': { provider: 'google', model: 'gemini-1.5-pro' },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const mappedModel = preferredModel ? modelMapping[preferredModel] : null;
|
|
174
|
+
const provider = mappedModel?.provider || preferredProvider || 'openai';
|
|
175
|
+
let model;
|
|
176
|
+
let aiProvider: string;
|
|
177
|
+
let aiModel: string;
|
|
178
|
+
|
|
179
|
+
if (provider === 'google') {
|
|
180
|
+
const googleApiKey = Deno.env.get('GEMINI_API_KEY');
|
|
181
|
+
if (!googleApiKey) {
|
|
182
|
+
return new Response(JSON.stringify({ error: 'Google AI not configured' }), {
|
|
183
|
+
status: 500,
|
|
184
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const google = createGoogleGenerativeAI({ apiKey: googleApiKey });
|
|
188
|
+
aiModel = mappedModel?.model || preferredModel || 'gemini-2.0-flash';
|
|
189
|
+
model = google(aiModel);
|
|
190
|
+
aiProvider = 'google';
|
|
191
|
+
} else {
|
|
192
|
+
const openaiApiKey = Deno.env.get('OPENAI_API_KEY');
|
|
193
|
+
if (!openaiApiKey) {
|
|
194
|
+
return new Response(JSON.stringify({ error: 'OpenAI not configured' }), {
|
|
195
|
+
status: 500,
|
|
196
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const openai = createOpenAI({ apiKey: openaiApiKey });
|
|
200
|
+
aiModel = mappedModel?.model || preferredModel || 'gpt-4o-mini';
|
|
201
|
+
model = openai(aiModel);
|
|
202
|
+
aiProvider = 'openai';
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// CREATE BOT MESSAGE IMMEDIATELY with is_streaming = true
|
|
206
|
+
const { data: botMessage, error: createError } = await supabase
|
|
207
|
+
.from('messages')
|
|
208
|
+
.insert({
|
|
209
|
+
conversation_id: conversationId,
|
|
210
|
+
user_id: user.id,
|
|
211
|
+
author_type: 'bot',
|
|
212
|
+
text: null,
|
|
213
|
+
streaming_content: '',
|
|
214
|
+
is_streaming: true,
|
|
215
|
+
ai_provider: aiProvider,
|
|
216
|
+
ai_model: aiModel,
|
|
217
|
+
})
|
|
218
|
+
.select('id')
|
|
219
|
+
.single();
|
|
220
|
+
|
|
221
|
+
if (createError || !botMessage) {
|
|
222
|
+
console.error('Error creating bot message:', createError);
|
|
223
|
+
return new Response(JSON.stringify({ error: 'Failed to create message' }), {
|
|
224
|
+
status: 500,
|
|
225
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const messageId = botMessage.id;
|
|
230
|
+
console.log('Created streaming message:', messageId);
|
|
231
|
+
|
|
232
|
+
// Create broadcast channel using client-provided streamId
|
|
233
|
+
const channelName = streamId ? `stream:${streamId}` : `chat:${messageId}`;
|
|
234
|
+
console.log('Using broadcast channel:', channelName);
|
|
235
|
+
const channel = supabase.channel(channelName);
|
|
236
|
+
|
|
237
|
+
// Subscribe to channel (required before sending)
|
|
238
|
+
await new Promise<void>((resolve) => {
|
|
239
|
+
channel.subscribe((status) => {
|
|
240
|
+
if (status === 'SUBSCRIBED') {
|
|
241
|
+
console.log('Broadcast channel ready:', channelName);
|
|
242
|
+
resolve();
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
let fullResponse = '';
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
console.log('Starting AI stream with model:', aiModel, 'provider:', aiProvider);
|
|
251
|
+
console.log('Conversation history:', JSON.stringify(conversationHistory));
|
|
252
|
+
|
|
253
|
+
const result = streamText({
|
|
254
|
+
model,
|
|
255
|
+
messages: conversationHistory as any,
|
|
256
|
+
system: 'You are a helpful AI assistant. Be concise and helpful.',
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
console.log('streamText called, iterating textStream...');
|
|
260
|
+
|
|
261
|
+
// Stream chunks and broadcast each one
|
|
262
|
+
let chunkCount = 0;
|
|
263
|
+
for await (const chunk of result.textStream) {
|
|
264
|
+
chunkCount++;
|
|
265
|
+
fullResponse += chunk;
|
|
266
|
+
console.log(`Chunk ${chunkCount}: "${chunk.substring(0, 20)}..." (total: ${fullResponse.length})`);
|
|
267
|
+
|
|
268
|
+
// Broadcast chunk to client
|
|
269
|
+
await channel.send({
|
|
270
|
+
type: 'broadcast',
|
|
271
|
+
event: 'chunk',
|
|
272
|
+
payload: { chunk, accumulated: fullResponse },
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log('Stream completed, chunks:', chunkCount, 'total length:', fullResponse.length);
|
|
277
|
+
|
|
278
|
+
if (fullResponse.length === 0) {
|
|
279
|
+
fullResponse = 'I apologize, but I was unable to generate a response. Please try again.';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Broadcast completion
|
|
283
|
+
await channel.send({
|
|
284
|
+
type: 'broadcast',
|
|
285
|
+
event: 'done',
|
|
286
|
+
payload: { content: fullResponse, msgId: messageId },
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Final DB update
|
|
290
|
+
await supabase
|
|
291
|
+
.from('messages')
|
|
292
|
+
.update({
|
|
293
|
+
text: fullResponse,
|
|
294
|
+
streaming_content: fullResponse,
|
|
295
|
+
is_streaming: false,
|
|
296
|
+
})
|
|
297
|
+
.eq('id', messageId);
|
|
298
|
+
|
|
299
|
+
// Cleanup channel
|
|
300
|
+
await supabase.removeChannel(channel);
|
|
301
|
+
|
|
302
|
+
return new Response(
|
|
303
|
+
JSON.stringify({ success: true, messageId, content: fullResponse }),
|
|
304
|
+
{ headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
305
|
+
);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('AI error:', error);
|
|
308
|
+
console.error('Error name:', (error as Error)?.name);
|
|
309
|
+
console.error('Error message:', (error as Error)?.message);
|
|
310
|
+
console.error('Error stack:', (error as Error)?.stack);
|
|
311
|
+
|
|
312
|
+
// Broadcast error
|
|
313
|
+
await channel.send({
|
|
314
|
+
type: 'broadcast',
|
|
315
|
+
event: 'error',
|
|
316
|
+
payload: { error: String(error) },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
await supabase.removeChannel(channel);
|
|
320
|
+
|
|
321
|
+
await supabase
|
|
322
|
+
.from('messages')
|
|
323
|
+
.update({
|
|
324
|
+
text: 'Sorry, an error occurred.',
|
|
325
|
+
streaming_content: null,
|
|
326
|
+
is_streaming: false,
|
|
327
|
+
})
|
|
328
|
+
.eq('id', messageId);
|
|
329
|
+
|
|
330
|
+
return new Response(
|
|
331
|
+
JSON.stringify({ success: false, messageId, error: String(error) }),
|
|
332
|
+
{ status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Chat stream error:', error);
|
|
337
|
+
return new Response(
|
|
338
|
+
JSON.stringify({
|
|
339
|
+
error: error instanceof Error ? error.message : 'Internal server error',
|
|
340
|
+
}),
|
|
341
|
+
{
|
|
342
|
+
status: 500,
|
|
343
|
+
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
});
|