diff --git a/src/screens/AIChatScreen.tsx b/src/screens/AIChatScreen.tsx index 34ad7cc..6f49d0b 100644 --- a/src/screens/AIChatScreen.tsx +++ b/src/screens/AIChatScreen.tsx @@ -14,12 +14,12 @@ import { Keyboard, } from 'react-native'; import CustomAlert from '../components/CustomAlert'; -// Removed duplicate AIChatScreen definition and alert state at the top. The correct component is defined after SuggestionChip. import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { useTheme } from '../contexts/ThemeContext'; import FastImage from '@d11/react-native-fast-image'; import { BlurView as ExpoBlurView } from 'expo-blur'; +import { LinearGradient } from 'expo-linear-gradient'; // Lazy-safe community blur import (avoid bundling issues on web) let AndroidBlurView: any = null; if (Platform.OS === 'android') { @@ -49,11 +49,14 @@ import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context' import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, createSeriesContext, generateConversationStarters } from '../services/aiService'; import { tmdbService } from '../services/tmdbService'; import Markdown from 'react-native-markdown-display'; -import Animated, { - useAnimatedStyle, - useSharedValue, - withSpring, +import Animated, { + useAnimatedStyle, + useSharedValue, + withSpring, withTiming, + withRepeat, + withSequence, + withDelay, interpolate, Extrapolate, runOnJS @@ -80,16 +83,56 @@ interface ChatBubbleProps { isLast: boolean; } +// Animated typing dot component +const TypingDot: React.FC<{ delay: number; color: string }> = ({ delay, color }) => { + const opacity = useSharedValue(0.3); + const scale = useSharedValue(1); + + useEffect(() => { + opacity.value = withDelay( + delay, + withRepeat( + withSequence( + withTiming(1, { duration: 400 }), + withTiming(0.3, { duration: 400 }) + ), + -1, + false + ) + ); + scale.value = withDelay( + delay, + withRepeat( + withSequence( + withTiming(1.2, { duration: 400 }), + withTiming(1, { duration: 400 }) + ), + -1, + false + ) + ); + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + transform: [{ scale: scale.value }], + })); + + return ( + + ); +}; + const ChatBubble: React.FC = React.memo(({ message, isLast }) => { const { currentTheme } = useTheme(); const isUser = message.role === 'user'; - + const bubbleAnimation = useSharedValue(0); - + useEffect(() => { - bubbleAnimation.value = withSpring(1, { damping: 15, stiffness: 120 }); + bubbleAnimation.value = withSpring(1, { damping: 18, stiffness: 100 }); }, []); - + const animatedStyle = useAnimatedStyle(() => ({ opacity: bubbleAnimation.value, transform: [ @@ -97,7 +140,7 @@ const ChatBubble: React.FC = React.memo(({ message, isLast }) = translateY: interpolate( bubbleAnimation.value, [0, 1], - [20, 0], + [16, 0], Extrapolate.CLAMP ) }, @@ -105,7 +148,7 @@ const ChatBubble: React.FC = React.memo(({ message, isLast }) = scale: interpolate( bubbleAnimation.value, [0, 1], - [0.8, 1], + [0.95, 1], Extrapolate.CLAMP ) } @@ -120,16 +163,30 @@ const ChatBubble: React.FC = React.memo(({ message, isLast }) = animatedStyle ]}> {!isUser && ( - - + + + + )} - + = React.memo(({ message, isLast }) = {!isUser && ( {Platform.OS === 'android' && AndroidBlurView - ? + ? : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable - ? - : } - + ? + : } + )} - {isUser ? ( - - {message.content} - - ) : ( - - {message.content} - - )} + {isUser ? ( + + {message.content} + + ) : ( + + {message.content} + + )} - {new Date(message.timestamp).toLocaleTimeString([], { - hour: '2-digit', - minute: '2-digit' + {new Date(message.timestamp).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit' })} - + {isUser && ( - - + + )} @@ -296,23 +372,60 @@ const ChatBubble: React.FC = React.memo(({ message, isLast }) = interface SuggestionChipProps { text: string; onPress: () => void; + index: number; } -const SuggestionChip: React.FC = React.memo(({ text, onPress }) => { +const SuggestionChip: React.FC = React.memo(({ text, onPress, index }) => { const { currentTheme } = useTheme(); - + const animValue = useSharedValue(0); + + useEffect(() => { + animValue.value = withDelay( + index * 80, + withSpring(1, { damping: 18, stiffness: 120 }) + ); + }, []); + + const animatedStyle = useAnimatedStyle(() => ({ + opacity: animValue.value, + transform: [ + { translateY: interpolate(animValue.value, [0, 1], [12, 0], Extrapolate.CLAMP) }, + { scale: interpolate(animValue.value, [0, 1], [0.95, 1], Extrapolate.CLAMP) }, + ], + })); + return ( - - - {text} - - + + + + + {text} + + + + ); -}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress); +}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index); const AIChatScreen: React.FC = () => { // CustomAlert state @@ -347,9 +460,9 @@ const AIChatScreen: React.FC = () => { const navigation = useNavigation(); const { currentTheme } = useTheme(); const insets = useSafeAreaInsets(); - + const { contentId, contentType, episodeId, seasonNumber, episodeNumber, title } = route.params; - + const [messages, setMessages] = useState([]); const [inputText, setInputText] = useState(''); const [isLoading, setIsLoading] = useState(false); @@ -369,10 +482,10 @@ const AIChatScreen: React.FC = () => { }; }, []) ); - + const scrollViewRef = useRef(null); const inputRef = useRef(null); - + // Animation values const headerOpacity = useSharedValue(1); const inputContainerY = useSharedValue(0); @@ -432,7 +545,7 @@ const AIChatScreen: React.FC = () => { const loadContext = async () => { try { setIsLoadingContext(true); - + if (contentType === 'movie') { // Movies: contentId may be TMDB id string or IMDb id (tt...) let movieData = await tmdbService.getMovieDetails(contentId); @@ -451,7 +564,7 @@ const AIChatScreen: React.FC = () => { try { const path = movieData.backdrop_path || movieData.poster_path || null; if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`); - } catch {} + } catch { } } else { // Series: resolve TMDB numeric id first (contentId may be IMDb/stremio id) let tmdbNumericId: number | null = null; @@ -476,25 +589,25 @@ const AIChatScreen: React.FC = () => { try { const path = showData.backdrop_path || showData.poster_path || null; if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`); - } catch {} - + } catch { } + if (!showData) throw new Error('Unable to load TV show details'); const seriesContext = createSeriesContext(showData, allEpisodes || {}); setContext(seriesContext); } } catch (error) { if (__DEV__) console.error('Error loading context:', error); - openAlert('Error', 'Failed to load content details for AI chat'); + openAlert('Error', 'Failed to load content details for AI chat'); } finally { setIsLoadingContext(false); - {/* CustomAlert at root */} - setAlertVisible(false)} - actions={alertActions} - /> + {/* CustomAlert at root */ } + setAlertVisible(false)} + actions={alertActions} + /> } }; @@ -527,10 +640,10 @@ const AIChatScreen: React.FC = () => { const sxe = messageText.match(/s(\d+)e(\d+)/i); const words = messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i); const seasonOnly = messageText.match(/s(\d+)(?!e)/i) || messageText.match(/season\s+(\d+)/i); - + let season = sxe ? parseInt(sxe[1], 10) : (words ? parseInt(words[1], 10) : undefined); let episode = sxe ? parseInt(sxe[2], 10) : (words ? parseInt(words[2], 10) : undefined); - + // If only season mentioned (like "s2" or "season 2"), default to episode 1 if (!season && seasonOnly) { season = parseInt(seasonOnly[1], 10); @@ -558,7 +671,7 @@ const AIChatScreen: React.FC = () => { requestContext = createEpisodeContext(episodeData, showData, season, episode); } } - } catch {} + } catch { } } } @@ -578,7 +691,7 @@ const AIChatScreen: React.FC = () => { setMessages(prev => [...prev, assistantMessage]); } catch (error) { if (__DEV__) console.error('Error sending message:', error); - + let errorMessage = 'Sorry, I encountered an error. Please try again.'; if (error instanceof Error) { if (error.message.includes('not configured')) { @@ -623,7 +736,7 @@ const AIChatScreen: React.FC = () => { const getDisplayTitle = () => { if (!context) return title; - + if ('episodesBySeason' in (context as any)) { // Always show just the series title return (context as any).title; @@ -656,200 +769,219 @@ const AIChatScreen: React.FC = () => { return ( - - {backdropUrl && ( - - - {Platform.OS === 'android' && AndroidBlurView - ? - : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable - ? - : } - - - )} - - - {/* Header */} - - - { - if (Platform.OS === 'android') { - modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => { - if (finished) runOnJS(navigation.goBack)(); - }); - } else { - navigation.goBack(); - } - }} - style={styles.backButton} - > - - - - - - AI Chat - - - {getDisplayTitle()} - + + {backdropUrl && ( + + + {Platform.OS === 'android' && AndroidBlurView + ? + : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable + ? + : } + - - - - - - + )} + - {/* Chat Messages */} - - - {messages.length === 0 && suggestions.length > 0 && ( - - - - - - Ask me anything about + {/* Header */} + + + { + if (Platform.OS === 'android') { + modalOpacity.value = withSpring(0, { damping: 18, stiffness: 160 }, (finished) => { + if (finished) runOnJS(navigation.goBack)(); + }); + } else { + navigation.goBack(); + } + }} + style={styles.backButton} + > + + + + + + AI Chat - + {getDisplayTitle()} - - I have detailed knowledge about this content and can answer questions about plot, characters, themes, and more. - - - - - Try asking: - - - {suggestions.map((suggestion, index) => ( - handleSuggestionPress(suggestion)} - /> - ))} - - - )} - - {messages.map((message, index) => ( - - ))} - - {isLoading && ( - - - - - - - - - - )} - - {/* Input Container */} - - - - - {Platform.OS === 'android' && AndroidBlurView - ? - : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable - ? - : } - - - - - - - + + - - - - setAlertVisible(false)} - actions={alertActions} - /> + + {/* Chat Messages */} + + + {messages.length === 0 && suggestions.length > 0 && ( + + + + + + Ask me anything about + + + {getDisplayTitle()} + + + I have detailed knowledge about this content and can answer questions about plot, characters, themes, and more. + + + + + Try asking: + + + {suggestions.map((suggestion, index) => ( + handleSuggestionPress(suggestion)} + index={index} + /> + ))} + + + + )} + + {messages.map((message, index) => ( + + ))} + + {isLoading && ( + + + + + + + + + + )} + + + {/* Input Container */} + + + + + {Platform.OS === 'android' && AndroidBlurView + ? + : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable + ? + : } + + + + + + {inputText.trim() ? ( + + + + ) : ( + + + + )} + + + + + + + setAlertVisible(false)} + actions={alertActions} + /> ); }; @@ -862,41 +994,49 @@ const styles = StyleSheet.create({ flex: 1, justifyContent: 'center', alignItems: 'center', - gap: 16, + gap: 20, }, loadingText: { - fontSize: 16, + fontSize: 15, + fontWeight: '500', + letterSpacing: 0.3, textAlign: 'center', }, header: { - paddingHorizontal: 16, - paddingBottom: 12, - borderBottomWidth: 1, - borderBottomColor: 'rgba(255,255,255,0.1)', + paddingHorizontal: 20, + paddingBottom: 16, + borderBottomWidth: StyleSheet.hairlineWidth, + borderBottomColor: 'rgba(255,255,255,0.08)', }, headerContent: { flexDirection: 'row', alignItems: 'center', - gap: 12, + gap: 14, }, backButton: { - padding: 8, + padding: 10, + marginLeft: -6, + borderRadius: 12, }, headerInfo: { flex: 1, }, headerTitle: { - fontSize: 20, + fontSize: 22, fontWeight: '700', + letterSpacing: -0.3, }, headerSubtitle: { fontSize: 14, - marginTop: 2, + fontWeight: '500', + marginTop: 3, + opacity: 0.7, + letterSpacing: 0.1, }, aiIndicator: { - width: 40, - height: 40, - borderRadius: 20, + width: 44, + height: 44, + borderRadius: 22, justifyContent: 'center', alignItems: 'center', }, @@ -907,65 +1047,80 @@ const styles = StyleSheet.create({ flex: 1, }, messagesContent: { - padding: 16, - paddingBottom: 8, + padding: 20, + paddingBottom: 12, }, welcomeContainer: { alignItems: 'center', - paddingVertical: 32, - paddingHorizontal: 24, + paddingVertical: 40, + paddingHorizontal: 28, }, welcomeIcon: { - width: 64, - height: 64, - borderRadius: 32, + width: 72, + height: 72, + borderRadius: 36, justifyContent: 'center', alignItems: 'center', - marginBottom: 16, + marginBottom: 20, }, welcomeTitle: { - fontSize: 20, - fontWeight: '600', + fontSize: 18, + fontWeight: '500', textAlign: 'center', + letterSpacing: 0.2, + opacity: 0.85, }, welcomeSubtitle: { - fontSize: 22, + fontSize: 24, fontWeight: '700', textAlign: 'center', - marginTop: 4, - marginBottom: 12, + marginTop: 6, + marginBottom: 16, + letterSpacing: -0.4, }, welcomeDescription: { - fontSize: 16, - lineHeight: 24, + fontSize: 15, + lineHeight: 23, textAlign: 'center', - marginBottom: 32, + marginBottom: 36, + opacity: 0.7, + letterSpacing: 0.15, + maxWidth: 320, }, suggestionsContainer: { width: '100%', }, suggestionsTitle: { - fontSize: 14, + fontSize: 13, fontWeight: '600', - marginBottom: 12, + marginBottom: 14, textAlign: 'center', + textTransform: 'uppercase', + letterSpacing: 1.2, + opacity: 0.6, }, suggestionsGrid: { - gap: 8, + gap: 10, }, suggestionChip: { + flexDirection: 'row', + alignItems: 'center', paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 20, - alignSelf: 'flex-start', - marginBottom: 8, + paddingVertical: 14, + borderRadius: 14, + marginBottom: 0, + }, + suggestionIcon: { + marginRight: 10, }, suggestionText: { - fontSize: 15, + flex: 1, + fontSize: 14.5, fontWeight: '500', + letterSpacing: 0.1, }, messageContainer: { - marginBottom: 16, + marginBottom: 20, flexDirection: 'row', alignItems: 'flex-end', }, @@ -976,100 +1131,115 @@ const styles = StyleSheet.create({ justifyContent: 'flex-start', }, lastMessageContainer: { - marginBottom: 8, + marginBottom: 12, + }, + avatarWrapper: { + marginRight: 10, }, avatarContainer: { - width: 32, - height: 32, - borderRadius: 16, + width: 30, + height: 30, + borderRadius: 15, justifyContent: 'center', alignItems: 'center', - marginRight: 8, }, userAvatarContainer: { - width: 32, - height: 32, - borderRadius: 16, + width: 30, + height: 30, + borderRadius: 15, justifyContent: 'center', alignItems: 'center', - marginLeft: 8, + marginLeft: 10, }, messageBubble: { - maxWidth: width * 0.75, - paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 20, + maxWidth: width * 0.78, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 22, overflow: 'hidden', }, userBubble: { - borderBottomRightRadius: 20, + borderBottomRightRadius: 6, }, assistantBubble: { - borderBottomLeftRadius: 20, + borderBottomLeftRadius: 6, }, assistantBlurBackdrop: { ...StyleSheet.absoluteFillObject, - borderRadius: 20, + borderRadius: 22, }, messageText: { - fontSize: 16, - lineHeight: 22, + fontSize: 15.5, + lineHeight: 23, + letterSpacing: 0.15, + }, + userMessageText: { + color: 'white', + fontWeight: '400', }, messageTime: { - fontSize: 12, - marginTop: 4, - opacity: 0.8, + fontSize: 11, + marginTop: 8, + fontWeight: '500', + letterSpacing: 0.3, }, typingIndicator: { flexDirection: 'row', alignItems: 'flex-end', - marginBottom: 16, + marginBottom: 20, }, typingBubble: { - paddingHorizontal: 16, - paddingVertical: 12, - borderRadius: 20, - borderBottomLeftRadius: 4, + paddingHorizontal: 18, + paddingVertical: 14, + borderRadius: 22, + borderBottomLeftRadius: 6, marginLeft: 40, }, typingDots: { flexDirection: 'row', - gap: 4, + gap: 6, + alignItems: 'center', }, typingDot: { - width: 8, - height: 8, - borderRadius: 4, + width: 7, + height: 7, + borderRadius: 3.5, }, inputContainer: { - paddingHorizontal: 16, - paddingVertical: 12, - paddingBottom: 16, + paddingHorizontal: 20, + paddingVertical: 14, + paddingBottom: 18, }, inputWrapper: { flexDirection: 'row', alignItems: 'flex-end', - borderRadius: 24, - paddingHorizontal: 16, - paddingVertical: 8, - gap: 12, - overflow: 'hidden' + borderRadius: 26, + paddingHorizontal: 18, + paddingVertical: 10, + gap: 14, + overflow: 'hidden', + borderWidth: 1, + borderColor: 'rgba(255,255,255,0.06)', }, inputBlurBackdrop: { ...StyleSheet.absoluteFillObject, - borderRadius: 24, + borderRadius: 26, }, textInput: { flex: 1, fontSize: 16, - lineHeight: 22, - maxHeight: 100, - paddingVertical: 8, + lineHeight: 23, + maxHeight: 120, + paddingVertical: 10, + letterSpacing: 0.15, + }, + sendButtonWrapper: { + marginLeft: 2, }, sendButton: { - width: 40, - height: 40, - borderRadius: 20, + width: 44, + height: 44, + borderRadius: 22, justifyContent: 'center', alignItems: 'center', }, diff --git a/src/services/aiService.ts b/src/services/aiService.ts index e469f7b..8265045 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -25,6 +25,18 @@ export interface MovieContext { runtime?: number; tagline?: string; keywords?: string[]; + voteAverage?: number; + voteCount?: number; + popularity?: number; + budget?: number; + revenue?: number; + productionCompanies?: string[]; + productionCountries?: string[]; + spokenLanguages?: string[]; + originalLanguage?: string; + status?: string; + contentRating?: string; + imdbId?: string; } export interface EpisodeContext { @@ -50,6 +62,12 @@ export interface EpisodeContext { name: string; character: string; }>; + // New enhanced fields + voteAverage?: number; + showGenres?: string[]; + showNetworks?: string[]; + showStatus?: string; + contentRating?: string; } export interface SeriesContext { @@ -76,7 +94,19 @@ export interface SeriesContext { airDate: string; released: boolean; overview?: string; + voteAverage?: number; }>>; + // New enhanced fields + networks?: string[]; + status?: string; + originalLanguage?: string; + popularity?: number; + voteAverage?: number; + voteCount?: number; + createdBy?: string[]; + contentRating?: string; + productionCompanies?: string[]; + type?: string; // "Scripted", "Documentary", etc. } export type ContentContext = MovieContext | EpisodeContext | SeriesContext; @@ -101,7 +131,7 @@ class AIService { private apiKey: string | null = null; private baseUrl = 'https://openrouter.ai/api/v1'; - private constructor() {} + private constructor() { } static getInstance(): AIService { if (!AIService.instance) { @@ -130,7 +160,7 @@ class AIService { private createSystemPrompt(context: ContentContext): string { const isSeries = 'episodesBySeason' in (context as any); const isEpisode = !isSeries && 'showTitle' in (context as any); - + if (isSeries) { const series = context as SeriesContext; const currentDate = new Date().toISOString().split('T')[0]; @@ -148,11 +178,19 @@ CRITICAL: Today's date is ${currentDate}. Use ONLY the verified information prov VERIFIED CURRENT SERIES INFORMATION FROM DATABASE: - Title: ${series.title} +- Original Language: ${series.originalLanguage || 'Unknown'} +- Status: ${series.status || 'Unknown'} - First Air Date: ${series.firstAirDate || 'Unknown'} - Last Air Date: ${series.lastAirDate || 'Unknown'} - Seasons: ${series.totalSeasons} - Episodes: ${series.totalEpisodes} +- Classification: ${series.type || 'Scripted'} +- Content Rating: ${series.contentRating || 'Not Rated'} - Genres: ${series.genres.join(', ') || 'Unknown'} +- TMDB Rating: ${series.voteAverage ? `${series.voteAverage}/10 (${series.voteCount} votes)` : 'N/A'} +- Popularity Score: ${series.popularity || 'N/A'} +- Created By: ${series.createdBy?.join(', ') || 'Unknown'} +- Production: ${series.productionCompanies?.join(', ') || 'Unknown'} - Synopsis: ${series.overview || 'No synopsis available'} Cast: @@ -192,6 +230,11 @@ VERIFIED CURRENT INFORMATION FROM DATABASE: - Air Date: ${ep.airDate || 'Unknown'} - Release Status: ${ep.released ? 'RELEASED AND AVAILABLE FOR VIEWING' : 'Not Yet Released'} - Runtime: ${ep.runtime ? `${ep.runtime} minutes` : 'Unknown'} +- TMDB Rating: ${ep.voteAverage ? `${ep.voteAverage}/10` : 'N/A'} +- Show Content Rating: ${ep.contentRating || 'Not Rated'} +- Show Genres: ${ep.showGenres?.join(', ') || 'Unknown'} +- Network: ${ep.showNetworks?.join(', ') || 'Unknown'} +- Show Status: ${ep.showStatus || 'Unknown'} - Synopsis: ${ep.overview || 'No synopsis available'} Cast: @@ -227,11 +270,22 @@ CRITICAL: Today's date is ${currentDate}. Use ONLY the verified information prov VERIFIED CURRENT MOVIE INFORMATION FROM DATABASE: - Title: ${movie.title} +- Original Language: ${movie.originalLanguage || 'Unknown'} +- Status: ${movie.status || 'Unknown'} - Release Date: ${movie.releaseDate || 'Unknown'} +- Content Rating: ${movie.contentRating || 'Not Rated'} - Runtime: ${movie.runtime ? `${movie.runtime} minutes` : 'Unknown'} - Genres: ${movie.genres.join(', ') || 'Unknown'} +- TMDB Rating: ${movie.voteAverage ? `${movie.voteAverage}/10 (${movie.voteCount} votes)` : 'N/A'} +- Popularity Score: ${movie.popularity || 'N/A'} +- Budget: ${movie.budget && movie.budget > 0 ? `$${movie.budget.toLocaleString()}` : 'Unknown'} +- Revenue: ${movie.revenue && movie.revenue > 0 ? `$${movie.revenue.toLocaleString()}` : 'Unknown'} +- Production: ${movie.productionCompanies?.join(', ') || 'Unknown'} +- Countries: ${movie.productionCountries?.join(', ') || 'Unknown'} +- Spoken Languages: ${movie.spokenLanguages?.join(', ') || 'Unknown'} - Tagline: ${movie.tagline || 'N/A'} - Synopsis: ${movie.overview || 'No synopsis available'} +- IMDb ID: ${movie.imdbId || 'N/A'} Cast: ${movie.cast.map(c => `- ${c.name} as ${c.character}`).join('\n')} @@ -261,8 +315,8 @@ Answer questions about this movie using only the verified database information a } async sendMessage( - message: string, - context: ContentContext, + message: string, + context: ContentContext, conversationHistory: ChatMessage[] = [] ): Promise { if (!await this.isConfigured()) { @@ -271,7 +325,7 @@ Answer questions about this movie using only the verified database information a try { const systemPrompt = this.createSystemPrompt(context); - + // Prepare messages for API const messages = [ { role: 'system', content: systemPrompt }, @@ -288,7 +342,7 @@ Answer questions about this movie using only the verified database information a if (__DEV__) { console.log('[AIService] Sending request to OpenRouter with context:', { contentType: 'showTitle' in context ? 'episode' : 'movie', - title: 'showTitle' in context ? + title: 'showTitle' in context ? `${(context as EpisodeContext).showTitle} S${(context as EpisodeContext).seasonNumber}E${(context as EpisodeContext).episodeNumber}` : (context as MovieContext).title, messageCount: messages.length @@ -304,7 +358,7 @@ Answer questions about this movie using only the verified database information a 'X-Title': 'Nuvio - AI Chat', }, body: JSON.stringify({ - model: 'openai/gpt-oss-20b:free', + model: 'xiaomi/mimo-v2-flash:free', messages, max_tokens: 1000, temperature: 0.7, @@ -321,13 +375,13 @@ Answer questions about this movie using only the verified database information a } const data: OpenRouterResponse = await response.json(); - + if (!data.choices || data.choices.length === 0) { throw new Error('No response received from AI service'); } const responseContent = data.choices[0].message.content; - + if (__DEV__ && data.usage) { console.log('[AIService] Token usage:', data.usage); } @@ -368,7 +422,7 @@ Answer questions about this movie using only the verified database information a // TMDB returns full ISO timestamps; keep only date part releaseDate = String(anyDate).split('T')[0]; } - } catch {} + } catch { } const statusText: string = (movieData.status || '').toString().toLowerCase(); let released = statusText === 'released'; if (!released && releaseDate) { @@ -408,16 +462,36 @@ Answer questions about this movie using only the verified database information a })) || [], runtime: movieData.runtime, tagline: movieData.tagline, - keywords: movieData.keywords?.keywords?.map((k: any) => k.name) || - movieData.keywords?.results?.map((k: any) => k.name) || [] + keywords: movieData.keywords?.keywords?.map((k: any) => k.name) || + movieData.keywords?.results?.map((k: any) => k.name) || [], + // Enhanced fields + voteAverage: movieData.vote_average, + voteCount: movieData.vote_count, + popularity: movieData.popularity, + budget: movieData.budget, + revenue: movieData.revenue, + productionCompanies: movieData.production_companies?.map((c: any) => c.name) || [], + productionCountries: movieData.production_countries?.map((c: any) => c.name) || [], + spokenLanguages: movieData.spoken_languages?.map((l: any) => l.english_name || l.name) || [], + originalLanguage: movieData.original_language, + status: movieData.status, + contentRating: (() => { + // Extract US content rating from release_dates + try { + const usRelease = movieData.release_dates?.results?.find((r: any) => r.iso_3166_1 === 'US'); + const certification = usRelease?.release_dates?.find((d: any) => d.certification)?.certification; + return certification || undefined; + } catch { return undefined; } + })(), + imdbId: movieData.external_ids?.imdb_id || movieData.imdb_id, }; } // Helper method to create context from TMDB episode data static createEpisodeContext( - episodeData: any, - showData: any, - seasonNumber: number, + episodeData: any, + showData: any, + seasonNumber: number, episodeNumber: number ): EpisodeContext { // Compute release status from TMDB air date @@ -428,7 +502,7 @@ Answer questions about this movie using only the verified database information a const parsed = new Date(airDate); if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now(); } - } catch {} + } catch { } // Heuristics: if TMDB provides meaningful content, treat as released if (!released) { const hasOverview = typeof episodeData.overview === 'string' && episodeData.overview.trim().length > 40; @@ -479,7 +553,19 @@ Answer questions about this movie using only the verified database information a guestStars: episodeData.credits?.guest_stars?.map((g: any) => ({ name: g.name, character: g.character - })) || [] + })) || [], + // Enhanced fields + voteAverage: episodeData.vote_average, + showGenres: showData.genres?.map((g: any) => g.name) || [], + showNetworks: showData.networks?.map((n: any) => n.name) || [], + showStatus: showData.status, + contentRating: (() => { + // Extract US content rating from show's content_ratings + try { + const usRating = showData.content_ratings?.results?.find((r: any) => r.iso_3166_1 === 'US'); + return usRating?.rating || undefined; + } catch { return undefined; } + })(), }; } @@ -507,7 +593,7 @@ Answer questions about this movie using only the verified database information a const parsed = new Date(airDate); if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now(); } - } catch {} + } catch { } if (!released) { const hasOverview = typeof ep.overview === 'string' && ep.overview.trim().length > 40; const hasRuntime = typeof ep.runtime === 'number' && ep.runtime > 0; @@ -520,7 +606,8 @@ Answer questions about this movie using only the verified database information a title: ep.name || `Episode ${ep.episode_number}`, airDate, released, - overview: ep.overview || '' + overview: ep.overview || '', + voteAverage: ep.vote_average, }; }); }); @@ -542,6 +629,23 @@ Answer questions about this movie using only the verified database information a cast, crew, episodesBySeason: normalized, + // Enhanced fields + networks: showData.networks?.map((n: any) => n.name) || [], + status: showData.status, + originalLanguage: showData.original_language, + popularity: showData.popularity, + voteAverage: showData.vote_average, + voteCount: showData.vote_count, + createdBy: showData.created_by?.map((c: any) => c.name) || [], + contentRating: (() => { + // Extract US content rating + try { + const usRating = showData.content_ratings?.results?.find((r: any) => r.iso_3166_1 === 'US'); + return usRating?.rating || undefined; + } catch { return undefined; } + })(), + productionCompanies: showData.production_companies?.map((c: any) => c.name) || [], + type: showData.type, }; }