diff --git a/apps/expo/app.config.ts b/apps/expo/app.config.ts index 34944b5ce9..7843eb4335 100644 --- a/apps/expo/app.config.ts +++ b/apps/expo/app.config.ts @@ -37,7 +37,7 @@ export default (): ExpoConfig => { name: getAppName(), slug: 'packrat', - version: '2.0.15', + version: '2.0.16', scheme: 'packrat', web: { bundler: 'metro', diff --git a/apps/expo/app/(app)/(tabs)/profile/index.tsx b/apps/expo/app/(app)/(tabs)/profile/index.tsx index 5b4a05bd21..4289f368b0 100644 --- a/apps/expo/app/(app)/(tabs)/profile/index.tsx +++ b/apps/expo/app/(app)/(tabs)/profile/index.tsx @@ -1,9 +1,10 @@ import type { AlertRef } from '@packrat/ui/nativewindui'; import { ActivityIndicator, - Alert, + Alert as AlertComponent, Avatar, AvatarFallback, + AvatarImage, Button, List, ListItem, @@ -17,16 +18,23 @@ import TabScreen from 'expo-app/components/TabScreen'; import { withAuthWall } from 'expo-app/features/auth/hocs'; import { useAuth } from 'expo-app/features/auth/hooks/useAuth'; import { useUser } from 'expo-app/features/auth/hooks/useUser'; +import { useImagePicker } from 'expo-app/features/packs/hooks/useImagePicker'; +import { uploadImage } from 'expo-app/features/packs/utils/uploadImage'; import { ProfileAuthWall } from 'expo-app/features/profile/components'; +import { useUpdateProfile } from 'expo-app/features/profile/hooks/useUpdateProfile'; import { cn } from 'expo-app/lib/cn'; import { hasUnsyncedChanges } from 'expo-app/lib/hasUnsyncedChanges'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; -import { Stack } from 'expo-router'; +import { buildPackTemplateItemImageUrl } from 'expo-app/lib/utils/buildPackTemplateItemImageUrl'; +import * as FileSystem from 'expo-file-system'; +import { router, Stack } from 'expo-router'; import * as Updates from 'expo-updates'; import { useRef, useState } from 'react'; -import { Platform, View } from 'react-native'; +import { Alert, Platform, TouchableOpacity, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; +const AVATAR_MAX_BYTES = 5 * 1024 * 1024; // 5 MB + function Profile() { const user = useUser(); const { t } = useTranslation(); @@ -50,6 +58,7 @@ function Profile() { { id: 'name', title: t('common.name'), + onPress: () => router.push('/(app)/(tabs)/profile/name'), ...(Platform.OS === 'ios' ? { value: displayName } : { subTitle: displayName }), }, { @@ -89,6 +98,7 @@ function Item({ info }: { info: ListRenderItemInfo }) { return ( {!!info.item.value && {info.item.value}} @@ -101,6 +111,11 @@ function Item({ info }: { info: ListRenderItemInfo }) { function ListHeaderComponent() { const user = useUser(); + const { updateProfile } = useUpdateProfile(); + const { pickImage } = useImagePicker(); + const [isUploading, setIsUploading] = useState(false); + const { t } = useTranslation(); + const initials = user?.firstName && user?.lastName ? `${user.firstName[0]}${user.lastName[0]}` @@ -113,21 +128,66 @@ function ListHeaderComponent() { const username = user?.email || ''; + // Build the full avatar URL from the stored R2 key or an absolute URL + const avatarUri = user?.avatarUrl ? buildPackTemplateItemImageUrl(user.avatarUrl) : null; + + async function handleAvatarPress() { + try { + const image = await pickImage(); + if (!image) return; + + // Validate file size before uploading (5 MB limit) + const info = await FileSystem.getInfoAsync(image.uri, { size: true }); + if (info.exists && info.size > AVATAR_MAX_BYTES) { + Alert.alert(t('errors.somethingWentWrong'), t('profile.imageTooLarge')); + return; + } + + setIsUploading(true); + const remoteFileName = await uploadImage(image.fileName, image.uri); + if (remoteFileName) { + const success = await updateProfile({ avatarUrl: remoteFileName }); + if (!success) { + Alert.alert(t('errors.somethingWentWrong'), t('errors.tryAgain')); + } + } + } catch (err) { + if (err instanceof Error && err.message === 'Permission to access media library was denied') { + Alert.alert(t('permissions.photoLibraryTitle'), t('permissions.photoLibraryMessage'), [ + { text: t('common.cancel'), style: 'cancel' }, + { text: t('permissions.openSettings'), onPress: () => Linking.openSettings() }, + ]); + } else { + Alert.alert(t('errors.somethingWentWrong'), t('errors.tryAgain')); + } + } finally { + setIsUploading(false); + } + } + return ( - - - - {initials} - - - + + + {avatarUri ? : null} + + + {initials} + + + + {isUploading && ( + + + + )} + {displayName} {username} @@ -214,7 +274,7 @@ function ListFooterComponent() { {t('auth.logOut')} )} - + ); } diff --git a/apps/expo/app/(app)/(tabs)/profile/name.tsx b/apps/expo/app/(app)/(tabs)/profile/name.tsx index d1fc92a19f..9da0faba40 100644 --- a/apps/expo/app/(app)/(tabs)/profile/name.tsx +++ b/apps/expo/app/(app)/(tabs)/profile/name.tsx @@ -1,35 +1,54 @@ import { Button, Form, FormItem, FormSection, Text, TextField } from '@packrat/ui/nativewindui'; +import { useUser } from 'expo-app/features/auth/hooks/useUser'; +import { useUpdateProfile } from 'expo-app/features/profile/hooks/useUpdateProfile'; import { cn } from 'expo-app/lib/cn'; import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { router, Stack } from 'expo-router'; import * as React from 'react'; -import { Platform, View } from 'react-native'; -import { KeyboardAwareScrollView, KeyboardController } from 'react-native-keyboard-controller'; +import { Alert, Platform, View } from 'react-native'; +import { KeyboardAwareScrollView } from 'react-native-keyboard-controller'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function NameScreen() { const insets = useSafeAreaInsets(); const { t } = useTranslation(); + const user = useUser(); + const { updateProfile, isLoading } = useUpdateProfile(); + + const initialFirst = React.useRef(user?.firstName || ''); + const initialLast = React.useRef(user?.lastName || ''); + const [form, setForm] = React.useState({ - first: 'Zach', - middle: 'Danger', - last: 'Nugent', + first: initialFirst.current, + last: initialLast.current, }); - function onChangeText(type: 'first' | 'middle' | 'last') { + function onChangeText(type: 'first' | 'last') { return (text: string) => { setForm((prev) => ({ ...prev, [type]: text })); }; } - function focusNext() { - KeyboardController.setFocusTo('next'); - } + const trimmedFirst = React.useMemo(() => form.first.trim(), [form.first]); + const trimmedLast = React.useMemo(() => form.last.trim(), [form.last]); const canSave = - (form.first !== 'Zach' || form.middle !== 'Danger' || form.last !== 'Nugent') && - !!form.first && - !!form.last; + (trimmedFirst !== initialFirst.current.trim() || trimmedLast !== initialLast.current.trim()) && + !!trimmedFirst && + !!trimmedLast; + + async function handleSave() { + if (!canSave || isLoading) return; + const success = await updateProfile({ + firstName: trimmedFirst, + lastName: trimmedLast, + }); + if (success) { + router.back(); + } else { + Alert.alert(t('errors.somethingWentWrong'), t('errors.tryAgain')); + } + } return ( <> @@ -42,9 +61,9 @@ export default function NameScreen() { ios: () => ( @@ -61,7 +80,7 @@ export default function NameScreen() { contentContainerStyle={{ paddingBottom: insets.bottom }} >
- + - - - {t('profile.middleNameLabel')}, - })} - placeholder={t('profile.optionalPlaceholder')} - value={form.middle} - onChangeText={onChangeText('middle')} - onSubmitEditing={focusNext} submitBehavior="submit" enterKeyHint="next" /> @@ -106,7 +108,7 @@ export default function NameScreen() { placeholder={t('profile.requiredPlaceholder')} value={form.last} onChangeText={onChangeText('last')} - onSubmitEditing={router.back} + onSubmitEditing={handleSave} enterKeyHint="done" /> @@ -114,9 +116,9 @@ export default function NameScreen() { {Platform.OS !== 'ios' && ( diff --git a/apps/expo/features/profile/hooks/useUpdateProfile.ts b/apps/expo/features/profile/hooks/useUpdateProfile.ts new file mode 100644 index 0000000000..d184571c6c --- /dev/null +++ b/apps/expo/features/profile/hooks/useUpdateProfile.ts @@ -0,0 +1,35 @@ +import { userStore } from 'expo-app/features/auth/store'; +import axiosInstance, { handleApiError } from 'expo-app/lib/api/client'; +import { useState } from 'react'; + +export type UpdateProfilePayload = { + firstName?: string; + lastName?: string; + email?: string; + avatarUrl?: string | null; +}; + +export function useUpdateProfile() { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const updateProfile = async (payload: UpdateProfilePayload): Promise => { + setIsLoading(true); + setError(null); + try { + const response = await axiosInstance.put('/api/user/profile', payload); + if (response.data?.user) { + userStore.set(response.data.user); + } + return true; + } catch (err) { + const { message } = handleApiError(err); + setError(message); + return false; + } finally { + setIsLoading(false); + } + }; + + return { updateProfile, isLoading, error }; +} diff --git a/apps/expo/features/profile/types.ts b/apps/expo/features/profile/types.ts index 84cb690450..16645c2203 100644 --- a/apps/expo/features/profile/types.ts +++ b/apps/expo/features/profile/types.ts @@ -5,6 +5,7 @@ export interface User { email: string; firstName: string; lastName: string; + avatarUrl?: string | null; role: 'USER' | 'ADMIN'; preferredWeightUnit: WeightUnit; } diff --git a/apps/expo/features/trips/screens/TripDetailScreen.tsx b/apps/expo/features/trips/screens/TripDetailScreen.tsx index 27691c449f..fddd285f42 100644 --- a/apps/expo/features/trips/screens/TripDetailScreen.tsx +++ b/apps/expo/features/trips/screens/TripDetailScreen.tsx @@ -12,7 +12,7 @@ import { useTranslation } from 'expo-app/lib/hooks/useTranslation'; import { assertDefined } from 'expo-app/utils/typeAssertions'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useRef } from 'react'; -import { ScrollView, View } from 'react-native'; +import { ScrollView, Share, View } from 'react-native'; import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useDetailedPacks } from '../../packs/hooks/useDetailedPacks'; diff --git a/apps/expo/lib/i18n/locales/en.json b/apps/expo/lib/i18n/locales/en.json index 09a5677416..c67eabe633 100644 --- a/apps/expo/lib/i18n/locales/en.json +++ b/apps/expo/lib/i18n/locales/en.json @@ -174,7 +174,8 @@ "optionalPlaceholder": "optional", "usernameFootnote": "Choose a unique identifier for your account.", "notificationsFootnote": "Receive communication including announcements, marketing, recommendations, and updates about products, services, and software.", - "dangerZone": "Danger Zone" + "dangerZone": "Danger Zone", + "imageTooLarge": "Image must be smaller than 5 MB" }, "navigation": { "dashboard": "Dashboard", @@ -832,13 +833,12 @@ "appTemplateFootnote": "Featured templates are shown to all users. Option is only available to admins.", "appTemplate": "Featured", "importFromTikTok": "Import from TikTok", - "importFromTikTokDescription": "Import gear from a TikTok slideshow post", + "importFromTikTokDescription": "Import gear from a TikTok video or slideshow post", "tiktokUrl": "TikTok URL", "tiktokUrlPlaceholder": "https://www.tiktok.com/@user/video/...", "generateFromTikTok": "Generate Template", "generatingFromTikTok": "Generating...", "tiktokUrlRequired": "TikTok URL is required", - "tiktokImageUrlsRequired": "At least one slideshow image URL is required", "tiktokImportSuccess": "Pack template created successfully!", "tiktokImportError": "Failed to generate template from TikTok. Please try again.", "templateAlreadyExists": "Template Already Exists", @@ -846,7 +846,7 @@ "tiktokImportDuplicateError": "A template already exists for this content.", "tiktokImportServiceError": "TikTok service is unavailable. Please try again later.", "tiktokImportAIError": "AI analysis failed. Please try again or contact support.", - "tiktokImportDescription": "Paste a TikTok slideshow URL below. AI will identify items and build a pack template using our catalog.", + "tiktokImportDescription": "Paste a TikTok video or slideshow URL below. AI will identify items and build a pack template using our catalog.", "viewExistingTemplate": "View", "creating": "Creating...", "updating": "Updating...", @@ -926,5 +926,10 @@ "by": "By", "updated": "Updated", "all": "All" + }, + "permissions": { + "photoLibraryTitle": "Photo Library Access Required", + "photoLibraryMessage": "PackRat needs access to your photo library to update your profile photo. Please enable it in Settings.", + "openSettings": "Open Settings" } } diff --git a/apps/expo/package.json b/apps/expo/package.json index 9c7a00470c..2c93e8c88e 100644 --- a/apps/expo/package.json +++ b/apps/expo/package.json @@ -1,6 +1,6 @@ { "name": "packrat-expo-app", - "version": "2.0.15", + "version": "2.0.16", "main": "expo-router/entry", "scripts": { "android": "APP_VARIANT=development expo run:android", @@ -102,7 +102,7 @@ "i18n-js": "^4.4.3", "jotai": "^2.12.2", "lodash.debounce": "^4.0.8", - "nativewind": "^4.1.23", + "nativewind": "^4.2.3", "radash": "^12.1.1", "react": "19.1.0", "react-dom": "19.1.0", diff --git a/apps/guides/package.json b/apps/guides/package.json index 9ec54825e1..830d0e3d72 100644 --- a/apps/guides/package.json +++ b/apps/guides/package.json @@ -1,6 +1,6 @@ { "name": "packrat-guides-app", - "version": "2.0.15", + "version": "2.0.16", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/landing/package.json b/apps/landing/package.json index 636becd57b..a9d003e0df 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -1,6 +1,6 @@ { "name": "packrat-landing-app", - "version": "2.0.15", + "version": "2.0.16", "private": true, "scripts": { "dev": "next dev", diff --git a/bun.lock b/bun.lock index 30866b6c50..352dd3fa20 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "packrat-monorepo", @@ -17,7 +16,7 @@ }, "apps/expo": { "name": "packrat-expo-app", - "version": "2.0.14", + "version": "2.0.15", "dependencies": { "@ai-sdk/react": "^2.0.11", "@expo/react-native-action-sheet": "^4.1.1", @@ -82,7 +81,7 @@ "i18n-js": "^4.4.3", "jotai": "^2.12.2", "lodash.debounce": "^4.0.8", - "nativewind": "^4.1.23", + "nativewind": "^4.2.3", "radash": "^12.1.1", "react": "19.1.0", "react-dom": "19.1.0", @@ -124,7 +123,7 @@ }, "apps/guides": { "name": "packrat-guides-app", - "version": "2.0.14", + "version": "2.0.15", "dependencies": { "@ai-sdk/openai": "^2.0.11", "@hookform/resolvers": "^3.10.0", @@ -203,7 +202,7 @@ }, "apps/landing": { "name": "packrat-landing-app", - "version": "2.0.14", + "version": "2.0.15", "dependencies": { "@emotion/is-prop-valid": "^1.3.1", "@hookform/resolvers": "^3.10.0", @@ -269,6 +268,7 @@ "packages/api": { "name": "@packrat/api", "dependencies": { + "@ai-sdk/google": "^2.0.62", "@ai-sdk/openai": "^2.0.11", "@ai-sdk/perplexity": "^2.0.1", "@aws-sdk/client-s3": "^3.787.0", @@ -316,9 +316,9 @@ }, "packages/ui": { "name": "@packrat/ui", - "version": "2.0.14", + "version": "2.0.15", "dependencies": { - "@packrat-ai/nativewindui": "^1.1.0", + "@packrat-ai/nativewindui": "^2.0.0", }, }, }, @@ -330,13 +330,15 @@ "@ai-sdk/gateway": ["@ai-sdk/gateway@1.0.5", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-GOhxiHm2nfuS618Ia13AWxEIhCsj5+tFaw6sjSO7pvMZT03QgFAJyX4xBYj+3i3mfIvw+yJOvyhVu1fI+pAHQA=="], + "@ai-sdk/google": ["@ai-sdk/google@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.22" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RUpgkG5dWsmkYQsluTdutXakFpyQQ1NvELnQ0KD1VWTNLHWD70fO0FOpOs1cQKeTe7PspcJSii9Zekpaepv6qA=="], + "@ai-sdk/openai": ["@ai-sdk/openai@2.0.11", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-t4i+vS825EC0Gc2DdTsC5UkXIu1ScOi363noTD8DuFZp6WFPHRnW6HCyEQKxEm6cNjv3BW89rdXWqq932IFJhA=="], "@ai-sdk/perplexity": ["@ai-sdk/perplexity@2.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-wOK5jFtUFapEvVerSysFethaQmozcBArWtVfWeAM5VY8VpuDeOqTQET2dP/qV6QB/b/sqP9Tmu2cF5K3oLFHfw=="], - "@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/provider": ["@ai-sdk/provider@2.0.1", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-KCUwswvsC5VsW2PWFqF8eJgSCu5Ysj7m1TxiHTVA6g7k360bk0RNQENT8KTMAYEs+8fWPD3Uu4dEmzGHc+jGng=="], - "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.22", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fFT1KfUUKktfAFm5mClJhS1oux9tP2qgzmEZVl5UdwltQ1LO/s8hd7znVrgKzivwv1s1FIPza0s9OpJaNB/vHw=="], "@ai-sdk/react": ["@ai-sdk/react@2.0.11", "", { "dependencies": { "@ai-sdk/provider-utils": "3.0.2", "ai": "5.0.11", "swr": "^2.2.5", "throttleit": "2.1.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.25.76 || ^4" }, "optionalPeers": ["zod"] }, "sha512-XL73e7RSOQjYRCJQ96sDY6TxrMJK9YBgI518E6Jy306BjRwy5XyY94e/DN71TE6VpiwDzxixlymfDK90Ro95Jg=="], @@ -946,7 +948,7 @@ "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], - "@packrat-ai/nativewindui": ["@packrat-ai/nativewindui@1.1.0", "https://npm.pkg.github.com/download/@packrat-ai/nativewindui/1.1.0/5c57f431925d2054857b76bb898aa64633559b4e", { "peerDependencies": { "@gorhom/bottom-sheet": "^5.1.2", "@react-native-community/datetimepicker": "8.4.4", "@react-native-community/slider": "5.0.1", "@react-native-picker/picker": "2.11.1", "@react-native-segmented-control/segmented-control": "2.5.7", "@react-navigation/drawer": "^7.1.1", "@react-navigation/elements": "^2.3.1", "@react-navigation/native": "^7.0.14", "@rn-primitives/alert-dialog": "^1.1.0", "@rn-primitives/avatar": "^1.1.0", "@rn-primitives/checkbox": "^1.1.0", "@rn-primitives/context-menu": "^1.1.0", "@rn-primitives/dropdown-menu": "^1.1.0", "@rn-primitives/hooks": "^1.1.0", "@rn-primitives/portal": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@roninoss/icons": "^0.0.4", "@shopify/flash-list": "2.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "expo-blur": "~15.0.8", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linear-gradient": "~15.0.8", "expo-navigation-bar": "~5.0.10", "expo-router": "~6.0.23", "expo-symbols": "~1.0.8", "nativewind": "^4.1.23", "react": "19.1.0", "react-native": "0.81.5", "react-native-keyboard-controller": "^1.16.7", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-uitextview": "^1.1.4", "rn-icon-mapper": "^0.0.1", "tailwind-merge": "^2.2.1" } }, "sha512-iCPNH/SSqSGbs6p9je3UA4BSEezq+XnZ5x5xj3BGqxS1d4H89Jv/jeqIm/789/Q/xgBTSlozvfFXRemMKCqiUA=="], + "@packrat-ai/nativewindui": ["@packrat-ai/nativewindui@2.0.1", "https://npm.pkg.github.com/download/@packrat-ai/nativewindui/2.0.1/c8f3e4e6113c8d464803f637dbfb6fe5fa9a5e36", { "peerDependencies": { "@expo/vector-icons": ">=15.0.0", "@gorhom/bottom-sheet": "^5.1.2", "@react-native-community/datetimepicker": "^8.4.0", "@react-native-community/slider": "^5.0.0", "@react-native-picker/picker": "^2.11.0", "@react-native-segmented-control/segmented-control": "^2.5.0", "@react-navigation/drawer": "^7.1.1", "@react-navigation/elements": "^2.3.1", "@react-navigation/native": "^7.0.14", "@rn-primitives/alert-dialog": "^1.1.0", "@rn-primitives/avatar": "^1.0.4", "@rn-primitives/checkbox": "^1.1.0", "@rn-primitives/context-menu": "^1.1.0", "@rn-primitives/dropdown-menu": "^1.1.0", "@rn-primitives/hooks": "^1.1.0", "@rn-primitives/portal": "^1.1.0", "@rn-primitives/slot": "^1.1.0", "@shopify/flash-list": "^2.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "expo-blur": "~15.0.8", "expo-device": "~8.0.0", "expo-glass-effect": "*", "expo-haptics": "~15.0.8", "expo-image": "~3.0.11", "expo-linear-gradient": "~15.0.8", "expo-navigation-bar": "~5.0.10", "expo-router": "~6.0.23", "expo-symbols": "~1.0.8", "nativewind": "^4.1.21", "react": ">=19.0.0", "react-native": ">=0.79.0", "react-native-keyboard-controller": "^1.16.7", "react-native-reanimated": ">=3.17.0", "react-native-safe-area-context": ">=5.4.0", "react-native-screens": ">=4.11.0", "react-native-uitextview": "^1.1.4", "rn-icon-mapper": "^0.0.1", "tailwind-merge": "^2.2.1" } }, "sha512-zMzFalxu6MKuBMIIDzeiMj/7wM9qn8kFkAbWm3IxBWTHHSsl/Zic053DCJZS1GQcY0Ke2Om3TmUTuBujERuwvA=="], "@packrat/api": ["@packrat/api@workspace:packages/api"], @@ -1358,7 +1360,7 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="], - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@stardazed/streams-text-encoding": ["@stardazed/streams-text-encoding@1.0.2", "", {}, "sha512-f2Z15BId3t44a/u21yYSGXFAkCyKocmAyduoAy7swnZ4xIfbaZlOWsgly/jDNNOuj6hYQN72UaBRe3Z/tOHfqg=="], @@ -2026,7 +2028,7 @@ "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], - "eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "exec-async": ["exec-async@2.2.0", "", {}, "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw=="], @@ -2056,12 +2058,16 @@ "expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.0", "", { "peerDependencies": { "expo": "*" } }, "sha512-BvAMPt6x+vyXpThsyjjOYyjwfjREV4OOpQkZ0tNl+nGpsPfcY9mc6DRACoWnH9KpLzyIt3BOgh3cuy/h/OxQjw=="], + "expo-device": ["expo-device@8.0.10", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "*" } }, "sha512-jd5BxjaF7382JkDMaC+P04aXXknB2UhWaVx5WiQKA05ugm/8GH5uaz9P9ckWdMKZGQVVEOC8MHaUADoT26KmFA=="], + "expo-eas-client": ["expo-eas-client@1.0.8", "", {}, "sha512-5or11NJhSeDoHHI6zyvQDW2cz/yFyE+1Cz8NTs5NK8JzC7J0JrkUgptWtxyfB6Xs/21YRNifd3qgbBN3hfKVgA=="], "expo-file-system": ["expo-file-system@19.0.21", "", { "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg=="], "expo-font": ["expo-font@14.0.11", "", { "dependencies": { "fontfaceobserver": "^2.1.0" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg=="], + "expo-glass-effect": ["expo-glass-effect@55.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-IvUjHb/4t6r2H/LXDjcQ4uDoHrmO2cLOvEb9leLavQ4HX5+P4LRtQrMDMlkWAn5Wo5DkLcG8+1CrQU2nqgogTA=="], + "expo-haptics": ["expo-haptics@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g=="], "expo-image": ["expo-image@3.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA=="], @@ -2202,9 +2208,9 @@ "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], - "gaxios": ["gaxios@7.1.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ=="], + "gaxios": ["gaxios@7.1.4", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA=="], - "gcp-metadata": ["gcp-metadata@7.0.1", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ=="], + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], "gel": ["gel@2.1.1", "", { "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", "env-paths": "^3.0.0", "semver": "^7.6.2", "shell-quote": "^1.8.1", "which": "^4.0.0" }, "bin": { "gel": "dist/cli.mjs" } }, "sha512-Newg9X7mRYskoBjSw70l1YnJ/ZGbq64VPyR821H5WVkTGpHG2O0mQILxCeUhxdYERLFY9B4tUyKLyf3uMTjtKw=="], @@ -2244,9 +2250,9 @@ "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], - "google-auth-library": ["google-auth-library@10.2.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^7.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A=="], + "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], - "google-logging-utils": ["google-logging-utils@1.1.1", "", {}, "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="], + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], @@ -2748,7 +2754,7 @@ "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - "nativewind": ["nativewind@4.1.23", "", { "dependencies": { "comment-json": "^4.2.5", "debug": "^4.3.7", "react-native-css-interop": "0.1.22" }, "peerDependencies": { "tailwindcss": ">3.3.0" } }, "sha512-oLX3suGI6ojQqWxdQezOSM5GmJ4KvMnMtmaSMN9Ggb5j7ysFt4nHxb1xs8RDjZR7BWc+bsetNJU8IQdQMHqRpg=="], + "nativewind": ["nativewind@4.2.3", "", { "dependencies": { "comment-json": "^4.2.5", "debug": "^4.3.7", "react-native-css-interop": "0.2.3" }, "peerDependencies": { "tailwindcss": ">3.3.0" } }, "sha512-HglF1v6A8CqBFpXWs0d3yf4qQGurrreLuyE8FTRI/VDH8b0npZa2SDG5tviTkLiBg0s5j09mQALZOjxuocgMLA=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -3008,7 +3014,7 @@ "react-native": ["react-native@0.81.5", "", { "dependencies": { "@jest/create-cache-key-function": "^29.7.0", "@react-native/assets-registry": "0.81.5", "@react-native/codegen": "0.81.5", "@react-native/community-cli-plugin": "0.81.5", "@react-native/gradle-plugin": "0.81.5", "@react-native/js-polyfills": "0.81.5", "@react-native/normalize-colors": "0.81.5", "@react-native/virtualized-lists": "0.81.5", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-jest": "^29.7.0", "babel-plugin-syntax-hermes-parser": "0.29.1", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "glob": "^7.1.1", "invariant": "^2.2.4", "jest-environment-node": "^29.7.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.83.1", "metro-source-map": "^0.83.1", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.26.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.3", "yargs": "^17.6.2" }, "peerDependencies": { "@types/react": "^19.1.0", "react": "^19.1.0" }, "optionalPeers": ["@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw=="], - "react-native-css-interop": ["react-native-css-interop@0.1.22", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/traverse": "^7.23.0", "@babel/types": "^7.23.0", "debug": "^4.3.7", "lightningcss": "^1.27.0", "semver": "^7.6.3" }, "peerDependencies": { "react": ">=18", "react-native": "*", "react-native-reanimated": ">=3.6.2", "tailwindcss": "~3" } }, "sha512-Mu01e+H9G+fxSWvwtgWlF5MJBJC4VszTCBXopIpeR171lbeBInHb8aHqoqRPxmJpi3xIHryzqKFOJYAdk7PBxg=="], + "react-native-css-interop": ["react-native-css-interop@0.2.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.22.15", "@babel/traverse": "^7.23.0", "@babel/types": "^7.23.0", "debug": "^4.3.7", "lightningcss": "~1.27.0", "semver": "^7.6.3" }, "peerDependencies": { "react": ">=18", "react-native": "*", "react-native-reanimated": ">=3.6.2", "tailwindcss": "~3" } }, "sha512-wc+JI7iUfdFBqnE18HhMTtD0q9vkhuMczToA87UdHGWwMyxdT5sCcNy+i4KInPCE855IY0Ic8kLQqecAIBWz7w=="], "react-native-drawer-layout": ["react-native-drawer-layout@4.1.12", "", { "dependencies": { "use-latest-callback": "^0.2.4" }, "peerDependencies": { "react": ">= 18.2.0", "react-native": "*", "react-native-gesture-handler": ">= 2.0.0", "react-native-reanimated": ">= 2.0.0" } }, "sha512-oKolvp5seiUieG+RHGjpFe8rH8Ds24iW0QBl31TlCVOX7tdn42IQIBl5tuD1i7h3q+VqqnbcT+NB2dcJ5suZkw=="], @@ -3428,7 +3434,7 @@ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], - "ua-parser-js": ["ua-parser-js@1.0.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew=="], + "ua-parser-js": ["ua-parser-js@0.7.41", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg=="], "uc.micro": ["uc.micro@1.0.6", "", {}, "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="], @@ -3610,8 +3616,20 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], + "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + + "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + + "@ai-sdk/perplexity/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + "@ai-sdk/perplexity/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.1", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-/iP1sKc6UdJgGH98OCly7sWJKv+J9G47PnTjIj40IJMUQKwDrUMyf7zOOfRtPwSuNifYhSoJQ4s1WltI65gJ/g=="], + "@ai-sdk/react/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + "@apidevtools/json-schema-ref-parser/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3734,7 +3752,7 @@ "@manypkg/tools/js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], - "@packrat/api/@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + "@packrat/api/@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], @@ -3776,6 +3794,10 @@ "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.1.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg=="], + "ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + + "ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.2", "", { "dependencies": { "@ai-sdk/provider": "2.0.0", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.3", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.25.76 || ^4" } }, "sha512-0a5a6VafkV6+0irdpqnub8WE6qzG2VMsDBpXb9NQIz8c4TG8fI+GSTFIL9sqrLEwXrHdiRj7fwJsrir4jClL0w=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], @@ -3880,6 +3902,8 @@ "fbjs/promise": ["promise@7.3.1", "", { "dependencies": { "asap": "~2.0.3" } }, "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg=="], + "fbjs/ua-parser-js": ["ua-parser-js@1.0.40", "", { "bin": { "ua-parser-js": "script/cli.js" } }, "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], @@ -3890,6 +3914,8 @@ "flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "gtoken/gaxios": ["gaxios@7.1.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ=="], + "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3974,6 +4000,8 @@ "p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "packrat-expo-app/google-auth-library": ["google-auth-library@10.2.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^7.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-HMxFl2NfeHYnaL1HoRIN1XgorKS+6CDaM+z9LSSN+i/nKDDL4KFFEWogMXu7jV4HZQy2MsxpY+wA5XIf3w410A=="], + "packrat-guides-app/react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], "packrat-guides-app/react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], @@ -3998,6 +4026,12 @@ "react-native/ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], + "react-native-css-interop/@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "react-native-css-interop/@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "react-native-css-interop/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "react-native-web/@react-native/normalize-colors": ["@react-native/normalize-colors@0.74.89", "", {}, "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg=="], "react-native-web/memoize-one": ["memoize-one@6.0.0", "", {}, "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="], @@ -4078,6 +4112,24 @@ "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@ai-sdk/gateway/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + + "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@ai-sdk/openai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@ai-sdk/perplexity/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + + "@ai-sdk/react/@ai-sdk/provider-utils/@ai-sdk/provider": ["@ai-sdk/provider@2.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA=="], + + "@ai-sdk/react/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "@ai-sdk/react/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + "@apidevtools/json-schema-ref-parser/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "@aws-crypto/sha1-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], @@ -4262,7 +4314,7 @@ "@manypkg/tools/js-yaml/argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "@packrat/api/@types/bun/bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + "@packrat/api/@types/bun/bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], "@react-native/babel-preset/@babel/plugin-transform-classes/@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.27.1", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.27.1", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA=="], @@ -4276,6 +4328,10 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], + + "ai/@ai-sdk/provider-utils/eventsource-parser": ["eventsource-parser@3.0.3", "", {}, "sha512-nVpZkTMM9rF6AQ9gPJpFsNAMt48wIzB5TQgiTLdHiuO8XEDhUgZEhqKlZWXbIzo9VmJ/HvysHqEaVeD5v9TPvA=="], + "babel-jest/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "babel-plugin-istanbul/test-exclude/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -4452,10 +4508,26 @@ "p-locate/p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "packrat-expo-app/google-auth-library/gaxios": ["gaxios@7.1.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ=="], + + "packrat-expo-app/google-auth-library/gcp-metadata": ["gcp-metadata@7.0.1", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ=="], + + "packrat-expo-app/google-auth-library/google-logging-utils": ["google-logging-utils@1.1.1", "", {}, "sha512-rcX58I7nqpu4mbKztFeOAObbomBbHU2oIb/d3tJfF3dizGSApqtSwYJigGCooHdnMyQBIw8BrWyK96w3YXgr6A=="], + "packrat-guides-app/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], "packrat-landing-app/react-dom/scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], + "react-native-css-interop/@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "react-native-css-interop/@babel/traverse/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "react-native-css-interop/@babel/traverse/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], + + "react-native-css-interop/@babel/traverse/@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "react-native-css-interop/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "react-native/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], "read-pkg-up/find-up/path-exists": ["path-exists@2.1.0", "", { "dependencies": { "pinkie-promise": "^2.0.0" } }, "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ=="], @@ -4566,6 +4638,8 @@ "ora/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], + "react-native-css-interop/@babel/traverse/@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "react-native/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], diff --git a/package.json b/package.json index 262c4a7e67..8f8cde0014 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "packrat-monorepo", - "version": "2.0.15", + "version": "2.0.16", "workspaces": [ "apps/*", "packages/*" diff --git a/packages/api/container_src/package.json b/packages/api/container_src/package.json index b8113d9ac6..97d413d394 100644 --- a/packages/api/container_src/package.json +++ b/packages/api/container_src/package.json @@ -1,6 +1,6 @@ { "name": "container", - "version": "2.0.15", + "version": "2.0.16", "type": "module", "dependencies": { "@aws-sdk/client-s3": "^3.0.0", diff --git a/packages/api/container_src/server.ts b/packages/api/container_src/server.ts index 44b294ed15..9b8725269a 100644 --- a/packages/api/container_src/server.ts +++ b/packages/api/container_src/server.ts @@ -57,11 +57,12 @@ const TikTokImportSchema = z.object({ }); /** - * Detect image content type and file extension from response headers or buffer + * Detect media content type and file extension from response headers or buffer */ -function detectImageTypeAndExtension( +function detectMediaTypeAndExtension( response: Response, buffer?: ArrayBuffer, + isVideo = false, ): { contentType: string; extension: string; @@ -70,18 +71,31 @@ function detectImageTypeAndExtension( const headerContentType = response.headers.get('content-type'); if (headerContentType) { - // Common image content types - if (headerContentType.includes('image/jpeg') || headerContentType.includes('image/jpg')) { - return { contentType: 'image/jpeg', extension: 'jpg' }; - } - if (headerContentType.includes('image/png')) { - return { contentType: 'image/png', extension: 'png' }; - } - if (headerContentType.includes('image/webp')) { - return { contentType: 'image/webp', extension: 'webp' }; - } - if (headerContentType.includes('image/gif')) { - return { contentType: 'image/gif', extension: 'gif' }; + // Video content types + if (isVideo) { + if (headerContentType.includes('video/mp4')) { + return { contentType: 'video/mp4', extension: 'mp4' }; + } + if (headerContentType.includes('video/webm')) { + return { contentType: 'video/webm', extension: 'webm' }; + } + if (headerContentType.includes('video/quicktime')) { + return { contentType: 'video/quicktime', extension: 'mov' }; + } + } else { + // Image content types + if (headerContentType.includes('image/jpeg') || headerContentType.includes('image/jpg')) { + return { contentType: 'image/jpeg', extension: 'jpg' }; + } + if (headerContentType.includes('image/png')) { + return { contentType: 'image/png', extension: 'png' }; + } + if (headerContentType.includes('image/webp')) { + return { contentType: 'image/webp', extension: 'webp' }; + } + if (headerContentType.includes('image/gif')) { + return { contentType: 'image/gif', extension: 'gif' }; + } } } @@ -89,42 +103,57 @@ function detectImageTypeAndExtension( if (buffer) { const uint8Array = new Uint8Array(buffer.slice(0, 12)); - // JPEG magic bytes: FF D8 FF - if (uint8Array[0] === 0xff && uint8Array[1] === 0xd8 && uint8Array[2] === 0xff) { - return { contentType: 'image/jpeg', extension: 'jpg' }; - } + if (isVideo) { + // MP4 magic bytes: starts with ftyp box + if ( + uint8Array[4] === 0x66 && + uint8Array[5] === 0x74 && + uint8Array[6] === 0x79 && + uint8Array[7] === 0x70 + ) { + return { contentType: 'video/mp4', extension: 'mp4' }; + } + } else { + // JPEG magic bytes: FF D8 FF + if (uint8Array[0] === 0xff && uint8Array[1] === 0xd8 && uint8Array[2] === 0xff) { + return { contentType: 'image/jpeg', extension: 'jpg' }; + } - // PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A - if ( - uint8Array[0] === 0x89 && - uint8Array[1] === 0x50 && - uint8Array[2] === 0x4e && - uint8Array[3] === 0x47 - ) { - return { contentType: 'image/png', extension: 'png' }; - } + // PNG magic bytes: 89 50 4E 47 0D 0A 1A 0A + if ( + uint8Array[0] === 0x89 && + uint8Array[1] === 0x50 && + uint8Array[2] === 0x4e && + uint8Array[3] === 0x47 + ) { + return { contentType: 'image/png', extension: 'png' }; + } - // WebP magic bytes: RIFF ... WEBP - if ( - uint8Array[0] === 0x52 && - uint8Array[1] === 0x49 && - uint8Array[2] === 0x46 && - uint8Array[3] === 0x46 && - uint8Array[8] === 0x57 && - uint8Array[9] === 0x45 && - uint8Array[10] === 0x42 && - uint8Array[11] === 0x50 - ) { - return { contentType: 'image/webp', extension: 'webp' }; - } + // WebP magic bytes: RIFF ... WEBP + if ( + uint8Array[0] === 0x52 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x46 && + uint8Array[8] === 0x57 && + uint8Array[9] === 0x45 && + uint8Array[10] === 0x42 && + uint8Array[11] === 0x50 + ) { + return { contentType: 'image/webp', extension: 'webp' }; + } - // GIF magic bytes: GIF87a or GIF89a - if (uint8Array[0] === 0x47 && uint8Array[1] === 0x49 && uint8Array[2] === 0x46) { - return { contentType: 'image/gif', extension: 'gif' }; + // GIF magic bytes: GIF87a or GIF89a + if (uint8Array[0] === 0x47 && uint8Array[1] === 0x49 && uint8Array[2] === 0x46) { + return { contentType: 'image/gif', extension: 'gif' }; + } } } - // Default fallback to webp + // Default fallbacks + if (isVideo) { + return { contentType: 'video/mp4', extension: 'mp4' }; + } return { contentType: 'image/webp', extension: 'webp' }; } @@ -166,7 +195,7 @@ async function downloadAndRehostImage( const imageBuffer = await response.arrayBuffer(); // Detect the actual image type and extension - const { contentType, extension } = detectImageTypeAndExtension(response, imageBuffer); + const { contentType, extension } = detectMediaTypeAndExtension(response, imageBuffer, false); const timestamp = Date.now(); const imageKey = `tiktok-temp/${contentId}/${timestamp}-${index}.${extension}`; @@ -195,6 +224,67 @@ async function downloadAndRehostImage( } } +/** + * Download video and rehost to R2 with 5-minute expiration + */ +async function downloadAndRehostVideo(videoUrl: string, contentId: string): Promise { + if (!s3Client || !env) { + console.warn('R2 client not available, skipping video rehosting'); + return null; + } + + try { + console.log(`Downloading video: ${videoUrl}`); + + // Download video with TikTok-compatible headers + const response = await fetch(videoUrl, { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', + Referer: 'https://www.tiktok.com/', + Accept: 'video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5', + 'Accept-Language': 'en-US,en;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + Connection: 'keep-alive', + 'Upgrade-Insecure-Requests': '1', + }, + signal: AbortSignal.timeout(60000), // 60 second timeout for videos + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const videoBuffer = await response.arrayBuffer(); + + // Detect the actual video type and extension + const { contentType, extension } = detectMediaTypeAndExtension(response, videoBuffer, true); + + const timestamp = Date.now(); + const videoKey = `tiktok-temp/${contentId}/${timestamp}-video.${extension}`; + + console.log(`Uploading video to R2: ${videoKey} (${contentType})`); + + // Upload to R2 with temporary storage + await s3Client.send( + new PutObjectCommand({ + Bucket: env.R2_BUCKET_NAME, + Key: videoKey, + Body: new Uint8Array(videoBuffer), + ContentType: contentType, + }), + ); + + const rehostedUrl = `${env.R2_PUBLIC_URL}/${videoKey}`; + console.log(`Successfully rehosted video: ${rehostedUrl}`); + + return rehostedUrl; + } catch (error) { + console.error('Failed to rehost video:', error); + return null; + } +} + /** * Download and rehost multiple images with best effort approach */ @@ -240,15 +330,16 @@ async function downloadAndRehostImages( } /** - * Fetch TikTok slideshow data using TikTok API library + * Fetch TikTok content data (images or video) using TikTok API library */ async function fetchTikTokPostData( url: string, -): Promise<{ imageUrls: string[]; caption?: string; contentId?: string }> { +): Promise<{ imageUrls: string[]; videoUrl?: string; caption?: string; contentId?: string }> { try { + console.log('Attempting TikTok download for URL:', url); + const result = await Tiktok.Downloader(url, { version: 'v1', - showOriginalResponse: true, }); if (result.status !== 'success') { @@ -261,37 +352,41 @@ async function fetchTikTokPostData( } const imageUrls: string[] = []; + let videoUrl: string | undefined; let caption: string | undefined; let contentId: string | undefined; // Get caption from description - if (result.resultNotParsed.content?.desc) { - caption = result.resultNotParsed.content.desc; + if (result.result?.desc) { + caption = result.result.desc; } - // Get content ID (aweme_id) - if (result.resultNotParsed.content?.aweme_id) { - contentId = result.resultNotParsed.content.aweme_id; + // Get content ID + if (result.result?.id) { + contentId = result.result.id; } - // Get slideshow images from image_post_info - if (result.resultNotParsed.content?.image_post_info?.images) { - for (const image of result.resultNotParsed.content.image_post_info.images) { - if (image.display_image?.url_list && image.display_image.url_list.length > 0) { - // Use the first URL from the list (usually the best quality) - imageUrls.push(image.display_image.url_list[0]); - } + // Check content type and extract URLs accordingly + if (result.result?.type === 'video' && result.result.video?.playAddr) { + // Handle video content + if (Array.isArray(result.result.video.playAddr) && result.result.video.playAddr.length > 0) { + videoUrl = result.result.video.playAddr[0]; } + } else if (result.result?.type === 'image' && result.result.images) { + // Handle image slideshow content + imageUrls.push(...result.result.images); } - if (imageUrls.length === 0) { + // Check if we have any content + if (imageUrls.length === 0 && !videoUrl) { throw new Error( - 'No slideshow images found in TikTok content - this URL may not contain a slideshow/photo post', + 'No content found in TikTok post - this URL may not contain a slideshow/photo post or video', ); } return { imageUrls, + ...(videoUrl && { videoUrl }), ...(caption && { caption }), ...(contentId && { contentId }), }; @@ -323,7 +418,7 @@ app.get('/health', (c) => { }); }); -// TikTok slideshow import endpoint +// TikTok content import endpoint (supports both slideshows and videos) app.post('/import', async (c) => { try { const body = await c.req.json(); @@ -347,36 +442,84 @@ app.post('/import', async (c) => { // Fetch TikTok data const fetchedData = await fetchTikTokPostData(tiktokUrl); - console.log(`Successfully retrieved ${fetchedData.imageUrls.length} images from TikTok`); + const hasImages = fetchedData.imageUrls.length > 0; + const hasVideo = !!fetchedData.videoUrl; - // Rehost images to R2 with best effort approach - const { rehostedUrls, failedCount, expiresAt } = await downloadAndRehostImages( - fetchedData.imageUrls, - fetchedData.contentId || 'unknown', + console.log( + `Successfully retrieved TikTok content: ${hasImages ? `${fetchedData.imageUrls.length} images` : 'no images'}${hasVideo ? ', 1 video' : ''}`, ); - const responseData: { + let responseData: { imageUrls: string[]; + videoUrl?: string; caption?: string; contentId?: string; expiresAt?: string; failedImages?: number; - } = { - imageUrls: rehostedUrls.length > 0 ? rehostedUrls : fetchedData.imageUrls, + failedVideo?: boolean; + }; + + // Process images and video rehosting in parallel for efficiency + const [imageResult, videoResult] = await Promise.allSettled([ + hasImages + ? downloadAndRehostImages(fetchedData.imageUrls, fetchedData.contentId || 'unknown') + : Promise.resolve({ rehostedUrls: [], failedCount: 0, expiresAt: '' }), + hasVideo && fetchedData.videoUrl + ? downloadAndRehostVideo(fetchedData.videoUrl, fetchedData.contentId || 'unknown') + : Promise.resolve(null), + ]); + + // Process image rehosting results + let finalImageUrls = fetchedData.imageUrls; + let imageFailedCount = 0; + let expiresAt = new Date(Date.now() + 5 * 60 * 1000).toISOString(); + + if (imageResult.status === 'fulfilled' && hasImages) { + const { rehostedUrls, failedCount, expiresAt: imgExpiresAt } = imageResult.value; + if (rehostedUrls.length > 0) { + finalImageUrls = rehostedUrls; + } + imageFailedCount = failedCount; + expiresAt = imgExpiresAt; + } + + // Process video rehosting results + let finalVideoUrl = fetchedData.videoUrl; + let videoFailed = false; + + if (hasVideo) { + if (videoResult.status === 'fulfilled' && videoResult.value) { + finalVideoUrl = videoResult.value; + } else { + videoFailed = true; + if (videoResult.status === 'rejected') { + console.error('Video rehosting failed:', videoResult.reason); + } + } + } + + responseData = { + imageUrls: finalImageUrls, + ...(finalVideoUrl && { videoUrl: finalVideoUrl }), caption: fetchedData.caption, contentId: fetchedData.contentId, }; // Add metadata if rehosting was attempted - if (s3Client && env) { + if (s3Client && env && (hasImages || hasVideo)) { responseData.expiresAt = expiresAt; - if (failedCount > 0) { - responseData.failedImages = failedCount; + if (imageFailedCount > 0) { + responseData.failedImages = imageFailedCount; + } + if (videoFailed) { + responseData.failedVideo = true; } } console.log( - `Returning ${responseData.imageUrls.length} images (${rehostedUrls.length} rehosted, ${failedCount} failed)`, + `Returning ${responseData.imageUrls.length} images${responseData.videoUrl ? ' and 1 video' : ''}${ + responseData.failedImages ? ` (${responseData.failedImages} images failed)` : '' + }${responseData.failedVideo ? ' (video rehosting failed)' : ''}`, ); return c.json({ @@ -389,7 +532,7 @@ app.post('/import', async (c) => { return c.json( { success: false, - error: `Failed to import slideshow: ${error instanceof Error ? error.message : 'Unknown error'}`, + error: `Failed to import content: ${error instanceof Error ? error.message : 'Unknown error'}`, }, 500, ); diff --git a/packages/api/drizzle/0033_add_avatar_url_to_users.sql b/packages/api/drizzle/0033_add_avatar_url_to_users.sql new file mode 100644 index 0000000000..240b48c5ef --- /dev/null +++ b/packages/api/drizzle/0033_add_avatar_url_to_users.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "avatar_url" text; \ No newline at end of file diff --git a/packages/api/drizzle/0034_thin_spirit.sql b/packages/api/drizzle/0034_thin_spirit.sql new file mode 100644 index 0000000000..240b48c5ef --- /dev/null +++ b/packages/api/drizzle/0034_thin_spirit.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "avatar_url" text; \ No newline at end of file diff --git a/packages/api/drizzle/meta/0034_snapshot.json b/packages/api/drizzle/meta/0034_snapshot.json new file mode 100644 index 0000000000..c6d8050bba --- /dev/null +++ b/packages/api/drizzle/meta/0034_snapshot.json @@ -0,0 +1,1590 @@ +{ + "id": "f1e9f623-351d-41ae-be11-63ab0b22bc6e", + "prevId": "f7d48bb4-1fbe-4cd5-8b87-cd1b7a281fda", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.auth_providers": { + "name": "auth_providers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "auth_providers_user_id_users_id_fk": { + "name": "auth_providers_user_id_users_id_fk", + "tableFrom": "auth_providers", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_item_etl_jobs": { + "name": "catalog_item_etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "etl_job_id": { + "name": "etl_job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk": { + "name": "catalog_item_etl_jobs_catalog_item_id_catalog_items_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk": { + "name": "catalog_item_etl_jobs_etl_job_id_etl_jobs_id_fk", + "tableFrom": "catalog_item_etl_jobs", + "tableTo": "etl_jobs", + "columnsFrom": ["etl_job_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.catalog_items": { + "name": "catalog_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "product_url": { + "name": "product_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sku": { + "name": "sku", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "images": { + "name": "images", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "brand": { + "name": "brand", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rating_value": { + "name": "rating_value", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "price": { + "name": "price", + "type": "real", + "primaryKey": false, + "notNull": false + }, + "availability": { + "name": "availability", + "type": "availability", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "seller": { + "name": "seller", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "product_sku": { + "name": "product_sku", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "material": { + "name": "material", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "condition": { + "name": "condition", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_count": { + "name": "review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "variants": { + "name": "variants", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "techs": { + "name": "techs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "links": { + "name": "links", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "reviews": { + "name": "reviews", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "qas": { + "name": "qas", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "faqs": { + "name": "faqs", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "embedding_idx": { + "name": "embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "catalog_items_sku_unique": { + "name": "catalog_items_sku_unique", + "nullsNotDistinct": false, + "columns": ["sku"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.etl_jobs": { + "name": "etl_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "etl_job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_processed": { + "name": "total_processed", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_valid": { + "name": "total_valid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "total_invalid": { + "name": "total_invalid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "scraper_revision": { + "name": "scraper_revision", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "etl_jobs_scraper_revision_idx": { + "name": "etl_jobs_scraper_revision_idx", + "columns": [ + { + "expression": "scraper_revision", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invalid_item_logs": { + "name": "invalid_item_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "errors": { + "name": "errors", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "raw_data": { + "name": "raw_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "row_index": { + "name": "row_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "invalid_item_logs_job_id_etl_jobs_id_fk": { + "name": "invalid_item_logs_job_id_etl_jobs_id_fk", + "tableFrom": "invalid_item_logs", + "tableTo": "etl_jobs", + "columnsFrom": ["job_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.one_time_passwords": { + "name": "one_time_passwords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar(6)", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_passwords_user_id_users_id_fk": { + "name": "one_time_passwords_user_id_users_id_fk", + "tableFrom": "one_time_passwords", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_items": { + "name": "pack_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "template_item_id": { + "name": "template_item_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "pack_items_embedding_idx": { + "name": "pack_items_embedding_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": {} + } + }, + "foreignKeys": { + "pack_items_pack_id_packs_id_fk": { + "name": "pack_items_pack_id_packs_id_fk", + "tableFrom": "pack_items", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_user_id_users_id_fk": { + "name": "pack_items_user_id_users_id_fk", + "tableFrom": "pack_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_items_template_item_id_pack_template_items_id_fk": { + "name": "pack_items_template_item_id_pack_template_items_id_fk", + "tableFrom": "pack_items", + "tableTo": "pack_template_items", + "columnsFrom": ["template_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_template_items": { + "name": "pack_template_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "weight_unit": { + "name": "weight_unit", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consumable": { + "name": "consumable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "worn": { + "name": "worn", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pack_template_id": { + "name": "pack_template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "catalog_item_id": { + "name": "catalog_item_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_template_items_pack_template_id_pack_templates_id_fk": { + "name": "pack_template_items_pack_template_id_pack_templates_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "pack_templates", + "columnsFrom": ["pack_template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "pack_template_items_catalog_item_id_catalog_items_id_fk": { + "name": "pack_template_items_catalog_item_id_catalog_items_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "catalog_items", + "columnsFrom": ["catalog_item_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "pack_template_items_user_id_users_id_fk": { + "name": "pack_template_items_user_id_users_id_fk", + "tableFrom": "pack_template_items", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.pack_templates": { + "name": "pack_templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_app_template": { + "name": "is_app_template", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "pack_templates_user_id_users_id_fk": { + "name": "pack_templates_user_id_users_id_fk", + "tableFrom": "pack_templates", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.weight_history": { + "name": "weight_history", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "weight": { + "name": "weight", + "type": "real", + "primaryKey": false, + "notNull": true + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "weight_history_user_id_users_id_fk": { + "name": "weight_history_user_id_users_id_fk", + "tableFrom": "weight_history", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "weight_history_pack_id_packs_id_fk": { + "name": "weight_history_pack_id_packs_id_fk", + "tableFrom": "weight_history", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.packs": { + "name": "packs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_public": { + "name": "is_public", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tags": { + "name": "tags", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_ai_generated": { + "name": "is_ai_generated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "packs_user_id_users_id_fk": { + "name": "packs_user_id_users_id_fk", + "tableFrom": "packs", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "packs_template_id_pack_templates_id_fk": { + "name": "packs_template_id_pack_templates_id_fk", + "tableFrom": "packs", + "tableTo": "pack_templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.refresh_tokens": { + "name": "refresh_tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "replaced_by_token": { + "name": "replaced_by_token", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.reported_content": { + "name": "reported_content", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ai_response": { + "name": "ai_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_comment": { + "name": "user_comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "reviewed": { + "name": "reviewed", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "reviewed_by": { + "name": "reviewed_by", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "reviewed_at": { + "name": "reviewed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "reported_content_user_id_users_id_fk": { + "name": "reported_content_user_id_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reported_content_reviewed_by_users_id_fk": { + "name": "reported_content_reviewed_by_users_id_fk", + "tableFrom": "reported_content", + "tableTo": "users", + "columnsFrom": ["reviewed_by"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.trips": { + "name": "trips", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "pack_id": { + "name": "pack_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "local_created_at": { + "name": "local_created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "local_updated_at": { + "name": "local_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "deleted": { + "name": "deleted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "trips_user_id_users_id_fk": { + "name": "trips_user_id_users_id_fk", + "tableFrom": "trips", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "trips_pack_id_packs_id_fk": { + "name": "trips_pack_id_packs_id_fk", + "tableFrom": "trips", + "tableTo": "packs", + "columnsFrom": ["pack_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "first_name": { + "name": "first_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_name": { + "name": "last_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'USER'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/api/drizzle/meta/_journal.json b/packages/api/drizzle/meta/_journal.json index 9fe43d915b..e498f85424 100644 --- a/packages/api/drizzle/meta/_journal.json +++ b/packages/api/drizzle/meta/_journal.json @@ -246,6 +246,13 @@ "when": 1773066716880, "tag": "0033_stormy_next_avengers", "breakpoints": true + }, + { + "idx": 34, + "version": "7", + "when": 1774086319275, + "tag": "0034_thin_spirit", + "breakpoints": true } ] } diff --git a/packages/api/package.json b/packages/api/package.json index 7a2f8e8d8f..b60438bc73 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -14,6 +14,7 @@ "test:unit:coverage": "vitest run --config vitest.unit.config.ts --coverage" }, "dependencies": { + "@ai-sdk/google": "^2.0.62", "@ai-sdk/openai": "^2.0.11", "@ai-sdk/perplexity": "^2.0.1", "@aws-sdk/client-s3": "^3.787.0", diff --git a/packages/api/src/db/schema.ts b/packages/api/src/db/schema.ts index dccf9d7e54..662a9589b2 100644 --- a/packages/api/src/db/schema.ts +++ b/packages/api/src/db/schema.ts @@ -25,6 +25,7 @@ export const users = pgTable('users', { passwordHash: text('password_hash'), firstName: text('first_name'), lastName: text('last_name'), + avatarUrl: text('avatar_url'), role: text('role').default('USER'), // 'USER', 'ADMIN' createdAt: timestamp('created_at').defaultNow(), updatedAt: timestamp('updated_at').defaultNow(), diff --git a/packages/api/src/routes/packTemplates/generateFromTikTok.ts b/packages/api/src/routes/packTemplates/generateFromTikTok.ts index 009baf193d..65f2459ba8 100644 --- a/packages/api/src/routes/packTemplates/generateFromTikTok.ts +++ b/packages/api/src/routes/packTemplates/generateFromTikTok.ts @@ -1,4 +1,4 @@ -import { createOpenAI } from '@ai-sdk/openai'; +import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { getContainer } from '@cloudflare/containers'; import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { createDb } from '@packrat/api/db'; @@ -37,12 +37,15 @@ function generateContentIdFromUrl(url: string): string { return `url_${Math.abs(hash).toString(16)}`; } -const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown images from a TikTok slideshow featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). Your task is to: +const SYSTEM_PROMPT = `You are an expert outdoor gear analyst. You will be shown content from TikTok featuring packing content (e.g., a gear lay-flat, kit breakdown, or packing list). This content may be either images (slideshow) or a video. Your task is to: -1. Identify every outdoor gear or equipment item visible in the images or mentioned in the caption. +1. Identify every outdoor gear or equipment item visible in the images/video or mentioned in the caption. 2. For each item, provide a specific name, description, category, weight estimate (in grams), quantity, and flags for whether it is consumable or worn. 3. Also determine an appropriate pack template name and category (one of: hiking, backpacking, camping, climbing, winter, desert, custom, water sports, skiing) for this overall kit. +For video content: Analyze the video frames to identify gear items shown throughout the video. Pay attention to any gear being packed, displayed, or mentioned. +For slideshow content: Analyze each image to identify all visible gear items. + Focus on items that would realistically appear in an outdoor adventure packing list. Be thorough — identify every item you can see or infer.`; /** @@ -51,7 +54,7 @@ Focus on items that would realistically appear in an outdoor adventure packing l async function fetchTikTokPostData( c: Context<{ Bindings: Env; Variables: Variables }>, url: string, -): Promise<{ imageUrls: string[]; caption?: string; contentId?: string }> { +): Promise<{ imageUrls: string[]; videoUrl?: string; caption?: string; contentId?: string }> { try { const { TIKTOK_CONTAINER } = getEnv(c); @@ -78,7 +81,7 @@ async function fetchTikTokPostData( const result = (await response.json()) as { success: boolean; - data?: { imageUrls: string[]; caption?: string; contentId?: string }; + data?: { imageUrls: string[]; videoUrl?: string; caption?: string; contentId?: string }; error?: string; }; @@ -88,6 +91,7 @@ async function fetchTikTokPostData( return { imageUrls: result.data?.imageUrls || [], + videoUrl: result.data?.videoUrl, caption: result.data?.caption, contentId: result.data?.contentId, }; @@ -141,7 +145,7 @@ const generateFromTikTokRoute = createRoute({ tags: ['Pack Templates'], summary: 'Generate a pack template from a TikTok content URL', description: - 'Admin-only endpoint that uses TikTok API to fetch slideshow images and captions from a TikTok URL, then analyzes the content with AI (GPT-4o) to build a featured pack template using items from the catalog.', + 'Admin-only endpoint that uses TikTok API to fetch slideshow images or videos and captions from a TikTok URL, then analyzes the content with AI (Gemini-3-Flash-Preview) to build a featured pack template using items from the catalog.', security: [{ bearerAuth: [] }], request: { body: { @@ -215,19 +219,23 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { const { isAppTemplate } = body; tiktokUrl = body.tiktokUrl; - const { OPENAI_API_KEY } = getEnv(c); - const openai = createOpenAI({ apiKey: OPENAI_API_KEY }); + const { GOOGLE_GENERATIVE_AI_API_KEY } = getEnv(c); + const google = createGoogleGenerativeAI({ + apiKey: GOOGLE_GENERATIVE_AI_API_KEY, + }); // Fetch TikTok data using API library console.log(`Processing TikTok URL: ${tiktokUrl}`); let imageUrls: string[]; + let videoUrl: string | undefined; let caption: string | undefined; let contentId: string | undefined; try { const data = await fetchTikTokPostData(c, tiktokUrl); imageUrls = data.imageUrls; + videoUrl = data.videoUrl; caption = data.caption; contentId = data.contentId; } catch (apiError) { @@ -270,24 +278,34 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { ); } - // Build message content parts for GPT-4o + // Build message content parts for Gemini type TextPart = { type: 'text'; text: string }; type ImagePart = { type: 'image'; image: string }; - const contentParts: Array = []; - - const introText = caption - ? `Retrieved Caption: ${caption}\n\nPlease analyze the following slideshow images and identify all packing/gear items:` - : `Please analyze the following slideshow images and identify all packing/gear items:`; - - contentParts.push({ type: 'text', text: introText }); - - for (const imageUrl of imageUrls) { - contentParts.push({ type: 'image', image: imageUrl }); + type FilePart = { type: 'file'; data: string; mediaType: string }; + const contentParts: Array = []; + + let introText: string; + if (videoUrl) { + introText = caption + ? `Retrieved Caption: ${caption}\n\nPlease analyze the following TikTok video and identify all packing/gear items:` + : `Please analyze the following TikTok video and identify all packing/gear items:`; + contentParts.push({ type: 'text', text: introText }); + contentParts.push({ type: 'file', data: videoUrl, mediaType: 'video/mp4' }); + } else if (imageUrls.length > 0) { + introText = caption + ? `Retrieved Caption: ${caption}\n\nPlease analyze the following slideshow images and identify all packing/gear items:` + : `Please analyze the following slideshow images and identify all packing/gear items:`; + contentParts.push({ type: 'text', text: introText }); + for (const imageUrl of imageUrls) { + contentParts.push({ type: 'image', image: imageUrl }); + } + } else { + throw new Error('No content found in TikTok post (no images or video)'); } - // Analyze images with GPT-4o + // Analyze content with Gemini-3-Flash-Preview const { object: analysis } = await generateObject({ - model: openai('gpt-4o'), + model: google('gemini-3-flash-preview'), schema: analysisSchema, system: SYSTEM_PROMPT, prompt: [ @@ -377,7 +395,11 @@ generateFromTikTokRoutes.openapi(generateFromTikTokRoute, async (c) => { // Determine specific error type based on error context let errorCode = 'UNKNOWN_ERROR'; if (error instanceof Error) { - if (error.message.includes('OpenAI') || error.message.includes('AI')) { + if ( + error.message.includes('Google') || + error.message.includes('Gemini') || + error.message.includes('AI') + ) { errorCode = 'AI_ANALYSIS_ERROR'; } else if (error.message.includes('catalog') || error.message.includes('search')) { errorCode = 'CATALOG_SEARCH_ERROR'; diff --git a/packages/api/src/routes/user/index.ts b/packages/api/src/routes/user/index.ts index acd0ff975d..7340b13a21 100644 --- a/packages/api/src/routes/user/index.ts +++ b/packages/api/src/routes/user/index.ts @@ -63,6 +63,7 @@ userRoutes.openapi(getUserProfileRoute, async (c) => { email: users.email, firstName: users.firstName, lastName: users.lastName, + avatarUrl: users.avatarUrl, role: users.role, emailVerified: users.emailVerified, createdAt: users.createdAt, @@ -165,7 +166,7 @@ userRoutes.openapi(updateUserProfileRoute, async (c) => { try { const auth = c.get('user'); - const { firstName, lastName, email } = c.req.valid('json'); + const { firstName, lastName, email, avatarUrl } = c.req.valid('json'); const db = createDb(c); // If email is being updated, check if it's already in use @@ -191,6 +192,7 @@ userRoutes.openapi(updateUserProfileRoute, async (c) => { if (firstName !== undefined) updateData.firstName = firstName; if (lastName !== undefined) updateData.lastName = lastName; + if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl; if (email !== undefined) { updateData.email = email.toLowerCase(); updateData.emailVerified = false; // Reset verification if email changes @@ -220,6 +222,7 @@ userRoutes.openapi(updateUserProfileRoute, async (c) => { email: updatedUser.email, firstName: updatedUser.firstName, lastName: updatedUser.lastName, + avatarUrl: updatedUser.avatarUrl, role: updatedUser.role, emailVerified: updatedUser.emailVerified, createdAt: updatedUser.createdAt?.toISOString() || null, diff --git a/packages/api/src/schemas/packTemplates.ts b/packages/api/src/schemas/packTemplates.ts index 85ff36abe7..cb2dd46b45 100644 --- a/packages/api/src/schemas/packTemplates.ts +++ b/packages/api/src/schemas/packTemplates.ts @@ -315,7 +315,7 @@ export const GenerateFromTikTokRequestSchema = z .object({ tiktokUrl: z.string().url().openapi({ example: 'https://www.tiktok.com/@user/video/1234567890', - description: 'The TikTok slideshow URL', + description: 'The TikTok content URL (supports both slideshows and videos)', }), isAppTemplate: z.boolean().optional().default(true).openapi({ example: true, diff --git a/packages/api/src/schemas/users.ts b/packages/api/src/schemas/users.ts index 92086577d4..c4f52f421b 100644 --- a/packages/api/src/schemas/users.ts +++ b/packages/api/src/schemas/users.ts @@ -40,6 +40,10 @@ export const UserSchema = z example: '2024-01-15T10:30:00Z', description: 'User account last update timestamp', }), + avatarUrl: z.string().nullable().optional().openapi({ + example: 'https://example.com/avatar.jpg', + description: 'User profile avatar URL', + }), }) .openapi('User'); @@ -66,6 +70,10 @@ export const UpdateUserRequestSchema = z example: 'newemail@example.com', description: 'Updated email address (requires re-verification)', }), + avatarUrl: z.string().nullable().optional().openapi({ + example: 'https://example.com/avatar.jpg', + description: 'Updated profile avatar URL', + }), }) .openapi('UpdateUserRequest'); diff --git a/packages/api/src/utils/env-validation.ts b/packages/api/src/utils/env-validation.ts index 59e588b741..3a20577057 100644 --- a/packages/api/src/utils/env-validation.ts +++ b/packages/api/src/utils/env-validation.ts @@ -28,6 +28,7 @@ export const apiEnvSchema = z.object({ // AI & External APIs OPENAI_API_KEY: z.string().startsWith('sk-'), + GOOGLE_GENERATIVE_AI_API_KEY: z.string(), AI_PROVIDER: z.enum(['openai', 'cloudflare-workers-ai']), PERPLEXITY_API_KEY: z.string().startsWith('pplx-'), diff --git a/packages/api/test/setup.ts b/packages/api/test/setup.ts index 3fb43e78e7..3de5f322ef 100644 --- a/packages/api/test/setup.ts +++ b/packages/api/test/setup.ts @@ -23,6 +23,7 @@ const testEnv = { // AI & External APIs OPENAI_API_KEY: 'sk-test-key', + GOOGLE_GENERATIVE_AI_API_KEY: 'test-google-key', AI_PROVIDER: 'openai', PERPLEXITY_API_KEY: 'pplx-test-key', diff --git a/packages/ui/package.json b/packages/ui/package.json index f70769adfd..7a2c6e6e21 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,8 +1,8 @@ { "name": "@packrat/ui", - "version": "2.0.15", + "version": "2.0.16", "private": true, "dependencies": { - "@packrat-ai/nativewindui": "^1.1.0" + "@packrat-ai/nativewindui": "^2.0.0" } }