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,359 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { Alert, Keyboard } from 'react-native';
|
|
4
|
+
|
|
5
|
+
import { chatbotApi } from '@/api-client/chatbot';
|
|
6
|
+
import { reportingApi } from '@/api-client/reporting';
|
|
7
|
+
import { useToast } from '@/components/ui/utils';
|
|
8
|
+
import type { ReportReason } from '@/features/chatbot/constants/report-reasons';
|
|
9
|
+
import { useChatConfig } from '@/features/chatbot/hooks/use-chat-config';
|
|
10
|
+
import {
|
|
11
|
+
type AppAttachment,
|
|
12
|
+
MessageHandlerService,
|
|
13
|
+
} from '@/features/chatbot/services/message-handler-service';
|
|
14
|
+
import type { AppMessage } from '@/features/chatbot/types';
|
|
15
|
+
|
|
16
|
+
type UseChatHandlersOptions = {
|
|
17
|
+
conversationId: string | null;
|
|
18
|
+
authToken?: string;
|
|
19
|
+
initialMessages?: AppMessage[];
|
|
20
|
+
preferredProvider?: string;
|
|
21
|
+
preferredModel?: string;
|
|
22
|
+
searchMode?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Hook to manage chat interactions, message handling, and reporting
|
|
27
|
+
*/
|
|
28
|
+
export const useChatHandlers = (options: UseChatHandlersOptions) => {
|
|
29
|
+
const {
|
|
30
|
+
conversationId,
|
|
31
|
+
authToken,
|
|
32
|
+
initialMessages = [],
|
|
33
|
+
preferredProvider = 'openai',
|
|
34
|
+
preferredModel = 'gpt-5-nano',
|
|
35
|
+
searchMode = 'provider',
|
|
36
|
+
} = options;
|
|
37
|
+
|
|
38
|
+
// console.log('[CHATBOT_FRONTEND] 🎣 useChatHandlers hook 1:', {
|
|
39
|
+
// conversationId,
|
|
40
|
+
// hasAuthToken: !!authToken,
|
|
41
|
+
// initialMessagesCount: initialMessages.length,
|
|
42
|
+
// preferredProvider,
|
|
43
|
+
// preferredModel,
|
|
44
|
+
// searchMode,
|
|
45
|
+
// file: 'src/features/chatbot/hooks/use-chat-handlers.ts',
|
|
46
|
+
// function: 'useChatHandlers',
|
|
47
|
+
// });
|
|
48
|
+
|
|
49
|
+
const [messageToReport, setMessageToReport] = useState<AppMessage | null>(
|
|
50
|
+
null,
|
|
51
|
+
);
|
|
52
|
+
const lastUserAttachments = React.useRef<AppAttachment[] | undefined>(
|
|
53
|
+
undefined,
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// Persist attachments per live message ID to avoid disappearing images
|
|
57
|
+
const attachmentCacheRef = React.useRef<Record<string, AppAttachment[]>>({});
|
|
58
|
+
|
|
59
|
+
const storeUserMessageMutation = chatbotApi.useStoreMessage();
|
|
60
|
+
const clearConversationMutation = chatbotApi.useClearConversation();
|
|
61
|
+
const deleteUserMessageMutation = chatbotApi.useDeleteUserMessage();
|
|
62
|
+
const reportChatMessageMutation = reportingApi.useReportChatMessage();
|
|
63
|
+
const toast = useToast();
|
|
64
|
+
const handleInputChangeRef = React.useRef<(text: string) => void>(() => {});
|
|
65
|
+
|
|
66
|
+
// Define onStreamFailure BEFORE passing it to useChatConfig
|
|
67
|
+
const onStreamFailure = useCallback(
|
|
68
|
+
async (details: {
|
|
69
|
+
userMessageId: string | null;
|
|
70
|
+
text: string;
|
|
71
|
+
attachments?: {
|
|
72
|
+
type: 'image';
|
|
73
|
+
storageId: string;
|
|
74
|
+
fileName?: string;
|
|
75
|
+
mimeType?: string;
|
|
76
|
+
}[];
|
|
77
|
+
errorMessage?: string;
|
|
78
|
+
}) => {
|
|
79
|
+
const message =
|
|
80
|
+
details.errorMessage ?? 'Message failed to send. Please try again.';
|
|
81
|
+
|
|
82
|
+
// Show error toast to user
|
|
83
|
+
toast.error(message);
|
|
84
|
+
|
|
85
|
+
// Restore user's input text so they can retry
|
|
86
|
+
handleInputChangeRef.current(details.text ?? '');
|
|
87
|
+
|
|
88
|
+
// Delete the failed user message from backend
|
|
89
|
+
if (details.userMessageId) {
|
|
90
|
+
try {
|
|
91
|
+
await deleteUserMessageMutation({
|
|
92
|
+
messageId: details.userMessageId as Id<'messages'>,
|
|
93
|
+
});
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn('[useChatHandlers] Failed to remove failed message', {
|
|
96
|
+
messageId: details.userMessageId,
|
|
97
|
+
error,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
delete attachmentCacheRef.current[details.userMessageId];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clear attachment cache
|
|
104
|
+
lastUserAttachments.current = undefined;
|
|
105
|
+
},
|
|
106
|
+
[deleteUserMessageMutation, toast],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const chatConfig = useChatConfig({
|
|
110
|
+
conversationId,
|
|
111
|
+
authToken,
|
|
112
|
+
initialMessages,
|
|
113
|
+
preferredProvider,
|
|
114
|
+
preferredModel,
|
|
115
|
+
searchMode,
|
|
116
|
+
onStreamFailure,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
handleInputChangeRef.current = chatConfig.handleInputChange;
|
|
120
|
+
|
|
121
|
+
const displayMessages = useMemo((): AppMessage[] => {
|
|
122
|
+
return MessageHandlerService.transformDisplayMessages(
|
|
123
|
+
chatConfig.messages,
|
|
124
|
+
initialMessages,
|
|
125
|
+
lastUserAttachments.current,
|
|
126
|
+
attachmentCacheRef.current,
|
|
127
|
+
) as AppMessage[];
|
|
128
|
+
}, [initialMessages, chatConfig.messages]);
|
|
129
|
+
|
|
130
|
+
// After we compute display messages, cache any attachments by live message ID
|
|
131
|
+
React.useEffect(() => {
|
|
132
|
+
if (!displayMessages?.length) return;
|
|
133
|
+
const cache = attachmentCacheRef.current;
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const m of displayMessages) {
|
|
136
|
+
if (m.attachments && m.attachments.length) {
|
|
137
|
+
const existing = cache[m.id];
|
|
138
|
+
if (!existing || existing.length !== m.attachments.length) {
|
|
139
|
+
cache[m.id] = m.attachments as AppAttachment[];
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (changed) {
|
|
145
|
+
// noop: we mutate ref; next memo pass will read updated cache
|
|
146
|
+
}
|
|
147
|
+
}, [displayMessages]);
|
|
148
|
+
|
|
149
|
+
// useEffect(() => {
|
|
150
|
+
// console.log(
|
|
151
|
+
// '[Client useChatHandlers] DisplayMessages updated count:',
|
|
152
|
+
// displayMessages.length
|
|
153
|
+
// );
|
|
154
|
+
// if (displayMessages.length > 0) {
|
|
155
|
+
// const lastMsg = displayMessages[displayMessages.length - 1];
|
|
156
|
+
// if (lastMsg) {
|
|
157
|
+
// console.log(
|
|
158
|
+
// `[Client useChatHandlers] Last display message (ID: ${lastMsg.id}, Role: ${lastMsg.role}, Content Type: ${typeof lastMsg.content}):`,
|
|
159
|
+
// JSON.stringify(lastMsg.content)
|
|
160
|
+
// );
|
|
161
|
+
// }
|
|
162
|
+
// }
|
|
163
|
+
// }, [displayMessages]);
|
|
164
|
+
|
|
165
|
+
const handleReportMessage = useCallback((message: AppMessage) => {
|
|
166
|
+
setMessageToReport(message);
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const handleSubmitReport = useCallback(
|
|
170
|
+
async (reportData: {
|
|
171
|
+
messageId?: string;
|
|
172
|
+
reason: ReportReason;
|
|
173
|
+
details?: string;
|
|
174
|
+
}): Promise<boolean> => {
|
|
175
|
+
if (!reportData.messageId) {
|
|
176
|
+
console.warn(
|
|
177
|
+
'[Client useChatHandlers] Report dismissed: missing messageId',
|
|
178
|
+
);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
await reportChatMessageMutation({
|
|
183
|
+
messageId: reportData.messageId,
|
|
184
|
+
reason: reportData.reason,
|
|
185
|
+
details: reportData.details,
|
|
186
|
+
});
|
|
187
|
+
Alert.alert('Report Submitted', 'Thank you for your report.', [
|
|
188
|
+
{ text: 'OK' },
|
|
189
|
+
]);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
console.error(
|
|
193
|
+
'[Client useChatHandlers] Failed to submit report:',
|
|
194
|
+
error,
|
|
195
|
+
);
|
|
196
|
+
Alert.alert(
|
|
197
|
+
'Report Failed',
|
|
198
|
+
"We couldn't submit your report. Please try again.",
|
|
199
|
+
[{ text: 'OK' }],
|
|
200
|
+
);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
[reportChatMessageMutation],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const handleCancelReport = useCallback(() => {
|
|
208
|
+
setMessageToReport(null);
|
|
209
|
+
}, []);
|
|
210
|
+
|
|
211
|
+
const handleTextInputChange = useCallback(
|
|
212
|
+
(text: string) => {
|
|
213
|
+
chatConfig.handleInputChange(text);
|
|
214
|
+
},
|
|
215
|
+
[chatConfig],
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const handleMessageSubmit = useCallback(
|
|
219
|
+
async (
|
|
220
|
+
attachmentsFromInputBar?: {
|
|
221
|
+
type: 'image';
|
|
222
|
+
storageId: string;
|
|
223
|
+
fileName?: string;
|
|
224
|
+
mimeType: string;
|
|
225
|
+
}[],
|
|
226
|
+
) => {
|
|
227
|
+
// Dismiss keyboard when sending a message
|
|
228
|
+
Keyboard.dismiss();
|
|
229
|
+
|
|
230
|
+
// Read the current input value just before submission to prevent stale closure bugs
|
|
231
|
+
const currentInput = chatConfig.input;
|
|
232
|
+
const trimmedInput = currentInput.trim();
|
|
233
|
+
|
|
234
|
+
// console.log(
|
|
235
|
+
// '[Client useChatHandlers handleMessageSubmit] Attempting to submit. Input:',
|
|
236
|
+
// currentInput,
|
|
237
|
+
// 'Attachments from InputBar:',
|
|
238
|
+
// attachmentsFromInputBar,
|
|
239
|
+
// 'ConvID:',
|
|
240
|
+
// conversationId
|
|
241
|
+
// );
|
|
242
|
+
|
|
243
|
+
if (!conversationId) {
|
|
244
|
+
console.error(
|
|
245
|
+
'[Client useChatHandlers handleMessageSubmit] Missing conversationId.',
|
|
246
|
+
);
|
|
247
|
+
Alert.alert(
|
|
248
|
+
'Error',
|
|
249
|
+
'Chat session not fully initialized. Cannot send message.',
|
|
250
|
+
);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
!trimmedInput &&
|
|
256
|
+
(!attachmentsFromInputBar || attachmentsFromInputBar.length === 0)
|
|
257
|
+
) {
|
|
258
|
+
console.log(
|
|
259
|
+
'[Client useChatHandlers handleMessageSubmit] Empty message and no attachments. Not submitting.',
|
|
260
|
+
);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Store attachments in ref for optimistic UI display
|
|
265
|
+
lastUserAttachments.current = attachmentsFromInputBar?.map((att) => ({
|
|
266
|
+
type: 'image' as const,
|
|
267
|
+
storageId: att.storageId as Id<'_storage'>,
|
|
268
|
+
fileName: att.fileName,
|
|
269
|
+
mimeType: att.mimeType,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Store user message and retrieve the persisted ID before triggering streaming
|
|
273
|
+
const storedMessageId = await MessageHandlerService.storeUserMessage({
|
|
274
|
+
currentInput: trimmedInput,
|
|
275
|
+
conversationId,
|
|
276
|
+
attachmentsFromInputBar,
|
|
277
|
+
storeUserMessageMutation,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
if (!storedMessageId) {
|
|
281
|
+
console.error(
|
|
282
|
+
'[Client useChatHandlers] Unable to store user message; aborting streaming request.',
|
|
283
|
+
);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Submit to AI
|
|
288
|
+
const dummyFormEvent = {
|
|
289
|
+
preventDefault: () => {},
|
|
290
|
+
} as React.FormEvent<HTMLFormElement>;
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
await chatConfig.handleSubmit(dummyFormEvent, {
|
|
294
|
+
body: {
|
|
295
|
+
conversationId: conversationId,
|
|
296
|
+
preferredModel,
|
|
297
|
+
userMessageId: storedMessageId,
|
|
298
|
+
text: trimmedInput,
|
|
299
|
+
attachments:
|
|
300
|
+
attachmentsFromInputBar && attachmentsFromInputBar.length > 0
|
|
301
|
+
? attachmentsFromInputBar
|
|
302
|
+
: undefined,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
} catch (submitError) {
|
|
306
|
+
console.error(
|
|
307
|
+
'[Client useChatHandlers] Streaming submission failed:',
|
|
308
|
+
submitError,
|
|
309
|
+
);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// console.log(
|
|
314
|
+
// '[Client useChatHandlers handleMessageSubmit] chatConfig.handleSubmit called for bot response.'
|
|
315
|
+
// );
|
|
316
|
+
|
|
317
|
+
// Note: We don't clear lastUserAttachments here anymore.
|
|
318
|
+
// Once initialMessages contains the stored user message (with attachments),
|
|
319
|
+
// the initialEquivalent?.attachments path takes over naturally.
|
|
320
|
+
},
|
|
321
|
+
[conversationId, storeUserMessageMutation, chatConfig, preferredModel],
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
const handleClearChat = useCallback(() => {
|
|
325
|
+
if (!conversationId) {
|
|
326
|
+
console.error('[handleClearChat] Missing conversationId');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
MessageHandlerService.clearChat({
|
|
331
|
+
conversationId,
|
|
332
|
+
clearConversationMutation,
|
|
333
|
+
onCleared: () => {
|
|
334
|
+
chatConfig.resetMessages();
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
// Reset local caches immediately for a clean slate
|
|
338
|
+
lastUserAttachments.current = undefined;
|
|
339
|
+
attachmentCacheRef.current = {};
|
|
340
|
+
}, [conversationId, clearConversationMutation, chatConfig]);
|
|
341
|
+
|
|
342
|
+
// When conversation changes, reset transient caches to prevent leakage
|
|
343
|
+
React.useEffect(() => {
|
|
344
|
+
lastUserAttachments.current = undefined;
|
|
345
|
+
attachmentCacheRef.current = {};
|
|
346
|
+
}, [conversationId]);
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
...chatConfig,
|
|
350
|
+
messages: displayMessages,
|
|
351
|
+
messageToReport,
|
|
352
|
+
handleReportMessage,
|
|
353
|
+
handleSubmitReport,
|
|
354
|
+
handleCancelReport,
|
|
355
|
+
handleTextInputChange,
|
|
356
|
+
handleMessageSubmit,
|
|
357
|
+
handleClearChat,
|
|
358
|
+
};
|
|
359
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { defaultCacheService } from '@/core/cache';
|
|
4
|
+
|
|
5
|
+
import type { Provider } from '../constants/models';
|
|
6
|
+
|
|
7
|
+
export type SearchMode = 'provider' | 'tavily' | 'none';
|
|
8
|
+
|
|
9
|
+
type ChatbotSettings = {
|
|
10
|
+
provider: Provider;
|
|
11
|
+
model: string;
|
|
12
|
+
searchMode: SearchMode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const CACHE_KEY = 'chatbot_settings';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SETTINGS: ChatbotSettings = {
|
|
18
|
+
provider: 'openai',
|
|
19
|
+
model: 'gpt-4.1-nano',
|
|
20
|
+
searchMode: 'tavily',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function useChatbotSettings() {
|
|
24
|
+
const [settings, setSettings] = useState<ChatbotSettings>(DEFAULT_SETTINGS);
|
|
25
|
+
const [isLoaded, setIsLoaded] = useState(false);
|
|
26
|
+
|
|
27
|
+
// Load settings from cache on mount
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const loadSettings = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const cachedSettings =
|
|
32
|
+
await defaultCacheService.getItem<ChatbotSettings>(CACHE_KEY);
|
|
33
|
+
if (cachedSettings) {
|
|
34
|
+
setSettings(cachedSettings);
|
|
35
|
+
}
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('[useChatbotSettings] Failed to load settings:', error);
|
|
38
|
+
} finally {
|
|
39
|
+
setIsLoaded(true);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
loadSettings();
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Save settings to cache whenever they change
|
|
47
|
+
const updateSettings = useCallback(
|
|
48
|
+
async (newSettings: Partial<ChatbotSettings>) => {
|
|
49
|
+
const updatedSettings = { ...settings, ...newSettings };
|
|
50
|
+
setSettings(updatedSettings);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await defaultCacheService.setItem(CACHE_KEY, updatedSettings);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('[useChatbotSettings] Failed to save settings:', error);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
[settings],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const updateProvider = useCallback(
|
|
62
|
+
(provider: Provider) => {
|
|
63
|
+
updateSettings({ provider });
|
|
64
|
+
},
|
|
65
|
+
[updateSettings],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const updateModel = useCallback(
|
|
69
|
+
(model: string) => {
|
|
70
|
+
updateSettings({ model });
|
|
71
|
+
},
|
|
72
|
+
[updateSettings],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const updateSearchMode = useCallback(
|
|
76
|
+
(searchMode: SearchMode) => {
|
|
77
|
+
updateSettings({ searchMode });
|
|
78
|
+
},
|
|
79
|
+
[updateSettings],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
settings,
|
|
84
|
+
isLoaded,
|
|
85
|
+
updateProvider,
|
|
86
|
+
updateModel,
|
|
87
|
+
updateSearchMode,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Id } from '@vibefast/backend/_generated/dataModel';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { chatbotApi } from '@/api-client/chatbot';
|
|
5
|
+
import type {
|
|
6
|
+
AppMessage,
|
|
7
|
+
ConversationMessageFromDB,
|
|
8
|
+
} from '@/features/chatbot/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to manage conversation state and load historical messages
|
|
12
|
+
*/
|
|
13
|
+
export const useConversation = (userId?: Id<'users'>) => {
|
|
14
|
+
const getOrCreateConversation = chatbotApi.useGetOrCreateConversation();
|
|
15
|
+
const [conversationId, setConversationId] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const initializeConversation = async () => {
|
|
19
|
+
if (userId && !conversationId) {
|
|
20
|
+
console.log(
|
|
21
|
+
'[Client useConversation] Initializing conversation for user:',
|
|
22
|
+
userId,
|
|
23
|
+
);
|
|
24
|
+
try {
|
|
25
|
+
const defaultConversationId = await getOrCreateConversation({});
|
|
26
|
+
setConversationId(defaultConversationId);
|
|
27
|
+
console.log(
|
|
28
|
+
'[Client useConversation] Conversation ID set:',
|
|
29
|
+
defaultConversationId,
|
|
30
|
+
);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(
|
|
33
|
+
'[Client useConversation] Failed to get/create conversation:',
|
|
34
|
+
error,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
initializeConversation();
|
|
40
|
+
}, [userId, conversationId, getOrCreateConversation]);
|
|
41
|
+
|
|
42
|
+
const historicalMessagesResult = chatbotApi.useListMessages(
|
|
43
|
+
conversationId ? (conversationId as Id<'conversations'>) : undefined,
|
|
44
|
+
{ numItems: 50, cursor: null },
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const initialMessages = useMemo((): AppMessage[] => {
|
|
48
|
+
if (!historicalMessagesResult?.page) {
|
|
49
|
+
console.log('[Client useConversation] No historical messages.');
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
console.log(
|
|
53
|
+
'[Client useConversation] Processing historical messages count:',
|
|
54
|
+
historicalMessagesResult.page.length,
|
|
55
|
+
);
|
|
56
|
+
return historicalMessagesResult.page.reverse().map(
|
|
57
|
+
(msg: ConversationMessageFromDB): AppMessage => ({
|
|
58
|
+
id: msg._id,
|
|
59
|
+
role: msg.authorType === 'user' ? 'user' : 'assistant',
|
|
60
|
+
content: msg.text || '',
|
|
61
|
+
createdAt: new Date(msg._creationTime),
|
|
62
|
+
attachments: msg.attachments,
|
|
63
|
+
toolCalls: msg.toolCalls,
|
|
64
|
+
metadata: msg.metadata,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
}, [historicalMessagesResult]);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (initialMessages.length > 0) {
|
|
71
|
+
console.log(
|
|
72
|
+
'[Client useConversation] Initial AppMessages loaded for useChat:',
|
|
73
|
+
initialMessages.length,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}, [initialMessages]);
|
|
77
|
+
|
|
78
|
+
return { conversationId, initialMessages };
|
|
79
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { Alert } from 'react-native';
|
|
4
|
+
|
|
5
|
+
// Basic image type without upload state (added later in chat-input-bar)
|
|
6
|
+
export type BasicSelectedImage = {
|
|
7
|
+
uri: string;
|
|
8
|
+
type: string;
|
|
9
|
+
fileName?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const useImagePicker = () => {
|
|
13
|
+
const handleImagePicker = useCallback(async (): Promise<
|
|
14
|
+
BasicSelectedImage[]
|
|
15
|
+
> => {
|
|
16
|
+
try {
|
|
17
|
+
// Request permissions
|
|
18
|
+
const { status } =
|
|
19
|
+
await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
20
|
+
if (status !== 'granted') {
|
|
21
|
+
Alert.alert(
|
|
22
|
+
'Permission Required',
|
|
23
|
+
'Sorry, we need camera roll permissions to upload images.',
|
|
24
|
+
);
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Launch image picker with multiple selection enabled
|
|
29
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
30
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
31
|
+
allowsMultipleSelection: true,
|
|
32
|
+
allowsEditing: false,
|
|
33
|
+
aspect: [4, 3],
|
|
34
|
+
quality: 0.8,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!result.canceled && result.assets && result.assets.length > 0) {
|
|
38
|
+
return result.assets.map((asset) => ({
|
|
39
|
+
uri: asset.uri,
|
|
40
|
+
type: asset.mimeType || 'image/jpeg',
|
|
41
|
+
fileName: asset.fileName || undefined,
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return [];
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error('Error picking images:', error);
|
|
48
|
+
Alert.alert('Error', 'Failed to pick images. Please try again.');
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
const handleCamera =
|
|
54
|
+
useCallback(async (): Promise<BasicSelectedImage | null> => {
|
|
55
|
+
try {
|
|
56
|
+
// Request permissions
|
|
57
|
+
const { status } = await ImagePicker.requestCameraPermissionsAsync();
|
|
58
|
+
if (status !== 'granted') {
|
|
59
|
+
Alert.alert(
|
|
60
|
+
'Permission Required',
|
|
61
|
+
'Sorry, we need camera permissions to take photos.',
|
|
62
|
+
);
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Launch camera
|
|
67
|
+
const result = await ImagePicker.launchCameraAsync({
|
|
68
|
+
allowsEditing: true,
|
|
69
|
+
aspect: [4, 3],
|
|
70
|
+
quality: 0.8,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (!result.canceled && result.assets[0]) {
|
|
74
|
+
const asset = result.assets[0];
|
|
75
|
+
return {
|
|
76
|
+
uri: asset.uri,
|
|
77
|
+
type: asset.mimeType || 'image/jpeg',
|
|
78
|
+
fileName: asset.fileName || undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Error taking photo:', error);
|
|
85
|
+
Alert.alert('Error', 'Failed to take photo. Please try again.');
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
const showImageSourceOptions = useCallback(
|
|
91
|
+
(onImagesSelected: (images: BasicSelectedImage[]) => void) => {
|
|
92
|
+
Alert.alert('Select Image', 'Choose an image source:', [
|
|
93
|
+
{
|
|
94
|
+
text: 'Camera',
|
|
95
|
+
onPress: async () => {
|
|
96
|
+
const image = await handleCamera();
|
|
97
|
+
if (image) {
|
|
98
|
+
onImagesSelected([image]);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
text: 'Photo Library',
|
|
104
|
+
onPress: async () => {
|
|
105
|
+
const images = await handleImagePicker();
|
|
106
|
+
if (images.length > 0) {
|
|
107
|
+
onImagesSelected(images);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
112
|
+
]);
|
|
113
|
+
},
|
|
114
|
+
[handleCamera, handleImagePicker],
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
handleImagePicker,
|
|
119
|
+
handleCamera,
|
|
120
|
+
showImageSourceOptions,
|
|
121
|
+
};
|
|
122
|
+
};
|