vibefast-cli 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FINAL-STATUS.md +144 -0
- package/HOW-IT-WORKS.md +559 -0
- package/PLAN.md +453 -0
- package/README.md +129 -0
- package/RECIPES-READY.md +172 -0
- package/STATUS.md +199 -0
- package/SUCCESS.md +259 -0
- package/TESTING-CHECKLIST.md +450 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/64907821e2634080acce34618d2f3d4c/blobs/11f2769953c717e188062bc644da97c1fd1e4d6d0813a226ce7567dba759afab0000019a736fb8d4 +1 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/0b03767237c0408301af51ca35d4b09470cbc479c7e5f23cc9de774749d23c59.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-shm +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/miniflare-R2BucketObject/d1cc388a1a0ef44dd5669fd1a165d168b61362136c8b5fa50aefd96c72688e54.sqlite-wal +0 -0
- package/cloudflare-worker/.wrangler/state/v3/r2/vibefast-recipes/blobs/620e8cf7c35d9806da25dee237e1d7e8b2432bd98f755b60e2c7f08a48d2c7b90000019a73736484 +0 -0
- package/cloudflare-worker/MIGRATION.md +160 -0
- package/cloudflare-worker/QUICKSTART.md +200 -0
- package/cloudflare-worker/README.md +242 -0
- package/cloudflare-worker/generate-token.js +32 -0
- package/cloudflare-worker/mini-native@latest.zip +0 -0
- package/cloudflare-worker/setup.sh +143 -0
- package/cloudflare-worker/test-recipe/apps/native/src/app/mini/index.tsx +15 -0
- package/cloudflare-worker/test-recipe/recipe.json +16 -0
- package/cloudflare-worker/worker.js +308 -0
- package/cloudflare-worker/wrangler.toml +13 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +149 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/devices.d.ts +3 -0
- package/dist/commands/devices.d.ts.map +1 -0
- package/dist/commands/devices.js +35 -0
- package/dist/commands/devices.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +67 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +40 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +23 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.d.ts.map +1 -0
- package/dist/commands/logout.js +16 -0
- package/dist/commands/logout.js.map +1 -0
- package/dist/commands/remove.d.ts +3 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +67 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/core/__tests__/journal.test.d.ts +2 -0
- package/dist/core/__tests__/journal.test.d.ts.map +1 -0
- package/dist/core/__tests__/journal.test.js +101 -0
- package/dist/core/__tests__/journal.test.js.map +1 -0
- package/dist/core/__tests__/validate.test.d.ts +2 -0
- package/dist/core/__tests__/validate.test.d.ts.map +1 -0
- package/dist/core/__tests__/validate.test.js +53 -0
- package/dist/core/__tests__/validate.test.js.map +1 -0
- package/dist/core/archive.d.ts +2 -0
- package/dist/core/archive.d.ts.map +1 -0
- package/dist/core/archive.js +59 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/auth.d.ts +15 -0
- package/dist/core/auth.d.ts.map +1 -0
- package/dist/core/auth.js +76 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/codemod.d.ts +20 -0
- package/dist/core/codemod.d.ts.map +1 -0
- package/dist/core/codemod.js +150 -0
- package/dist/core/codemod.js.map +1 -0
- package/dist/core/fsx.d.ts +12 -0
- package/dist/core/fsx.d.ts.map +1 -0
- package/dist/core/fsx.js +70 -0
- package/dist/core/fsx.js.map +1 -0
- package/dist/core/http.d.ts +30 -0
- package/dist/core/http.d.ts.map +1 -0
- package/dist/core/http.js +95 -0
- package/dist/core/http.js.map +1 -0
- package/dist/core/journal.d.ts +18 -0
- package/dist/core/journal.d.ts.map +1 -0
- package/dist/core/journal.js +34 -0
- package/dist/core/journal.js.map +1 -0
- package/dist/core/log.d.ts +8 -0
- package/dist/core/log.d.ts.map +1 -0
- package/dist/core/log.js +9 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/pathGuard.d.ts +3 -0
- package/dist/core/pathGuard.d.ts.map +1 -0
- package/dist/core/pathGuard.js +18 -0
- package/dist/core/pathGuard.js.map +1 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.d.ts.map +1 -0
- package/dist/core/paths.js +22 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/validate.d.ts +8 -0
- package/dist/core/validate.d.ts.map +1 -0
- package/dist/core/validate.js +27 -0
- package/dist/core/validate.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/docs/decisions.md +55 -0
- package/package.json +39 -0
- package/recipes/audio-recorder/apps/native/src/app/audio-recorder/index.tsx +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-player.tsx +301 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-recorder.tsx +373 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/audio-waveform.tsx +270 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/index.ts +4 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/components/recording-list.tsx +89 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-player-demo.tsx +66 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-cloud.tsx +68 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/audio-recorder-interview.tsx +102 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/basic.tsx +27 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/index.ts +5 -0
- package/recipes/audio-recorder/apps/native/src/features/audio-recorder/demo/with-recording-list-demo.tsx +82 -0
- package/recipes/audio-recorder/recipe.json +22 -0
- package/recipes/audio-recorder@latest.zip +0 -0
- package/recipes/charts/apps/native/src/app/charts/index.tsx +3 -0
- package/recipes/charts/apps/native/src/features/charts/README.md +185 -0
- package/recipes/charts/apps/native/src/features/charts/app/preview.tsx +223 -0
- package/recipes/charts/apps/native/src/features/charts/components/area-chart.tsx +40 -0
- package/recipes/charts/apps/native/src/features/charts/components/bar-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/candlestick-chart.tsx +196 -0
- package/recipes/charts/apps/native/src/features/charts/components/chart-card.tsx +65 -0
- package/recipes/charts/apps/native/src/features/charts/components/column-chart.tsx +143 -0
- package/recipes/charts/apps/native/src/features/charts/components/doughnut-chart.tsx +246 -0
- package/recipes/charts/apps/native/src/features/charts/components/index.ts +10 -0
- package/recipes/charts/apps/native/src/features/charts/components/line-chart.tsx +308 -0
- package/recipes/charts/apps/native/src/features/charts/components/radar-chart.tsx +180 -0
- package/recipes/charts/apps/native/src/features/charts/components/radial-bar-chart.tsx +188 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-area-chart.tsx +265 -0
- package/recipes/charts/apps/native/src/features/charts/components/stacked-bar-chart.tsx +322 -0
- package/recipes/charts/apps/native/src/features/charts/data/mock-data.ts +183 -0
- package/recipes/charts/apps/native/src/features/charts/types/index.ts +66 -0
- package/recipes/charts/recipe.json +22 -0
- 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/app/index.tsx +302 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-header-buttons.tsx +59 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-input-bar.tsx +469 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-markdown.tsx +575 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-message-bubble.tsx +246 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/chat-settings-modal.tsx +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/image-preview-list.tsx +115 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/code-block.tsx +165 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/index.ts +10 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/markdown/table-renderer.tsx +129 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-error-boundary.tsx +78 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/message-list.tsx +173 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/model-selector.tsx +283 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/report-content-modal.tsx +188 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/components/suggested-messages.tsx +67 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/models.ts +20 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/constants/report-reasons.ts +9 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-attachment-cache.ts +143 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-config.ts +664 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chat-handlers.ts +359 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-chatbot-settings.ts +89 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-conversation.ts +79 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-image-picker.ts +122 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-keyboard-coordinator.ts +161 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/hooks/use-smart-scroll-manager.ts +207 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/index.ts +86 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/models.ts +162 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/providers.ts +62 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/models/types.ts +40 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/file-uploader.ts +238 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/services/message-handler-service.ts +180 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/types/index.ts +60 -0
- package/recipes/chatbot/apps/native/src/features/chatbot/utils/chat-telemetry.ts +91 -0
- package/recipes/chatbot/recipe.json +22 -0
- package/recipes/chatbot@latest.zip +0 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/gallery.tsx +3 -0
- package/recipes/image-generator/apps/native/src/app/image-generator/index.tsx +3 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/_layout.tsx +25 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/gallery.tsx +217 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/app/index.tsx +237 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/gallery-image.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-detail-modal.tsx +215 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-model-selector.tsx +210 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/components/image-placeholder.tsx +26 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-gallery.ts +71 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator-settings.ts +152 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/hooks/use-image-generator.ts +93 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/models/models.ts +66 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-gallery-service.ts +98 -0
- package/recipes/image-generator/apps/native/src/features/image-generator/services/image-save-service.ts +121 -0
- package/recipes/image-generator/recipe.json +22 -0
- package/recipes/image-generator@latest.zip +0 -0
- package/recipes/quiz/apps/native/src/app/quiz/index.tsx +47 -0
- package/recipes/quiz/apps/native/src/features/quiz/components/question.tsx +67 -0
- package/recipes/quiz/apps/native/src/features/quiz/config.ts +11 -0
- package/recipes/quiz/apps/native/src/features/quiz/index.tsx +133 -0
- package/recipes/quiz/recipe.json +22 -0
- package/recipes/quiz@latest.zip +0 -0
- package/recipes/tracker-app/apps/native/src/app/tracker-app/index.tsx +1 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/app/index.tsx +108 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/animated-number.tsx +102 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/calorie-card.tsx +66 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/circular-progress.tsx +97 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/floating-add-button.tsx +27 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/macro-card.tsx +80 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/promo-banner.tsx +98 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/recently-logged.tsx +64 -0
- package/recipes/tracker-app/apps/native/src/features/tracker-app/components/week-calendar.tsx +68 -0
- package/recipes/tracker-app/recipe.json +22 -0
- package/recipes/tracker-app@latest.zip +0 -0
- package/recipes/upload-all.sh +32 -0
- package/recipes/voice-bot/apps/native/src/app/voice-bot/index.tsx +27 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/README.md +185 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/conversation-status.tsx +76 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/index.ts +4 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/message-input.tsx +98 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-bot-screen.tsx +173 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/components/voice-controls.tsx +73 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/index.ts +3 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/index.ts +1 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/services/use-voice-bot.ts +161 -0
- package/recipes/voice-bot/apps/native/src/features/voice-bot/types.ts +29 -0
- package/recipes/voice-bot/recipe.json +22 -0
- package/recipes/voice-bot@latest.zip +0 -0
- package/scripts/create-recipes.mjs +189 -0
- package/src/commands/add.ts +183 -0
- package/src/commands/devices.ts +38 -0
- package/src/commands/doctor.ts +67 -0
- package/src/commands/list.ts +45 -0
- package/src/commands/login.ts +24 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/remove.ts +78 -0
- package/src/core/__tests__/journal.test.ts +119 -0
- package/src/core/__tests__/validate.test.ts +64 -0
- package/src/core/archive.ts +69 -0
- package/src/core/auth.ts +103 -0
- package/src/core/codemod.ts +211 -0
- package/src/core/fsx.ts +80 -0
- package/src/core/http.ts +136 -0
- package/src/core/journal.ts +64 -0
- package/src/core/log.ts +9 -0
- package/src/core/pathGuard.ts +22 -0
- package/src/core/paths.ts +33 -0
- package/src/core/validate.ts +44 -0
- package/src/index.ts +27 -0
- package/test-critical-cases.mjs +258 -0
- package/tsconfig.json +21 -0
- package/vitest.config.mts +12 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type Provider = 'claude' | 'openai' | 'gemini';
|
|
2
|
+
|
|
3
|
+
export type ModelCategory = 'free' | 'premium' | 'specialized';
|
|
4
|
+
|
|
5
|
+
export type ModelSpeed = 'fast' | 'medium' | 'slow';
|
|
6
|
+
|
|
7
|
+
export interface ModelConfig {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
provider: Provider;
|
|
11
|
+
description: string;
|
|
12
|
+
category: ModelCategory;
|
|
13
|
+
capabilities: string[];
|
|
14
|
+
contextWindow?: number;
|
|
15
|
+
speed?: ModelSpeed;
|
|
16
|
+
released?: string;
|
|
17
|
+
tags?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProviderConfig {
|
|
21
|
+
id: Provider;
|
|
22
|
+
name: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
icon: React.ComponentType<any>;
|
|
25
|
+
defaultModel: string;
|
|
26
|
+
description: string;
|
|
27
|
+
website?: string;
|
|
28
|
+
color?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModelOptions {
|
|
32
|
+
models: ModelConfig[];
|
|
33
|
+
providers: ProviderConfig[];
|
|
34
|
+
categories: Record<ModelCategory, ModelConfig[]>;
|
|
35
|
+
byProvider: Record<Provider, ModelConfig[]>;
|
|
36
|
+
premium: ModelConfig[];
|
|
37
|
+
free: ModelConfig[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type ModelId = string;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { manipulateAsync, SaveFormat } from 'expo-image-manipulator';
|
|
2
|
+
import { Image } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { sharedApi } from '@/api-client/shared';
|
|
5
|
+
import { handleError } from '@/components/ui';
|
|
6
|
+
import { error as logError, info } from '@/core/logging';
|
|
7
|
+
|
|
8
|
+
// Configuration constants from Convex config
|
|
9
|
+
const CONFIG = {
|
|
10
|
+
MAX_IMAGE_DIMENSION: 1024,
|
|
11
|
+
COMPRESSION_QUALITY: 0.7,
|
|
12
|
+
MAX_FILE_SIZE_MB: 2,
|
|
13
|
+
MAX_FILE_SIZE_BYTES: 2 * 1024 * 1024, // 2MB
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const TARGET_ASPECT_RATIO = 16 / 9;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Service for uploading images to Convex File Storage
|
|
20
|
+
*/
|
|
21
|
+
export class FileUploadService {
|
|
22
|
+
private generateUploadUrl: (args: any) => Promise<string>;
|
|
23
|
+
|
|
24
|
+
constructor(generateUploadUrlMutation: (args: any) => Promise<string>) {
|
|
25
|
+
this.generateUploadUrl = generateUploadUrlMutation;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Uploads an image file to Convex storage
|
|
30
|
+
*/
|
|
31
|
+
async uploadImage(fileUri: string): Promise<string | null> {
|
|
32
|
+
try {
|
|
33
|
+
info('Starting image upload process', {
|
|
34
|
+
feature: 'chatbot.file_upload',
|
|
35
|
+
fileUri,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Step 1: Compress and optimize the image
|
|
39
|
+
const optimizedUri = await this.optimizeImage(fileUri);
|
|
40
|
+
if (!optimizedUri) {
|
|
41
|
+
throw new Error('Failed to optimize image');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Step 2: Validate file size
|
|
45
|
+
const response = await fetch(optimizedUri);
|
|
46
|
+
const blob = await response.blob();
|
|
47
|
+
|
|
48
|
+
if (blob.size > CONFIG.MAX_FILE_SIZE_BYTES) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`File size (${(blob.size / 1024 / 1024).toFixed(1)}MB) exceeds maximum allowed size of ${CONFIG.MAX_FILE_SIZE_MB}MB`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
info('Image optimized and validated', {
|
|
55
|
+
feature: 'chatbot.file_upload',
|
|
56
|
+
originalUri: fileUri,
|
|
57
|
+
optimizedUri,
|
|
58
|
+
fileSize: blob.size,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Step 3: Get upload URL from Convex
|
|
62
|
+
const uploadUrl = await this.generateUploadUrl({});
|
|
63
|
+
|
|
64
|
+
// Step 4: Upload the file to Convex storage
|
|
65
|
+
const uploadResponse = await fetch(uploadUrl, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { 'Content-Type': blob.type },
|
|
68
|
+
body: blob,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!uploadResponse.ok) {
|
|
72
|
+
throw new Error(`Upload failed: ${uploadResponse.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Step 5: Get the storage ID from the response
|
|
76
|
+
const { storageId } = await uploadResponse.json();
|
|
77
|
+
|
|
78
|
+
info('Image uploaded successfully', {
|
|
79
|
+
feature: 'chatbot.file_upload',
|
|
80
|
+
storageId,
|
|
81
|
+
fileSize: blob.size,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return storageId;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
handleError(error, {
|
|
87
|
+
feature: 'chatbot.file_upload',
|
|
88
|
+
fallbackMessage: 'Failed to upload image',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Optimizes an image by resizing and compressing it while preserving aspect ratio and alpha
|
|
97
|
+
*/
|
|
98
|
+
private async optimizeImage(fileUri: string): Promise<string | null> {
|
|
99
|
+
try {
|
|
100
|
+
// Detect image format to preserve alpha channels
|
|
101
|
+
const response = await fetch(fileUri);
|
|
102
|
+
const blob = await response.blob();
|
|
103
|
+
const isPng = blob.type?.includes('png');
|
|
104
|
+
const isWebp = blob.type?.includes('webp');
|
|
105
|
+
|
|
106
|
+
const actions: Parameters<typeof manipulateAsync>[1][number][] = [];
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const { width, height } = await this.getImageDimensions(fileUri);
|
|
110
|
+
const cropRect = this.calculateCenterCrop(width, height);
|
|
111
|
+
|
|
112
|
+
if (cropRect) {
|
|
113
|
+
actions.push({ crop: cropRect });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const workingWidth = cropRect ? cropRect.width : width;
|
|
117
|
+
const workingHeight = cropRect ? cropRect.height : height;
|
|
118
|
+
const resize = this.calculateResizeDimensions(
|
|
119
|
+
workingWidth,
|
|
120
|
+
workingHeight,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
if (resize) {
|
|
124
|
+
actions.push({ resize });
|
|
125
|
+
}
|
|
126
|
+
} catch (dimensionError) {
|
|
127
|
+
logError(
|
|
128
|
+
dimensionError instanceof Error
|
|
129
|
+
? dimensionError
|
|
130
|
+
: new Error('Failed to read image dimensions'),
|
|
131
|
+
{ feature: 'chatbot.file_upload', fileUri },
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
actions.push({
|
|
135
|
+
resize: {
|
|
136
|
+
width: CONFIG.MAX_IMAGE_DIMENSION,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = await manipulateAsync(fileUri, actions, {
|
|
142
|
+
compress: CONFIG.COMPRESSION_QUALITY,
|
|
143
|
+
format: isPng
|
|
144
|
+
? SaveFormat.PNG
|
|
145
|
+
: isWebp
|
|
146
|
+
? SaveFormat.WEBP
|
|
147
|
+
: SaveFormat.JPEG,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return result.uri;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logError(
|
|
153
|
+
error instanceof Error ? error : new Error('Failed to optimize image'),
|
|
154
|
+
{ feature: 'chatbot.file_upload', fileUri },
|
|
155
|
+
);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private getImageDimensions(
|
|
161
|
+
uri: string,
|
|
162
|
+
): Promise<{ width: number; height: number }> {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
Image.getSize(
|
|
165
|
+
uri,
|
|
166
|
+
(width, height) => resolve({ width, height }),
|
|
167
|
+
(error) => reject(error),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private calculateCenterCrop(
|
|
173
|
+
width: number,
|
|
174
|
+
height: number,
|
|
175
|
+
): {
|
|
176
|
+
originX: number;
|
|
177
|
+
originY: number;
|
|
178
|
+
width: number;
|
|
179
|
+
height: number;
|
|
180
|
+
} | null {
|
|
181
|
+
const aspectRatio = width / height;
|
|
182
|
+
|
|
183
|
+
if (Math.abs(aspectRatio - TARGET_ASPECT_RATIO) < 0.01) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (aspectRatio > TARGET_ASPECT_RATIO) {
|
|
188
|
+
const targetWidth = Math.round(height * TARGET_ASPECT_RATIO);
|
|
189
|
+
const originX = Math.round((width - targetWidth) / 2);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
originX,
|
|
193
|
+
originY: 0,
|
|
194
|
+
width: targetWidth,
|
|
195
|
+
height,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const targetHeight = Math.round(width / TARGET_ASPECT_RATIO);
|
|
200
|
+
const originY = Math.round((height - targetHeight) / 2);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
originX: 0,
|
|
204
|
+
originY,
|
|
205
|
+
width,
|
|
206
|
+
height: targetHeight,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private calculateResizeDimensions(
|
|
211
|
+
width: number,
|
|
212
|
+
height: number,
|
|
213
|
+
): { width: number; height: number } | null {
|
|
214
|
+
const largestSide = Math.max(width, height);
|
|
215
|
+
|
|
216
|
+
if (largestSide <= CONFIG.MAX_IMAGE_DIMENSION) {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const scale = CONFIG.MAX_IMAGE_DIMENSION / largestSide;
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
width: Math.round(width * scale),
|
|
224
|
+
height: Math.round(height * scale),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Hook to create a FileUploadService instance
|
|
231
|
+
*
|
|
232
|
+
* @returns FileUploadService instance with the mutation hook bound
|
|
233
|
+
*/
|
|
234
|
+
export function useFileUploadService(): FileUploadService {
|
|
235
|
+
const generateUploadUrl = sharedApi.useGenerateUploadUrl();
|
|
236
|
+
|
|
237
|
+
return new FileUploadService(generateUploadUrl);
|
|
238
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
import { Alert } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { defaultCacheService } from '@/core/cache';
|
|
5
|
+
import type { AppMessage } from '@/features/chatbot/types';
|
|
6
|
+
import { translate } from '@/lib';
|
|
7
|
+
|
|
8
|
+
export type AppAttachment = {
|
|
9
|
+
storageId: Id<'_storage'>;
|
|
10
|
+
url?: string;
|
|
11
|
+
mimeType?: string;
|
|
12
|
+
type: 'image';
|
|
13
|
+
fileName?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type MessageSubmissionOptions = {
|
|
17
|
+
currentInput: string;
|
|
18
|
+
conversationId: string;
|
|
19
|
+
attachmentsFromInputBar?: {
|
|
20
|
+
type: 'image';
|
|
21
|
+
storageId: string;
|
|
22
|
+
fileName?: string;
|
|
23
|
+
mimeType: string;
|
|
24
|
+
}[];
|
|
25
|
+
storeUserMessageMutation: (args: {
|
|
26
|
+
conversationId: Id<'conversations'>;
|
|
27
|
+
authorType: 'user';
|
|
28
|
+
text?: string;
|
|
29
|
+
attachments?: {
|
|
30
|
+
type: 'image';
|
|
31
|
+
storageId: Id<'_storage'>;
|
|
32
|
+
fileName?: string;
|
|
33
|
+
mimeType?: string;
|
|
34
|
+
}[];
|
|
35
|
+
}) => Promise<Id<'messages'>>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type ClearChatOptions = {
|
|
39
|
+
conversationId: string;
|
|
40
|
+
clearConversationMutation: (args: {
|
|
41
|
+
conversationId: Id<'conversations'>;
|
|
42
|
+
}) => Promise<any>;
|
|
43
|
+
onCleared?: () => void;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Service for handling message submission and chat operations
|
|
48
|
+
*/
|
|
49
|
+
export class MessageHandlerService {
|
|
50
|
+
/**
|
|
51
|
+
* Store a user message to the database before sending to AI
|
|
52
|
+
*/
|
|
53
|
+
static async storeUserMessage(
|
|
54
|
+
options: MessageSubmissionOptions,
|
|
55
|
+
): Promise<Id<'messages'> | null> {
|
|
56
|
+
const {
|
|
57
|
+
currentInput,
|
|
58
|
+
conversationId,
|
|
59
|
+
attachmentsFromInputBar,
|
|
60
|
+
storeUserMessageMutation,
|
|
61
|
+
} = options;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log('[MessageHandlerService] Storing user message to DB...');
|
|
65
|
+
const messageId = await storeUserMessageMutation({
|
|
66
|
+
conversationId: conversationId as Id<'conversations'>,
|
|
67
|
+
authorType: 'user',
|
|
68
|
+
text: currentInput.trim() || undefined,
|
|
69
|
+
attachments: attachmentsFromInputBar?.map((att) => ({
|
|
70
|
+
type: 'image',
|
|
71
|
+
storageId: att.storageId as Id<'_storage'>,
|
|
72
|
+
fileName: att.fileName,
|
|
73
|
+
mimeType: att.mimeType,
|
|
74
|
+
})),
|
|
75
|
+
});
|
|
76
|
+
console.log('[MessageHandlerService] User message stored successfully.');
|
|
77
|
+
return messageId as Id<'messages'>;
|
|
78
|
+
} catch (dbError) {
|
|
79
|
+
console.error(
|
|
80
|
+
'[MessageHandlerService] Failed to store user message:',
|
|
81
|
+
dbError,
|
|
82
|
+
);
|
|
83
|
+
Alert.alert(translate('common.error'), translate('chatbot.send_failed'), [
|
|
84
|
+
{
|
|
85
|
+
text: translate('common.ok'),
|
|
86
|
+
onPress: () => {
|
|
87
|
+
// Reset the useChat hook by reloading the data
|
|
88
|
+
defaultCacheService.removeItem(`conversation_${conversationId}`);
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
]);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Handle clearing a chat conversation
|
|
98
|
+
*/
|
|
99
|
+
static clearChat(options: ClearChatOptions): void {
|
|
100
|
+
const { conversationId, clearConversationMutation } = options;
|
|
101
|
+
|
|
102
|
+
Alert.alert(
|
|
103
|
+
'Clear Chat',
|
|
104
|
+
'Are you sure you want to clear all messages? This action cannot be undone.',
|
|
105
|
+
[
|
|
106
|
+
{
|
|
107
|
+
text: 'Cancel',
|
|
108
|
+
style: 'cancel',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
text: 'Clear',
|
|
112
|
+
style: 'destructive',
|
|
113
|
+
onPress: async () => {
|
|
114
|
+
try {
|
|
115
|
+
console.log(
|
|
116
|
+
'[MessageHandlerService] Clearing conversation:',
|
|
117
|
+
conversationId,
|
|
118
|
+
);
|
|
119
|
+
await clearConversationMutation({
|
|
120
|
+
conversationId: conversationId as Id<'conversations'>,
|
|
121
|
+
});
|
|
122
|
+
console.log(
|
|
123
|
+
'[MessageHandlerService] Conversation cleared successfully',
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// Reset the useChat hook by reloading the data
|
|
127
|
+
defaultCacheService.removeItem(`conversation_${conversationId}`);
|
|
128
|
+
options.onCleared?.();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(
|
|
131
|
+
'[MessageHandlerService] Failed to clear conversation:',
|
|
132
|
+
error,
|
|
133
|
+
);
|
|
134
|
+
Alert.alert('Error', 'Failed to clear chat. Please try again.');
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Transform display messages with stable attachment mapping
|
|
144
|
+
*/
|
|
145
|
+
static transformDisplayMessages(
|
|
146
|
+
liveMessages: AppMessage[],
|
|
147
|
+
persistedMessages: AppMessage[],
|
|
148
|
+
lastUserAttachments?: AppAttachment[],
|
|
149
|
+
attachmentCache?: Record<string, AppAttachment[]>,
|
|
150
|
+
): AppMessage[] {
|
|
151
|
+
const persisted = new Map(persistedMessages.map((m) => [m.id, m]));
|
|
152
|
+
const lastUserIdx = [...liveMessages]
|
|
153
|
+
.map((m, i) => ({ m, i }))
|
|
154
|
+
.reverse()
|
|
155
|
+
.find((x) => x.m.role === 'user')?.i;
|
|
156
|
+
|
|
157
|
+
return liveMessages.map((m, i) => {
|
|
158
|
+
const fromDb = persisted.get(m.id);
|
|
159
|
+
const isMostRecentUser = m.role === 'user' && i === lastUserIdx;
|
|
160
|
+
// Attachment precedence:
|
|
161
|
+
// 1) Cached attachments for this live message ID (persist across renders)
|
|
162
|
+
// 2) Most recent user's optimistic attachments (for immediate display)
|
|
163
|
+
// 3) Attachments from persisted DB messages when IDs align
|
|
164
|
+
const fromCache = attachmentCache?.[m.id];
|
|
165
|
+
const attachments =
|
|
166
|
+
fromCache && fromCache.length
|
|
167
|
+
? fromCache
|
|
168
|
+
: ((isMostRecentUser && lastUserAttachments?.length
|
|
169
|
+
? lastUserAttachments
|
|
170
|
+
: fromDb?.attachments) ?? undefined);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
...m,
|
|
174
|
+
attachments,
|
|
175
|
+
toolCalls: m.toolCalls ?? fromDb?.toolCalls,
|
|
176
|
+
metadata: m.metadata ?? fromDb?.metadata,
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
import type { Message } from 'ai';
|
|
3
|
+
|
|
4
|
+
export type LocalMessageContentPart =
|
|
5
|
+
| { type: 'text'; text: string }
|
|
6
|
+
| { type: 'image'; image: string | URL };
|
|
7
|
+
|
|
8
|
+
export type ConvexMessageAttachment = {
|
|
9
|
+
type: 'image';
|
|
10
|
+
storageId: Id<'_storage'>;
|
|
11
|
+
url?: string;
|
|
12
|
+
fileName?: string;
|
|
13
|
+
mimeType?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type ToolCallRecord = {
|
|
17
|
+
toolName: string;
|
|
18
|
+
args: unknown;
|
|
19
|
+
result?: unknown;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type MessageMetadata = {
|
|
23
|
+
streamId?: string;
|
|
24
|
+
status?: string;
|
|
25
|
+
usage?: {
|
|
26
|
+
promptTokens?: number;
|
|
27
|
+
completionTokens?: number;
|
|
28
|
+
totalTokens?: number;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type AgentStatus =
|
|
33
|
+
| {
|
|
34
|
+
type: 'thinking';
|
|
35
|
+
label?: string | null;
|
|
36
|
+
startedAt: number;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
type: 'tool';
|
|
40
|
+
toolName: string;
|
|
41
|
+
label?: string | null;
|
|
42
|
+
startedAt: number;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type AppMessage = Omit<Message, 'content'> & {
|
|
46
|
+
content: string | LocalMessageContentPart[];
|
|
47
|
+
attachments?: ConvexMessageAttachment[];
|
|
48
|
+
toolCalls?: ToolCallRecord[];
|
|
49
|
+
metadata?: MessageMetadata;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type ConversationMessageFromDB = {
|
|
53
|
+
_id: string;
|
|
54
|
+
_creationTime: number;
|
|
55
|
+
authorType: 'user' | 'bot';
|
|
56
|
+
text?: string;
|
|
57
|
+
attachments?: ConvexMessageAttachment[];
|
|
58
|
+
toolCalls?: ToolCallRecord[];
|
|
59
|
+
metadata?: MessageMetadata;
|
|
60
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { TelemetryMode } from '@/core/config';
|
|
2
|
+
import { getChatTelemetryMode } from '@/core/config/telemetry';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_SAMPLE_PROBABILITY = 0.1;
|
|
5
|
+
|
|
6
|
+
type TimelineEntry = {
|
|
7
|
+
label: string;
|
|
8
|
+
msFromStart: number;
|
|
9
|
+
data?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ChatTelemetryTracker = {
|
|
13
|
+
readonly event: string;
|
|
14
|
+
mark: (label: string, data?: Record<string, unknown>) => void;
|
|
15
|
+
finalize: (status: 'ok' | 'error', extra?: Record<string, unknown>) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function shouldTrack(mode: TelemetryMode, probability: number): boolean {
|
|
19
|
+
if (mode === 'off') {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
if (mode === 'debug') {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clampedProbability = Math.min(Math.max(probability, 0), 1);
|
|
27
|
+
return Math.random() < clampedProbability;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createChatTelemetryTracker(
|
|
31
|
+
event: string,
|
|
32
|
+
baseContext: Record<string, unknown>,
|
|
33
|
+
sampleProbability = DEFAULT_SAMPLE_PROBABILITY,
|
|
34
|
+
): ChatTelemetryTracker | null {
|
|
35
|
+
const mode = getChatTelemetryMode();
|
|
36
|
+
if (!shouldTrack(mode, sampleProbability)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const start = Date.now();
|
|
41
|
+
const timeline: TimelineEntry[] = [{ label: 'start', msFromStart: 0 }];
|
|
42
|
+
let finalized = false;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
event,
|
|
46
|
+
mark(label, data) {
|
|
47
|
+
if (finalized) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const elapsed = Date.now() - start;
|
|
51
|
+
timeline.push({
|
|
52
|
+
label,
|
|
53
|
+
msFromStart: elapsed,
|
|
54
|
+
data,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
finalize(status, extra) {
|
|
58
|
+
if (finalized) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
finalized = true;
|
|
62
|
+
const totalMs = Date.now() - start;
|
|
63
|
+
console.log('[chat][telemetry]', {
|
|
64
|
+
event,
|
|
65
|
+
mode,
|
|
66
|
+
status,
|
|
67
|
+
totalMs,
|
|
68
|
+
...baseContext,
|
|
69
|
+
...(extra ?? {}),
|
|
70
|
+
timeline,
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function logChatTelemetryEvent(
|
|
77
|
+
event: string,
|
|
78
|
+
payload: Record<string, unknown>,
|
|
79
|
+
sampleProbability = DEFAULT_SAMPLE_PROBABILITY,
|
|
80
|
+
) {
|
|
81
|
+
const mode = getChatTelemetryMode();
|
|
82
|
+
if (!shouldTrack(mode, sampleProbability)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('[chat][telemetry]', {
|
|
87
|
+
event,
|
|
88
|
+
mode,
|
|
89
|
+
...payload,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chatbot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered chat assistant",
|
|
5
|
+
"copy": [
|
|
6
|
+
{
|
|
7
|
+
"from": "apps/native/src/app/chatbot",
|
|
8
|
+
"to": "apps/native/src/app/(root)/(protected)/chatbot"
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"from": "apps/native/src/features/chatbot",
|
|
12
|
+
"to": "apps/native/src/features/chatbot"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"nav": {
|
|
16
|
+
"href": "/(root)/(protected)/chatbot",
|
|
17
|
+
"label": "AI Chatbot",
|
|
18
|
+
"icon": "💬",
|
|
19
|
+
"color": "#F43F5E"
|
|
20
|
+
},
|
|
21
|
+
"target": "native"
|
|
22
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Stack } from 'expo-router';
|
|
2
|
+
import { translate } from '@/lib';
|
|
3
|
+
|
|
4
|
+
export default function ImageGeneratorLayout() {
|
|
5
|
+
return (
|
|
6
|
+
<Stack
|
|
7
|
+
screenOptions={{
|
|
8
|
+
headerShown: true,
|
|
9
|
+
}}
|
|
10
|
+
>
|
|
11
|
+
<Stack.Screen
|
|
12
|
+
name="index"
|
|
13
|
+
options={{
|
|
14
|
+
title: translate('image_generator.title'),
|
|
15
|
+
}}
|
|
16
|
+
/>
|
|
17
|
+
<Stack.Screen
|
|
18
|
+
name="gallery"
|
|
19
|
+
options={{
|
|
20
|
+
title: `${translate('image_generator.gallery_title')} (0)`,
|
|
21
|
+
}}
|
|
22
|
+
/>
|
|
23
|
+
</Stack>
|
|
24
|
+
);
|
|
25
|
+
}
|