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,161 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { Keyboard } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
useAnimatedStyle,
|
|
5
|
+
useSharedValue,
|
|
6
|
+
withTiming,
|
|
7
|
+
} from 'react-native-reanimated';
|
|
8
|
+
import { runOnUI } from 'react-native-worklets';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Hook to coordinate keyboard animations and padding.
|
|
12
|
+
* Eliminates race conditions in keyboard show/hide animations.
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Smooth 300ms animations using Reanimated
|
|
16
|
+
* - Cancels pending animations to prevent stacking
|
|
17
|
+
* - Synchronized timing with message submission
|
|
18
|
+
* - Automatic cleanup on unmount
|
|
19
|
+
* - 60fps animations using runOnUI
|
|
20
|
+
*
|
|
21
|
+
* Requirements addressed:
|
|
22
|
+
* - 3.1: Animate padding smoothly over 300ms
|
|
23
|
+
* - 3.2: Reset padding to zero with synchronized timing
|
|
24
|
+
* - 3.3: Dismiss keyboard and reset padding atomically on message send
|
|
25
|
+
* - 3.4: Cancel pending animations to prevent stacking
|
|
26
|
+
* - 3.5: Prioritize submission action over animation completion
|
|
27
|
+
*
|
|
28
|
+
* @returns Object containing animated padding value, reset function, and animated style
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const keyboardCoordinator = useKeyboardCoordinator();
|
|
33
|
+
*
|
|
34
|
+
* // Use animated style for keyboard padding
|
|
35
|
+
* <Animated.View style={keyboardCoordinator.keyboardPadding} />
|
|
36
|
+
*
|
|
37
|
+
* // Reset padding when sending message
|
|
38
|
+
* const handleSubmit = async () => {
|
|
39
|
+
* await sendMessage();
|
|
40
|
+
* keyboardCoordinator.resetPadding();
|
|
41
|
+
* };
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useKeyboardCoordinator() {
|
|
45
|
+
// Animated padding value for smooth transitions
|
|
46
|
+
const height = useSharedValue(0);
|
|
47
|
+
|
|
48
|
+
// Pending timeout reference for cancellation
|
|
49
|
+
const pendingTimeout = useRef<NodeJS.Timeout | null>(null);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Cancel any pending animations.
|
|
53
|
+
* Prevents animation stacking when rapid keyboard events occur.
|
|
54
|
+
*/
|
|
55
|
+
const cancelPending = useCallback(() => {
|
|
56
|
+
if (pendingTimeout.current) {
|
|
57
|
+
clearTimeout(pendingTimeout.current);
|
|
58
|
+
pendingTimeout.current = null;
|
|
59
|
+
}
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reset padding to zero with smooth animation.
|
|
64
|
+
* Uses runOnUI for 60fps performance.
|
|
65
|
+
* Cancels any pending animations before starting new one.
|
|
66
|
+
*/
|
|
67
|
+
const resetPadding = useCallback(() => {
|
|
68
|
+
cancelPending();
|
|
69
|
+
|
|
70
|
+
runOnUI(() => {
|
|
71
|
+
'worklet';
|
|
72
|
+
height.value = withTiming(0, { duration: 300 });
|
|
73
|
+
})();
|
|
74
|
+
}, [height, cancelPending]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Handle keyboard show event.
|
|
78
|
+
* Animates padding to match keyboard height.
|
|
79
|
+
*
|
|
80
|
+
* @param keyboardHeight - Height of the keyboard in pixels
|
|
81
|
+
*/
|
|
82
|
+
const onKeyboardShow = useCallback(
|
|
83
|
+
(keyboardHeight: number) => {
|
|
84
|
+
cancelPending();
|
|
85
|
+
|
|
86
|
+
runOnUI(() => {
|
|
87
|
+
'worklet';
|
|
88
|
+
height.value = withTiming(keyboardHeight, { duration: 300 });
|
|
89
|
+
})();
|
|
90
|
+
},
|
|
91
|
+
[height, cancelPending],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Handle keyboard hide event.
|
|
96
|
+
* Resets padding to zero with animation.
|
|
97
|
+
*/
|
|
98
|
+
const onKeyboardHide = useCallback(() => {
|
|
99
|
+
resetPadding();
|
|
100
|
+
}, [resetPadding]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Animated style for keyboard padding.
|
|
104
|
+
* Apply this to a View component to create dynamic spacing.
|
|
105
|
+
*/
|
|
106
|
+
const keyboardPadding = useAnimatedStyle(() => {
|
|
107
|
+
return {
|
|
108
|
+
height: height.value,
|
|
109
|
+
};
|
|
110
|
+
}, [height]);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Set up keyboard event listeners.
|
|
114
|
+
* Uses keyboardWillShow for instant response (iOS) and keyboardDidShow as fallback (Android).
|
|
115
|
+
* Automatically cleans up on unmount.
|
|
116
|
+
*/
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
// Use 'Will' events for instant response on iOS, 'Did' events as fallback for Android
|
|
119
|
+
const showListener = Keyboard.addListener('keyboardWillShow', (e) => {
|
|
120
|
+
onKeyboardShow(e.endCoordinates.height);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const hideListener = Keyboard.addListener(
|
|
124
|
+
'keyboardWillHide',
|
|
125
|
+
onKeyboardHide,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Fallback for Android (doesn't have 'Will' events)
|
|
129
|
+
const showListenerAndroid = Keyboard.addListener('keyboardDidShow', (e) => {
|
|
130
|
+
onKeyboardShow(e.endCoordinates.height);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const hideListenerAndroid = Keyboard.addListener(
|
|
134
|
+
'keyboardDidHide',
|
|
135
|
+
onKeyboardHide,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
showListener.remove();
|
|
140
|
+
hideListener.remove();
|
|
141
|
+
showListenerAndroid.remove();
|
|
142
|
+
hideListenerAndroid.remove();
|
|
143
|
+
cancelPending();
|
|
144
|
+
};
|
|
145
|
+
}, [onKeyboardShow, onKeyboardHide, cancelPending]);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
// Animated values
|
|
149
|
+
height,
|
|
150
|
+
keyboardPadding,
|
|
151
|
+
|
|
152
|
+
// Methods
|
|
153
|
+
resetPadding,
|
|
154
|
+
cancelPending,
|
|
155
|
+
onKeyboardShow,
|
|
156
|
+
onKeyboardHide,
|
|
157
|
+
|
|
158
|
+
// Refs
|
|
159
|
+
pendingTimeout,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { FlashListRef } from '@shopify/flash-list';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
|
|
4
|
+
|
|
5
|
+
const NEAR_BOTTOM_THRESHOLD = 120;
|
|
6
|
+
const SCROLL_DEBOUNCE_MS = 32;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Smart scroll manager for chat message lists.
|
|
10
|
+
* Handles auto-scrolling based on user intent and scroll position.
|
|
11
|
+
*
|
|
12
|
+
* Features:
|
|
13
|
+
* - Auto-scroll when user is near bottom (within 120px threshold)
|
|
14
|
+
* - Cancel auto-scroll when user manually scrolls up
|
|
15
|
+
* - Always scroll on user message send
|
|
16
|
+
* - Track drag state to prevent scroll interruption
|
|
17
|
+
* - Debounced scroll scheduling (32ms) for performance
|
|
18
|
+
*
|
|
19
|
+
* Requirements addressed:
|
|
20
|
+
* - 4.1: Auto-scroll when near bottom (120px threshold)
|
|
21
|
+
* - 4.2: Don't auto-scroll when user scrolls up to read history
|
|
22
|
+
* - 4.3: Always scroll to bottom when user sends a message
|
|
23
|
+
* - 4.4: Maintain scroll position during streaming if reading history
|
|
24
|
+
* - 4.5: Cancel pending auto-scroll operations when user drags
|
|
25
|
+
*
|
|
26
|
+
* @param flashListRef - Reference to the FlashList component
|
|
27
|
+
* @returns Object containing scroll state refs, methods, and event handlers
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```tsx
|
|
31
|
+
* const flashListRef = useRef<FlashListRef<Message> | null>(null);
|
|
32
|
+
* const scrollManager = useSmartScrollManager(flashListRef);
|
|
33
|
+
*
|
|
34
|
+
* // In useEffect for message updates
|
|
35
|
+
* useEffect(() => {
|
|
36
|
+
* scrollManager.handleMessagesUpdate(messages, previousMessageIds, { logTelemetry: true });
|
|
37
|
+
* previousMessageIds.current = messages.map(m => m.id);
|
|
38
|
+
* }, [messages, scrollManager]);
|
|
39
|
+
*
|
|
40
|
+
* // In FlashList props
|
|
41
|
+
* <FlashList
|
|
42
|
+
* onScroll={scrollManager.onScroll}
|
|
43
|
+
* onScrollBeginDrag={scrollManager.onScrollBeginDrag}
|
|
44
|
+
* onScrollEndDrag={scrollManager.onScrollEndDrag}
|
|
45
|
+
* onMomentumScrollBegin={scrollManager.onMomentumScrollBegin}
|
|
46
|
+
* onMomentumScrollEnd={scrollManager.onMomentumScrollEnd}
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export function useSmartScrollManager<T>(
|
|
51
|
+
flashListRef: React.RefObject<FlashListRef<T> | null>,
|
|
52
|
+
) {
|
|
53
|
+
// Track if user is near bottom (within 120px)
|
|
54
|
+
const isNearBottom = useRef(true);
|
|
55
|
+
|
|
56
|
+
// Track if user is actively dragging
|
|
57
|
+
const isDragging = useRef(false);
|
|
58
|
+
|
|
59
|
+
// Pending scroll timeout for debouncing
|
|
60
|
+
const pendingScroll = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cancel any pending scroll operation
|
|
64
|
+
*/
|
|
65
|
+
const cancelScroll = useCallback(() => {
|
|
66
|
+
if (pendingScroll.current) {
|
|
67
|
+
clearTimeout(pendingScroll.current);
|
|
68
|
+
pendingScroll.current = null;
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scroll to bottom immediately
|
|
74
|
+
* Includes null check to prevent crashes when component unmounts
|
|
75
|
+
*/
|
|
76
|
+
const scrollToBottom = useCallback(() => {
|
|
77
|
+
if (flashListRef.current) {
|
|
78
|
+
try {
|
|
79
|
+
flashListRef.current.scrollToEnd({ animated: true });
|
|
80
|
+
isNearBottom.current = true;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
// Silently catch scroll errors (e.g., if list is unmounting)
|
|
83
|
+
console.warn('[ScrollManager] Scroll failed:', error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}, [flashListRef]);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Schedule scroll to bottom with 32ms debounce
|
|
90
|
+
* Cancels any pending scroll before scheduling new one
|
|
91
|
+
* This prevents rapid scroll calls during streaming
|
|
92
|
+
*/
|
|
93
|
+
const scheduleScroll = useCallback(() => {
|
|
94
|
+
// If already pending, don't schedule another one
|
|
95
|
+
if (pendingScroll.current) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
pendingScroll.current = setTimeout(() => {
|
|
100
|
+
pendingScroll.current = null;
|
|
101
|
+
scrollToBottom();
|
|
102
|
+
}, SCROLL_DEBOUNCE_MS);
|
|
103
|
+
}, [scrollToBottom]);
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Handle scroll events to track near-bottom state
|
|
107
|
+
* Updates isNearBottom based on 120px threshold
|
|
108
|
+
*/
|
|
109
|
+
const onScroll = useCallback(
|
|
110
|
+
(event: NativeSyntheticEvent<NativeScrollEvent>) => {
|
|
111
|
+
const { contentOffset, contentSize, layoutMeasurement } =
|
|
112
|
+
event.nativeEvent;
|
|
113
|
+
const visibleBottom = contentOffset.y + layoutMeasurement.height;
|
|
114
|
+
const distanceFromBottom = Math.max(
|
|
115
|
+
contentSize.height - visibleBottom,
|
|
116
|
+
0,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
isNearBottom.current = distanceFromBottom < NEAR_BOTTOM_THRESHOLD;
|
|
120
|
+
},
|
|
121
|
+
[],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Handle scroll begin drag - cancel pending scrolls
|
|
126
|
+
*/
|
|
127
|
+
const onScrollBeginDrag = useCallback(() => {
|
|
128
|
+
isDragging.current = true;
|
|
129
|
+
cancelScroll();
|
|
130
|
+
}, [cancelScroll]);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handle scroll end drag - mark dragging as complete
|
|
134
|
+
*/
|
|
135
|
+
const onScrollEndDrag = useCallback(() => {
|
|
136
|
+
isDragging.current = false;
|
|
137
|
+
}, []);
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Handle momentum scroll begin - treat as dragging
|
|
141
|
+
*/
|
|
142
|
+
const onMomentumScrollBegin = useCallback(() => {
|
|
143
|
+
isDragging.current = true;
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Handle momentum scroll end - mark dragging as complete
|
|
148
|
+
*/
|
|
149
|
+
const onMomentumScrollEnd = useCallback(() => {
|
|
150
|
+
isDragging.current = false;
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Determine if should auto-scroll based on current state
|
|
155
|
+
* Always scrolls on user message send
|
|
156
|
+
* Otherwise only scrolls if user is near bottom and not dragging
|
|
157
|
+
*/
|
|
158
|
+
const shouldAutoScroll = useCallback((userSentMessage: boolean): boolean => {
|
|
159
|
+
if (userSentMessage) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return !isDragging.current && isNearBottom.current;
|
|
163
|
+
}, []);
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Cleanup pending scrolls on unmount
|
|
167
|
+
*/
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
return () => {
|
|
170
|
+
cancelScroll();
|
|
171
|
+
};
|
|
172
|
+
}, [cancelScroll]);
|
|
173
|
+
|
|
174
|
+
// Memoize the return object to prevent unnecessary re-renders
|
|
175
|
+
return useMemo(
|
|
176
|
+
() => ({
|
|
177
|
+
// State refs
|
|
178
|
+
isNearBottom,
|
|
179
|
+
isDragging,
|
|
180
|
+
pendingScroll,
|
|
181
|
+
|
|
182
|
+
// Methods
|
|
183
|
+
scheduleScroll,
|
|
184
|
+
cancelScroll,
|
|
185
|
+
scrollToBottom,
|
|
186
|
+
shouldAutoScroll,
|
|
187
|
+
|
|
188
|
+
// Event handlers
|
|
189
|
+
onScroll,
|
|
190
|
+
onScrollBeginDrag,
|
|
191
|
+
onScrollEndDrag,
|
|
192
|
+
onMomentumScrollBegin,
|
|
193
|
+
onMomentumScrollEnd,
|
|
194
|
+
}),
|
|
195
|
+
[
|
|
196
|
+
scheduleScroll,
|
|
197
|
+
cancelScroll,
|
|
198
|
+
scrollToBottom,
|
|
199
|
+
shouldAutoScroll,
|
|
200
|
+
onScroll,
|
|
201
|
+
onScrollBeginDrag,
|
|
202
|
+
onScrollEndDrag,
|
|
203
|
+
onMomentumScrollBegin,
|
|
204
|
+
onMomentumScrollEnd,
|
|
205
|
+
],
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Import directly to avoid circular dependencies
|
|
2
|
+
import { DEFAULT_MODELS, MODELS } from './models';
|
|
3
|
+
import type { ModelConfig, ModelId, Provider } from './types';
|
|
4
|
+
|
|
5
|
+
// Re-export models and model functions
|
|
6
|
+
export { DEFAULT_MODELS, getModelDescription, MODELS } from './models';
|
|
7
|
+
// Re-export provider functions
|
|
8
|
+
export {
|
|
9
|
+
getAllProviders,
|
|
10
|
+
getProviderConfig,
|
|
11
|
+
getProviderDisplayName,
|
|
12
|
+
getProviderIcon,
|
|
13
|
+
PROVIDER_CONFIGS,
|
|
14
|
+
} from './providers';
|
|
15
|
+
// Re-export types
|
|
16
|
+
export type {
|
|
17
|
+
ModelCategory,
|
|
18
|
+
ModelConfig,
|
|
19
|
+
ModelId,
|
|
20
|
+
ModelOptions,
|
|
21
|
+
ModelSpeed,
|
|
22
|
+
Provider,
|
|
23
|
+
ProviderConfig,
|
|
24
|
+
} from './types';
|
|
25
|
+
|
|
26
|
+
// Additional helper functions
|
|
27
|
+
export const getModelById = (id: string): ModelConfig | undefined => {
|
|
28
|
+
return MODELS.find((model) => model.id === id);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const getModelsByProvider = (provider: Provider): ModelConfig[] => {
|
|
32
|
+
return MODELS.filter((model) => model.provider === provider);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const getPremiumModels = (): ModelConfig[] => {
|
|
36
|
+
return MODELS.filter((model) => model.category === 'premium');
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const getFreeModels = (): ModelConfig[] => {
|
|
40
|
+
return MODELS.filter((model) => model.category === 'free');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const getDefaultModel = (provider: Provider): ModelConfig => {
|
|
44
|
+
const defaultId = DEFAULT_MODELS[provider];
|
|
45
|
+
const model = getModelById(defaultId);
|
|
46
|
+
if (!model) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Default model ${defaultId} not found for provider ${provider}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return model;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const isPremiumModel = (modelId: string): boolean => {
|
|
55
|
+
const model = getModelById(modelId);
|
|
56
|
+
return model?.category === 'premium';
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const getAllModelIds = (): string[] => {
|
|
60
|
+
return MODELS.map((model: any) => model.id);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const getModelsByCategory = (
|
|
64
|
+
category: 'premium' | 'free' | 'specialized',
|
|
65
|
+
) => {
|
|
66
|
+
return MODELS.filter((model: any) => model.category === category);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const getModelProvider = (modelId: string): string | undefined => {
|
|
70
|
+
const model = getModelById(modelId);
|
|
71
|
+
return model?.provider;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Backward compatibility exports (to match old API) - lazy evaluation
|
|
75
|
+
export const ALL_MODELS = (): string[] => {
|
|
76
|
+
return MODELS.map((model: any) => model.id);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const PREMIUM_MODELS = (): string[] => {
|
|
80
|
+
return MODELS.filter((model: any) => model.category === 'premium').map(
|
|
81
|
+
(model: any) => model.id,
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Legacy type for backward compatibility
|
|
86
|
+
export type ModelType = ModelId;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { ModelConfig, Provider } from './types';
|
|
2
|
+
|
|
3
|
+
export const MODELS: ModelConfig[] = [
|
|
4
|
+
// Claude Models
|
|
5
|
+
{
|
|
6
|
+
id: 'claude-4-sonnet',
|
|
7
|
+
name: 'Claude 4 Sonnet',
|
|
8
|
+
provider: 'claude',
|
|
9
|
+
description: 'Most capable • Great at coding',
|
|
10
|
+
category: 'premium',
|
|
11
|
+
capabilities: ['coding', 'reasoning', 'analysis', 'writing'],
|
|
12
|
+
contextWindow: 200000,
|
|
13
|
+
speed: 'medium',
|
|
14
|
+
released: '2024',
|
|
15
|
+
tags: ['latest', 'most-capable'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'claude-4.5-sonnet',
|
|
19
|
+
name: 'Claude 4.5 Sonnet',
|
|
20
|
+
provider: 'claude',
|
|
21
|
+
description: 'Fast & efficient • Perfect for chat',
|
|
22
|
+
category: 'premium',
|
|
23
|
+
capabilities: ['chat', 'reasoning', 'coding', 'analysis'],
|
|
24
|
+
contextWindow: 200000,
|
|
25
|
+
speed: 'fast',
|
|
26
|
+
released: '2024',
|
|
27
|
+
tags: ['balanced', 'chat-optimized'],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
// OpenAI Models
|
|
31
|
+
{
|
|
32
|
+
id: 'gpt-5',
|
|
33
|
+
name: 'GPT-5',
|
|
34
|
+
provider: 'openai',
|
|
35
|
+
description: 'For complex tasks and thinking',
|
|
36
|
+
category: 'premium',
|
|
37
|
+
capabilities: ['reasoning', 'coding', 'analysis', 'mathematics'],
|
|
38
|
+
contextWindow: 200000,
|
|
39
|
+
speed: 'medium',
|
|
40
|
+
released: '2024',
|
|
41
|
+
tags: ['most-capable', 'complex-tasks'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'gpt-5-mini',
|
|
45
|
+
name: 'GPT-5 Mini',
|
|
46
|
+
provider: 'openai',
|
|
47
|
+
description: 'Quick thinking & reliable',
|
|
48
|
+
category: 'free',
|
|
49
|
+
capabilities: ['chat', 'reasoning', 'coding', 'analysis'],
|
|
50
|
+
contextWindow: 128000,
|
|
51
|
+
speed: 'fast',
|
|
52
|
+
released: '2024',
|
|
53
|
+
tags: ['balanced', 'efficient'],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'gpt-5-nano',
|
|
57
|
+
name: 'GPT-5 Nano',
|
|
58
|
+
provider: 'openai',
|
|
59
|
+
description: 'Very fast • Great for basic work',
|
|
60
|
+
category: 'free',
|
|
61
|
+
capabilities: ['chat', 'basic-tasks', 'quick-responses'],
|
|
62
|
+
contextWindow: 16000,
|
|
63
|
+
speed: 'fast',
|
|
64
|
+
released: '2024',
|
|
65
|
+
tags: ['fastest', 'basic-tasks'],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'gpt-4o-mini',
|
|
69
|
+
name: 'GPT-4o Mini',
|
|
70
|
+
provider: 'openai',
|
|
71
|
+
description: 'Very fast • Great for basic work',
|
|
72
|
+
category: 'free',
|
|
73
|
+
capabilities: ['chat', 'basic-tasks', 'quick-responses'],
|
|
74
|
+
contextWindow: 16000,
|
|
75
|
+
speed: 'fast',
|
|
76
|
+
released: '2024',
|
|
77
|
+
tags: ['fastest', 'basic-tasks'],
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// Gemini Models
|
|
81
|
+
{
|
|
82
|
+
id: 'gemini-2.0-flash',
|
|
83
|
+
name: 'Gemini 2.0 Flash',
|
|
84
|
+
provider: 'gemini',
|
|
85
|
+
description: 'Latest model • Ultra fast',
|
|
86
|
+
category: 'free',
|
|
87
|
+
capabilities: ['chat', 'reasoning', 'multimodal', 'coding'],
|
|
88
|
+
contextWindow: 1000000,
|
|
89
|
+
speed: 'fast',
|
|
90
|
+
released: '2024',
|
|
91
|
+
tags: ['latest', 'multimodal', 'fast'],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'gemini-2.5-flash',
|
|
95
|
+
name: 'Gemini 2.5 Flash',
|
|
96
|
+
provider: 'gemini',
|
|
97
|
+
description: 'Balanced • Great performance',
|
|
98
|
+
category: 'free',
|
|
99
|
+
capabilities: ['chat', 'reasoning', 'multimodal', 'analysis'],
|
|
100
|
+
contextWindow: 1000000,
|
|
101
|
+
speed: 'fast',
|
|
102
|
+
released: '2024',
|
|
103
|
+
tags: ['balanced', 'multimodal'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'gemini-2.5-pro',
|
|
107
|
+
name: 'Gemini 2.5 Pro',
|
|
108
|
+
provider: 'gemini',
|
|
109
|
+
description: 'Advanced reasoning',
|
|
110
|
+
category: 'premium',
|
|
111
|
+
capabilities: ['reasoning', 'coding', 'mathematics', 'analysis'],
|
|
112
|
+
contextWindow: 2000000,
|
|
113
|
+
speed: 'medium',
|
|
114
|
+
released: '2024',
|
|
115
|
+
tags: ['most-capable', 'advanced-reasoning'],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
// Helper to get models by provider
|
|
120
|
+
export const getModelsByProvider = (provider: Provider): ModelConfig[] => {
|
|
121
|
+
return MODELS.filter((model) => model.provider === provider);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Helper to get premium models
|
|
125
|
+
export const getPremiumModels = (): ModelConfig[] => {
|
|
126
|
+
return MODELS.filter((model) => model.category === 'premium');
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Helper to get free models
|
|
130
|
+
export const getFreeModels = (): ModelConfig[] => {
|
|
131
|
+
return MODELS.filter((model) => model.category === 'free');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Helper to get model by ID
|
|
135
|
+
export const getModelById = (id: string): ModelConfig | undefined => {
|
|
136
|
+
return MODELS.find((model) => model.id === id);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Default models for each provider
|
|
140
|
+
export const DEFAULT_MODELS: Record<Provider, string> = {
|
|
141
|
+
claude: 'claude-4.5-sonnet',
|
|
142
|
+
openai: 'gpt-5-nano',
|
|
143
|
+
gemini: 'gemini-2.0-flash',
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Get default model for a provider
|
|
147
|
+
export const getDefaultModel = (provider: Provider): ModelConfig => {
|
|
148
|
+
const defaultId = DEFAULT_MODELS[provider];
|
|
149
|
+
const model = getModelById(defaultId);
|
|
150
|
+
if (!model) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Default model ${defaultId} not found for provider ${provider}`,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return model;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Get model description (single source of truth)
|
|
159
|
+
export const getModelDescription = (modelId: string): string => {
|
|
160
|
+
const model = getModelById(modelId);
|
|
161
|
+
return model?.description || 'AI model';
|
|
162
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ClaudeIcon } from '@/components/ui/icons/claude';
|
|
4
|
+
import { GeminiIcon } from '@/components/ui/icons/gemini';
|
|
5
|
+
import { OpenaiIcon } from '@/components/ui/icons/openai';
|
|
6
|
+
|
|
7
|
+
import type { Provider, ProviderConfig } from './types';
|
|
8
|
+
|
|
9
|
+
export const PROVIDER_CONFIGS: Record<Provider, ProviderConfig> = {
|
|
10
|
+
claude: {
|
|
11
|
+
id: 'claude',
|
|
12
|
+
name: 'claude',
|
|
13
|
+
displayName: 'Anthropic Claude',
|
|
14
|
+
icon: ClaudeIcon,
|
|
15
|
+
defaultModel: 'claude-4.5-sonnet',
|
|
16
|
+
description: 'Advanced AI assistant with strong reasoning capabilities',
|
|
17
|
+
website: 'https://anthropic.com',
|
|
18
|
+
color: '#D97706',
|
|
19
|
+
},
|
|
20
|
+
openai: {
|
|
21
|
+
id: 'openai',
|
|
22
|
+
name: 'openai',
|
|
23
|
+
displayName: 'OpenAI',
|
|
24
|
+
icon: OpenaiIcon,
|
|
25
|
+
defaultModel: 'gpt-5-nano',
|
|
26
|
+
description: 'Leading AI research lab and ChatGPT creator',
|
|
27
|
+
website: 'https://openai.com',
|
|
28
|
+
color: '#10A37F',
|
|
29
|
+
},
|
|
30
|
+
gemini: {
|
|
31
|
+
id: 'gemini',
|
|
32
|
+
name: 'gemini',
|
|
33
|
+
displayName: 'Google Gemini',
|
|
34
|
+
icon: GeminiIcon,
|
|
35
|
+
defaultModel: 'gemini-2.0-flash',
|
|
36
|
+
description: "Google's multimodal AI models",
|
|
37
|
+
website: 'https://gemini.google.com',
|
|
38
|
+
color: '#4285F4',
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Get provider config by ID
|
|
43
|
+
export const getProviderConfig = (provider: Provider): ProviderConfig => {
|
|
44
|
+
return PROVIDER_CONFIGS[provider];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Get all providers
|
|
48
|
+
export const getAllProviders = (): Provider[] => {
|
|
49
|
+
return Object.keys(PROVIDER_CONFIGS) as Provider[];
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Get provider icon component
|
|
53
|
+
export const getProviderIcon = (
|
|
54
|
+
provider: Provider,
|
|
55
|
+
): React.ComponentType<any> => {
|
|
56
|
+
return PROVIDER_CONFIGS[provider].icon;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Get provider display name
|
|
60
|
+
export const getProviderDisplayName = (provider: Provider): string => {
|
|
61
|
+
return PROVIDER_CONFIGS[provider].displayName;
|
|
62
|
+
};
|