updated AI model

This commit is contained in:
tapframe 2026-01-09 17:02:08 +05:30
parent 01a041aebf
commit 76310dae1b
2 changed files with 741 additions and 467 deletions

View file

@ -14,12 +14,12 @@ import {
Keyboard, Keyboard,
} from 'react-native'; } from 'react-native';
import CustomAlert from '../components/CustomAlert'; 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 { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext'; import { useTheme } from '../contexts/ThemeContext';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { BlurView as ExpoBlurView } from 'expo-blur'; import { BlurView as ExpoBlurView } from 'expo-blur';
import { LinearGradient } from 'expo-linear-gradient';
// Lazy-safe community blur import (avoid bundling issues on web) // Lazy-safe community blur import (avoid bundling issues on web)
let AndroidBlurView: any = null; let AndroidBlurView: any = null;
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
@ -54,6 +54,9 @@ import Animated, {
useSharedValue, useSharedValue,
withSpring, withSpring,
withTiming, withTiming,
withRepeat,
withSequence,
withDelay,
interpolate, interpolate,
Extrapolate, Extrapolate,
runOnJS runOnJS
@ -80,6 +83,46 @@ interface ChatBubbleProps {
isLast: boolean; 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 (
<Animated.View style={[styles.typingDot, { backgroundColor: color }, animatedStyle]} />
);
};
const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) => { const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const isUser = message.role === 'user'; const isUser = message.role === 'user';
@ -87,7 +130,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
const bubbleAnimation = useSharedValue(0); const bubbleAnimation = useSharedValue(0);
useEffect(() => { useEffect(() => {
bubbleAnimation.value = withSpring(1, { damping: 15, stiffness: 120 }); bubbleAnimation.value = withSpring(1, { damping: 18, stiffness: 100 });
}, []); }, []);
const animatedStyle = useAnimatedStyle(() => ({ const animatedStyle = useAnimatedStyle(() => ({
@ -97,7 +140,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
translateY: interpolate( translateY: interpolate(
bubbleAnimation.value, bubbleAnimation.value,
[0, 1], [0, 1],
[20, 0], [16, 0],
Extrapolate.CLAMP Extrapolate.CLAMP
) )
}, },
@ -105,7 +148,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
scale: interpolate( scale: interpolate(
bubbleAnimation.value, bubbleAnimation.value,
[0, 1], [0, 1],
[0.8, 1], [0.95, 1],
Extrapolate.CLAMP Extrapolate.CLAMP
) )
} }
@ -120,8 +163,15 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
animatedStyle animatedStyle
]}> ]}>
{!isUser && ( {!isUser && (
<View style={[styles.avatarContainer, { backgroundColor: currentTheme.colors.primary }]}> <View style={styles.avatarWrapper}>
<MaterialIcons name="smart-toy" size={16} color="white" /> <LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}99`]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.avatarContainer}
>
<MaterialIcons name="auto-awesome" size={14} color="white" />
</LinearGradient>
</View> </View>
)} )}
@ -129,7 +179,14 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
styles.messageBubble, styles.messageBubble,
isUser ? [ isUser ? [
styles.userBubble, styles.userBubble,
{ backgroundColor: currentTheme.colors.primary } {
backgroundColor: currentTheme.colors.primary,
shadowColor: currentTheme.colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.25,
shadowRadius: 8,
elevation: 6,
}
] : [ ] : [
styles.assistantBubble, styles.assistantBubble,
{ backgroundColor: 'transparent' } { backgroundColor: 'transparent' }
@ -138,15 +195,15 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
{!isUser && ( {!isUser && (
<View style={styles.assistantBlurBackdrop} pointerEvents="none"> <View style={styles.assistantBlurBackdrop} pointerEvents="none">
{Platform.OS === 'android' && AndroidBlurView {Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={16} blurRadius={8} style={StyleSheet.absoluteFill} /> ? <AndroidBlurView blurAmount={18} blurRadius={10} style={StyleSheet.absoluteFill} />
: Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable : Platform.OS === 'ios' && GlassViewComp && liquidGlassAvailable
? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" /> ? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" />
: <ExpoBlurView intensity={70} tint="dark" style={StyleSheet.absoluteFill} />} : <ExpoBlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.50)' }]} /> <View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.45)' }]} />
</View> </View>
)} )}
{isUser ? ( {isUser ? (
<Text style={[styles.messageText, { color: 'white' }]}> <Text style={[styles.messageText, styles.userMessageText]}>
{message.content} {message.content}
</Text> </Text>
) : ( ) : (
@ -154,71 +211,85 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
style={{ style={{
body: { body: {
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontSize: 16, fontSize: 15.5,
lineHeight: 22, lineHeight: 24,
margin: 0, margin: 0,
padding: 0 padding: 0,
letterSpacing: 0.15,
}, },
paragraph: { paragraph: {
marginBottom: 8, marginBottom: 12,
marginTop: 0, marginTop: 0,
color: currentTheme.colors.highEmphasis color: currentTheme.colors.highEmphasis
}, },
heading1: { heading1: {
fontSize: 20, fontSize: 22,
fontWeight: '700', fontWeight: '700',
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
marginBottom: 8, marginBottom: 12,
marginTop: 0 marginTop: 4,
letterSpacing: -0.3,
}, },
heading2: { heading2: {
fontSize: 18, fontSize: 19,
fontWeight: '600', fontWeight: '600',
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
marginBottom: 6, marginBottom: 10,
marginTop: 0 marginTop: 4,
letterSpacing: -0.2,
},
heading3: {
fontSize: 17,
fontWeight: '600',
color: currentTheme.colors.highEmphasis,
marginBottom: 8,
marginTop: 2,
}, },
link: { link: {
color: currentTheme.colors.primary, color: currentTheme.colors.primary,
textDecorationLine: 'underline' textDecorationLine: 'underline'
}, },
code_inline: { code_inline: {
backgroundColor: currentTheme.colors.elevation2, backgroundColor: 'rgba(255,255,255,0.08)',
paddingHorizontal: 6, paddingHorizontal: 8,
paddingVertical: 2, paddingVertical: 3,
borderRadius: 4, borderRadius: 6,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14, fontSize: 13.5,
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.primary,
}, },
code_block: { code_block: {
backgroundColor: currentTheme.colors.elevation2, backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 8, borderRadius: 12,
padding: 12, padding: 14,
marginVertical: 8, marginVertical: 10,
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14, fontSize: 13.5,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
}, },
fence: { fence: {
backgroundColor: currentTheme.colors.elevation2, backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 8, borderRadius: 12,
padding: 12, padding: 14,
marginVertical: 8, marginVertical: 10,
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14, fontSize: 13.5,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
}, },
bullet_list: { bullet_list: {
marginBottom: 8, marginBottom: 10,
marginTop: 0 marginTop: 0
}, },
ordered_list: { ordered_list: {
marginBottom: 8, marginBottom: 10,
marginTop: 0 marginTop: 0
}, },
list_item: { list_item: {
marginBottom: 4, marginBottom: 6,
color: currentTheme.colors.highEmphasis color: currentTheme.colors.highEmphasis
}, },
strong: { strong: {
@ -227,38 +298,39 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
}, },
em: { em: {
fontStyle: 'italic', fontStyle: 'italic',
color: currentTheme.colors.highEmphasis color: currentTheme.colors.mediumEmphasis
}, },
blockquote: { blockquote: {
backgroundColor: currentTheme.colors.elevation1, backgroundColor: 'rgba(255,255,255,0.04)',
borderLeftWidth: 4, borderLeftWidth: 3,
borderLeftColor: currentTheme.colors.primary, borderLeftColor: currentTheme.colors.primary,
paddingLeft: 12, paddingLeft: 14,
paddingVertical: 8, paddingVertical: 10,
marginVertical: 8, marginVertical: 10,
borderRadius: 4, borderRadius: 6,
}, },
table: { table: {
borderWidth: 1, borderWidth: 1,
borderColor: currentTheme.colors.elevation2, borderColor: 'rgba(255,255,255,0.1)',
borderRadius: 8, borderRadius: 10,
marginVertical: 8, marginVertical: 10,
overflow: 'hidden',
}, },
thead: { thead: {
backgroundColor: currentTheme.colors.elevation1, backgroundColor: 'rgba(255,255,255,0.06)',
}, },
th: { th: {
padding: 8, padding: 10,
fontWeight: '600', fontWeight: '600',
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: currentTheme.colors.elevation2, borderBottomColor: 'rgba(255,255,255,0.1)',
}, },
td: { td: {
padding: 8, padding: 10,
color: currentTheme.colors.highEmphasis, color: currentTheme.colors.highEmphasis,
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomColor: currentTheme.colors.elevation2, borderBottomColor: 'rgba(255,255,255,0.06)',
}, },
}} }}
> >
@ -267,7 +339,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
)} )}
<Text style={[ <Text style={[
styles.messageTime, styles.messageTime,
{ color: isUser ? 'rgba(255,255,255,0.7)' : currentTheme.colors.mediumEmphasis } { color: isUser ? 'rgba(255,255,255,0.65)' : currentTheme.colors.disabled }
]}> ]}>
{new Date(message.timestamp).toLocaleTimeString([], { {new Date(message.timestamp).toLocaleTimeString([], {
hour: '2-digit', hour: '2-digit',
@ -277,8 +349,12 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
</View> </View>
{isUser && ( {isUser && (
<View style={[styles.userAvatarContainer, { backgroundColor: currentTheme.colors.elevation2 }]}> <View style={[styles.userAvatarContainer, {
<MaterialIcons name="person" size={16} color={currentTheme.colors.primary} /> backgroundColor: 'rgba(255,255,255,0.08)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.12)',
}]}>
<MaterialIcons name="person" size={14} color={currentTheme.colors.primary} />
</View> </View>
)} )}
</Animated.View> </Animated.View>
@ -296,23 +372,60 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
interface SuggestionChipProps { interface SuggestionChipProps {
text: string; text: string;
onPress: () => void; onPress: () => void;
index: number;
} }
const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPress }) => { const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPress, index }) => {
const { currentTheme } = useTheme(); 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 ( return (
<Animated.View style={animatedStyle}>
<TouchableOpacity <TouchableOpacity
style={[styles.suggestionChip, { backgroundColor: currentTheme.colors.elevation1 }]} style={[
styles.suggestionChip,
{
backgroundColor: 'rgba(255,255,255,0.04)',
borderWidth: 1,
borderColor: `${currentTheme.colors.primary}40`,
}
]}
onPress={onPress} onPress={onPress}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={[styles.suggestionText, { color: currentTheme.colors.primary }]}> <MaterialIcons
name="lightbulb-outline"
size={16}
color={currentTheme.colors.primary}
style={styles.suggestionIcon}
/>
<Text style={[styles.suggestionText, { color: currentTheme.colors.highEmphasis }]}>
{text} {text}
</Text> </Text>
<MaterialIcons
name="arrow-forward"
size={14}
color={currentTheme.colors.mediumEmphasis}
/>
</TouchableOpacity> </TouchableOpacity>
</Animated.View>
); );
}, (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 = () => { const AIChatScreen: React.FC = () => {
// CustomAlert state // CustomAlert state
@ -451,7 +564,7 @@ const AIChatScreen: React.FC = () => {
try { try {
const path = movieData.backdrop_path || movieData.poster_path || null; const path = movieData.backdrop_path || movieData.poster_path || null;
if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`); if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`);
} catch {} } catch { }
} else { } else {
// Series: resolve TMDB numeric id first (contentId may be IMDb/stremio id) // Series: resolve TMDB numeric id first (contentId may be IMDb/stremio id)
let tmdbNumericId: number | null = null; let tmdbNumericId: number | null = null;
@ -476,7 +589,7 @@ const AIChatScreen: React.FC = () => {
try { try {
const path = showData.backdrop_path || showData.poster_path || null; const path = showData.backdrop_path || showData.poster_path || null;
if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`); if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`);
} catch {} } catch { }
if (!showData) throw new Error('Unable to load TV show details'); if (!showData) throw new Error('Unable to load TV show details');
const seriesContext = createSeriesContext(showData, allEpisodes || {}); const seriesContext = createSeriesContext(showData, allEpisodes || {});
@ -487,7 +600,7 @@ const AIChatScreen: React.FC = () => {
openAlert('Error', 'Failed to load content details for AI chat'); openAlert('Error', 'Failed to load content details for AI chat');
} finally { } finally {
setIsLoadingContext(false); setIsLoadingContext(false);
{/* CustomAlert at root */} {/* CustomAlert at root */ }
<CustomAlert <CustomAlert
visible={alertVisible} visible={alertVisible}
title={alertTitle} title={alertTitle}
@ -558,7 +671,7 @@ const AIChatScreen: React.FC = () => {
requestContext = createEpisodeContext(episodeData, showData, season, episode); requestContext = createEpisodeContext(episodeData, showData, season, episode);
} }
} }
} catch {} } catch { }
} }
} }
@ -656,7 +769,7 @@ const AIChatScreen: React.FC = () => {
return ( return (
<Animated.View style={{ flex: 1, opacity: modalOpacity }}> <Animated.View style={{ flex: 1, opacity: modalOpacity }}>
<SafeAreaView edges={['top','bottom']} style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}> <SafeAreaView edges={['top', 'bottom']} style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
{backdropUrl && ( {backdropUrl && (
<View style={StyleSheet.absoluteFill} pointerEvents="none"> <View style={StyleSheet.absoluteFill} pointerEvents="none">
<FastImage <FastImage
@ -708,9 +821,14 @@ const AIChatScreen: React.FC = () => {
</Text> </Text>
</View> </View>
<View style={[styles.aiIndicator, { backgroundColor: currentTheme.colors.primary }]}> <LinearGradient
<MaterialIcons name="smart-toy" size={20} color="white" /> colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}CC`]}
</View> start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.aiIndicator}
>
<MaterialIcons name="auto-awesome" size={22} color="white" />
</LinearGradient>
</View> </View>
</Animated.View> </Animated.View>
@ -734,9 +852,14 @@ const AIChatScreen: React.FC = () => {
> >
{messages.length === 0 && suggestions.length > 0 && ( {messages.length === 0 && suggestions.length > 0 && (
<View style={styles.welcomeContainer}> <View style={styles.welcomeContainer}>
<View style={[styles.welcomeIcon, { backgroundColor: currentTheme.colors.primary }]}> <LinearGradient
<MaterialIcons name="smart-toy" size={32} color="white" /> colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}99`]}
</View> start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.welcomeIcon}
>
<MaterialIcons name="auto-awesome" size={34} color="white" />
</LinearGradient>
<Text style={[styles.welcomeTitle, { color: currentTheme.colors.highEmphasis }]}> <Text style={[styles.welcomeTitle, { color: currentTheme.colors.highEmphasis }]}>
Ask me anything about Ask me anything about
</Text> </Text>
@ -757,6 +880,7 @@ const AIChatScreen: React.FC = () => {
key={index} key={index}
text={suggestion} text={suggestion}
onPress={() => handleSuggestionPress(suggestion)} onPress={() => handleSuggestionPress(suggestion)}
index={index}
/> />
))} ))}
</View> </View>
@ -774,11 +898,11 @@ const AIChatScreen: React.FC = () => {
{isLoading && ( {isLoading && (
<View style={styles.typingIndicator}> <View style={styles.typingIndicator}>
<View style={[styles.typingBubble, { backgroundColor: currentTheme.colors.elevation2 }]}> <View style={[styles.typingBubble, { backgroundColor: 'rgba(255,255,255,0.06)' }]}>
<View style={styles.typingDots}> <View style={styles.typingDots}>
<View style={[styles.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} /> <TypingDot delay={0} color={currentTheme.colors.primary} />
<View style={[styles.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} /> <TypingDot delay={150} color={currentTheme.colors.primary} />
<View style={[styles.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} /> <TypingDot delay={300} color={currentTheme.colors.primary} />
</View> </View>
</View> </View>
</View> </View>
@ -822,21 +946,29 @@ const AIChatScreen: React.FC = () => {
/> />
<TouchableOpacity <TouchableOpacity
style={[
styles.sendButton,
{
backgroundColor: inputText.trim() ? currentTheme.colors.primary : currentTheme.colors.elevation2
}
]}
onPress={handleSendPress} onPress={handleSendPress}
disabled={!inputText.trim() || isLoading} disabled={!inputText.trim() || isLoading}
activeOpacity={0.7} activeOpacity={0.7}
style={styles.sendButtonWrapper}
> >
{inputText.trim() ? (
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}DD`]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.sendButton}
>
<MaterialIcons name="arrow-upward" size={22} color="white" />
</LinearGradient>
) : (
<View style={[styles.sendButton, { backgroundColor: 'rgba(255,255,255,0.08)' }]}>
<MaterialIcons <MaterialIcons
name="send" name="arrow-upward"
size={20} size={22}
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis} color={currentTheme.colors.disabled}
/> />
</View>
)}
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</Animated.View> </Animated.View>
@ -862,41 +994,49 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
gap: 16, gap: 20,
}, },
loadingText: { loadingText: {
fontSize: 16, fontSize: 15,
fontWeight: '500',
letterSpacing: 0.3,
textAlign: 'center', textAlign: 'center',
}, },
header: { header: {
paddingHorizontal: 16, paddingHorizontal: 20,
paddingBottom: 12, paddingBottom: 16,
borderBottomWidth: 1, borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(255,255,255,0.1)', borderBottomColor: 'rgba(255,255,255,0.08)',
}, },
headerContent: { headerContent: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
gap: 12, gap: 14,
}, },
backButton: { backButton: {
padding: 8, padding: 10,
marginLeft: -6,
borderRadius: 12,
}, },
headerInfo: { headerInfo: {
flex: 1, flex: 1,
}, },
headerTitle: { headerTitle: {
fontSize: 20, fontSize: 22,
fontWeight: '700', fontWeight: '700',
letterSpacing: -0.3,
}, },
headerSubtitle: { headerSubtitle: {
fontSize: 14, fontSize: 14,
marginTop: 2, fontWeight: '500',
marginTop: 3,
opacity: 0.7,
letterSpacing: 0.1,
}, },
aiIndicator: { aiIndicator: {
width: 40, width: 44,
height: 40, height: 44,
borderRadius: 20, borderRadius: 22,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
@ -907,65 +1047,80 @@ const styles = StyleSheet.create({
flex: 1, flex: 1,
}, },
messagesContent: { messagesContent: {
padding: 16, padding: 20,
paddingBottom: 8, paddingBottom: 12,
}, },
welcomeContainer: { welcomeContainer: {
alignItems: 'center', alignItems: 'center',
paddingVertical: 32, paddingVertical: 40,
paddingHorizontal: 24, paddingHorizontal: 28,
}, },
welcomeIcon: { welcomeIcon: {
width: 64, width: 72,
height: 64, height: 72,
borderRadius: 32, borderRadius: 36,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginBottom: 16, marginBottom: 20,
}, },
welcomeTitle: { welcomeTitle: {
fontSize: 20, fontSize: 18,
fontWeight: '600', fontWeight: '500',
textAlign: 'center', textAlign: 'center',
letterSpacing: 0.2,
opacity: 0.85,
}, },
welcomeSubtitle: { welcomeSubtitle: {
fontSize: 22, fontSize: 24,
fontWeight: '700', fontWeight: '700',
textAlign: 'center', textAlign: 'center',
marginTop: 4, marginTop: 6,
marginBottom: 12, marginBottom: 16,
letterSpacing: -0.4,
}, },
welcomeDescription: { welcomeDescription: {
fontSize: 16, fontSize: 15,
lineHeight: 24, lineHeight: 23,
textAlign: 'center', textAlign: 'center',
marginBottom: 32, marginBottom: 36,
opacity: 0.7,
letterSpacing: 0.15,
maxWidth: 320,
}, },
suggestionsContainer: { suggestionsContainer: {
width: '100%', width: '100%',
}, },
suggestionsTitle: { suggestionsTitle: {
fontSize: 14, fontSize: 13,
fontWeight: '600', fontWeight: '600',
marginBottom: 12, marginBottom: 14,
textAlign: 'center', textAlign: 'center',
textTransform: 'uppercase',
letterSpacing: 1.2,
opacity: 0.6,
}, },
suggestionsGrid: { suggestionsGrid: {
gap: 8, gap: 10,
}, },
suggestionChip: { suggestionChip: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16, paddingHorizontal: 16,
paddingVertical: 12, paddingVertical: 14,
borderRadius: 20, borderRadius: 14,
alignSelf: 'flex-start', marginBottom: 0,
marginBottom: 8, },
suggestionIcon: {
marginRight: 10,
}, },
suggestionText: { suggestionText: {
fontSize: 15, flex: 1,
fontSize: 14.5,
fontWeight: '500', fontWeight: '500',
letterSpacing: 0.1,
}, },
messageContainer: { messageContainer: {
marginBottom: 16, marginBottom: 20,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-end', alignItems: 'flex-end',
}, },
@ -976,100 +1131,115 @@ const styles = StyleSheet.create({
justifyContent: 'flex-start', justifyContent: 'flex-start',
}, },
lastMessageContainer: { lastMessageContainer: {
marginBottom: 8, marginBottom: 12,
},
avatarWrapper: {
marginRight: 10,
}, },
avatarContainer: { avatarContainer: {
width: 32, width: 30,
height: 32, height: 30,
borderRadius: 16, borderRadius: 15,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginRight: 8,
}, },
userAvatarContainer: { userAvatarContainer: {
width: 32, width: 30,
height: 32, height: 30,
borderRadius: 16, borderRadius: 15,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
marginLeft: 8, marginLeft: 10,
}, },
messageBubble: { messageBubble: {
maxWidth: width * 0.75, maxWidth: width * 0.78,
paddingHorizontal: 16, paddingHorizontal: 18,
paddingVertical: 12, paddingVertical: 14,
borderRadius: 20, borderRadius: 22,
overflow: 'hidden', overflow: 'hidden',
}, },
userBubble: { userBubble: {
borderBottomRightRadius: 20, borderBottomRightRadius: 6,
}, },
assistantBubble: { assistantBubble: {
borderBottomLeftRadius: 20, borderBottomLeftRadius: 6,
}, },
assistantBlurBackdrop: { assistantBlurBackdrop: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
borderRadius: 20, borderRadius: 22,
}, },
messageText: { messageText: {
fontSize: 16, fontSize: 15.5,
lineHeight: 22, lineHeight: 23,
letterSpacing: 0.15,
},
userMessageText: {
color: 'white',
fontWeight: '400',
}, },
messageTime: { messageTime: {
fontSize: 12, fontSize: 11,
marginTop: 4, marginTop: 8,
opacity: 0.8, fontWeight: '500',
letterSpacing: 0.3,
}, },
typingIndicator: { typingIndicator: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-end', alignItems: 'flex-end',
marginBottom: 16, marginBottom: 20,
}, },
typingBubble: { typingBubble: {
paddingHorizontal: 16, paddingHorizontal: 18,
paddingVertical: 12, paddingVertical: 14,
borderRadius: 20, borderRadius: 22,
borderBottomLeftRadius: 4, borderBottomLeftRadius: 6,
marginLeft: 40, marginLeft: 40,
}, },
typingDots: { typingDots: {
flexDirection: 'row', flexDirection: 'row',
gap: 4, gap: 6,
alignItems: 'center',
}, },
typingDot: { typingDot: {
width: 8, width: 7,
height: 8, height: 7,
borderRadius: 4, borderRadius: 3.5,
}, },
inputContainer: { inputContainer: {
paddingHorizontal: 16, paddingHorizontal: 20,
paddingVertical: 12, paddingVertical: 14,
paddingBottom: 16, paddingBottom: 18,
}, },
inputWrapper: { inputWrapper: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-end', alignItems: 'flex-end',
borderRadius: 24, borderRadius: 26,
paddingHorizontal: 16, paddingHorizontal: 18,
paddingVertical: 8, paddingVertical: 10,
gap: 12, gap: 14,
overflow: 'hidden' overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.06)',
}, },
inputBlurBackdrop: { inputBlurBackdrop: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
borderRadius: 24, borderRadius: 26,
}, },
textInput: { textInput: {
flex: 1, flex: 1,
fontSize: 16, fontSize: 16,
lineHeight: 22, lineHeight: 23,
maxHeight: 100, maxHeight: 120,
paddingVertical: 8, paddingVertical: 10,
letterSpacing: 0.15,
},
sendButtonWrapper: {
marginLeft: 2,
}, },
sendButton: { sendButton: {
width: 40, width: 44,
height: 40, height: 44,
borderRadius: 20, borderRadius: 22,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },

View file

@ -25,6 +25,18 @@ export interface MovieContext {
runtime?: number; runtime?: number;
tagline?: string; tagline?: string;
keywords?: 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 { export interface EpisodeContext {
@ -50,6 +62,12 @@ export interface EpisodeContext {
name: string; name: string;
character: string; character: string;
}>; }>;
// New enhanced fields
voteAverage?: number;
showGenres?: string[];
showNetworks?: string[];
showStatus?: string;
contentRating?: string;
} }
export interface SeriesContext { export interface SeriesContext {
@ -76,7 +94,19 @@ export interface SeriesContext {
airDate: string; airDate: string;
released: boolean; released: boolean;
overview?: string; 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; export type ContentContext = MovieContext | EpisodeContext | SeriesContext;
@ -101,7 +131,7 @@ class AIService {
private apiKey: string | null = null; private apiKey: string | null = null;
private baseUrl = 'https://openrouter.ai/api/v1'; private baseUrl = 'https://openrouter.ai/api/v1';
private constructor() {} private constructor() { }
static getInstance(): AIService { static getInstance(): AIService {
if (!AIService.instance) { if (!AIService.instance) {
@ -148,11 +178,19 @@ CRITICAL: Today's date is ${currentDate}. Use ONLY the verified information prov
VERIFIED CURRENT SERIES INFORMATION FROM DATABASE: VERIFIED CURRENT SERIES INFORMATION FROM DATABASE:
- Title: ${series.title} - Title: ${series.title}
- Original Language: ${series.originalLanguage || 'Unknown'}
- Status: ${series.status || 'Unknown'}
- First Air Date: ${series.firstAirDate || 'Unknown'} - First Air Date: ${series.firstAirDate || 'Unknown'}
- Last Air Date: ${series.lastAirDate || 'Unknown'} - Last Air Date: ${series.lastAirDate || 'Unknown'}
- Seasons: ${series.totalSeasons} - Seasons: ${series.totalSeasons}
- Episodes: ${series.totalEpisodes} - Episodes: ${series.totalEpisodes}
- Classification: ${series.type || 'Scripted'}
- Content Rating: ${series.contentRating || 'Not Rated'}
- Genres: ${series.genres.join(', ') || 'Unknown'} - 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'} - Synopsis: ${series.overview || 'No synopsis available'}
Cast: Cast:
@ -192,6 +230,11 @@ VERIFIED CURRENT INFORMATION FROM DATABASE:
- Air Date: ${ep.airDate || 'Unknown'} - Air Date: ${ep.airDate || 'Unknown'}
- Release Status: ${ep.released ? 'RELEASED AND AVAILABLE FOR VIEWING' : 'Not Yet Released'} - Release Status: ${ep.released ? 'RELEASED AND AVAILABLE FOR VIEWING' : 'Not Yet Released'}
- Runtime: ${ep.runtime ? `${ep.runtime} minutes` : 'Unknown'} - 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'} - Synopsis: ${ep.overview || 'No synopsis available'}
Cast: Cast:
@ -227,11 +270,22 @@ CRITICAL: Today's date is ${currentDate}. Use ONLY the verified information prov
VERIFIED CURRENT MOVIE INFORMATION FROM DATABASE: VERIFIED CURRENT MOVIE INFORMATION FROM DATABASE:
- Title: ${movie.title} - Title: ${movie.title}
- Original Language: ${movie.originalLanguage || 'Unknown'}
- Status: ${movie.status || 'Unknown'}
- Release Date: ${movie.releaseDate || 'Unknown'} - Release Date: ${movie.releaseDate || 'Unknown'}
- Content Rating: ${movie.contentRating || 'Not Rated'}
- Runtime: ${movie.runtime ? `${movie.runtime} minutes` : 'Unknown'} - Runtime: ${movie.runtime ? `${movie.runtime} minutes` : 'Unknown'}
- Genres: ${movie.genres.join(', ') || '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'} - Tagline: ${movie.tagline || 'N/A'}
- Synopsis: ${movie.overview || 'No synopsis available'} - Synopsis: ${movie.overview || 'No synopsis available'}
- IMDb ID: ${movie.imdbId || 'N/A'}
Cast: Cast:
${movie.cast.map(c => `- ${c.name} as ${c.character}`).join('\n')} ${movie.cast.map(c => `- ${c.name} as ${c.character}`).join('\n')}
@ -304,7 +358,7 @@ Answer questions about this movie using only the verified database information a
'X-Title': 'Nuvio - AI Chat', 'X-Title': 'Nuvio - AI Chat',
}, },
body: JSON.stringify({ body: JSON.stringify({
model: 'openai/gpt-oss-20b:free', model: 'xiaomi/mimo-v2-flash:free',
messages, messages,
max_tokens: 1000, max_tokens: 1000,
temperature: 0.7, temperature: 0.7,
@ -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 // TMDB returns full ISO timestamps; keep only date part
releaseDate = String(anyDate).split('T')[0]; releaseDate = String(anyDate).split('T')[0];
} }
} catch {} } catch { }
const statusText: string = (movieData.status || '').toString().toLowerCase(); const statusText: string = (movieData.status || '').toString().toLowerCase();
let released = statusText === 'released'; let released = statusText === 'released';
if (!released && releaseDate) { if (!released && releaseDate) {
@ -409,7 +463,27 @@ Answer questions about this movie using only the verified database information a
runtime: movieData.runtime, runtime: movieData.runtime,
tagline: movieData.tagline, tagline: movieData.tagline,
keywords: movieData.keywords?.keywords?.map((k: any) => k.name) || keywords: movieData.keywords?.keywords?.map((k: any) => k.name) ||
movieData.keywords?.results?.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,
}; };
} }
@ -428,7 +502,7 @@ Answer questions about this movie using only the verified database information a
const parsed = new Date(airDate); const parsed = new Date(airDate);
if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now(); if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now();
} }
} catch {} } catch { }
// Heuristics: if TMDB provides meaningful content, treat as released // Heuristics: if TMDB provides meaningful content, treat as released
if (!released) { if (!released) {
const hasOverview = typeof episodeData.overview === 'string' && episodeData.overview.trim().length > 40; 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) => ({ guestStars: episodeData.credits?.guest_stars?.map((g: any) => ({
name: g.name, name: g.name,
character: g.character 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); const parsed = new Date(airDate);
if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now(); if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now();
} }
} catch {} } catch { }
if (!released) { if (!released) {
const hasOverview = typeof ep.overview === 'string' && ep.overview.trim().length > 40; const hasOverview = typeof ep.overview === 'string' && ep.overview.trim().length > 40;
const hasRuntime = typeof ep.runtime === 'number' && ep.runtime > 0; 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}`, title: ep.name || `Episode ${ep.episode_number}`,
airDate, airDate,
released, 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, cast,
crew, crew,
episodesBySeason: normalized, 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,
}; };
} }