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,
};
}