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,
} 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') {
@ -54,6 +54,9 @@ import Animated, {
useSharedValue,
withSpring,
withTiming,
withRepeat,
withSequence,
withDelay,
interpolate,
Extrapolate,
runOnJS
@ -80,6 +83,46 @@ 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 (
<Animated.View style={[styles.typingDot, { backgroundColor: color }, animatedStyle]} />
);
};
const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) => {
const { currentTheme } = useTheme();
const isUser = message.role === 'user';
@ -87,7 +130,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
const bubbleAnimation = useSharedValue(0);
useEffect(() => {
bubbleAnimation.value = withSpring(1, { damping: 15, stiffness: 120 });
bubbleAnimation.value = withSpring(1, { damping: 18, stiffness: 100 });
}, []);
const animatedStyle = useAnimatedStyle(() => ({
@ -97,7 +140,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
translateY: interpolate(
bubbleAnimation.value,
[0, 1],
[20, 0],
[16, 0],
Extrapolate.CLAMP
)
},
@ -105,7 +148,7 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
scale: interpolate(
bubbleAnimation.value,
[0, 1],
[0.8, 1],
[0.95, 1],
Extrapolate.CLAMP
)
}
@ -120,8 +163,15 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
animatedStyle
]}>
{!isUser && (
<View style={[styles.avatarContainer, { backgroundColor: currentTheme.colors.primary }]}>
<MaterialIcons name="smart-toy" size={16} color="white" />
<View style={styles.avatarWrapper}>
<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>
)}
@ -129,7 +179,14 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
styles.messageBubble,
isUser ? [
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,
{ backgroundColor: 'transparent' }
@ -138,15 +195,15 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
{!isUser && (
<View style={styles.assistantBlurBackdrop} pointerEvents="none">
{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
? <GlassViewComp style={StyleSheet.absoluteFill} glassEffectStyle="regular" />
: <ExpoBlurView intensity={70} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.50)' }]} />
: <ExpoBlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.45)' }]} />
</View>
)}
{isUser ? (
<Text style={[styles.messageText, { color: 'white' }]}>
<Text style={[styles.messageText, styles.userMessageText]}>
{message.content}
</Text>
) : (
@ -154,71 +211,85 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
style={{
body: {
color: currentTheme.colors.highEmphasis,
fontSize: 16,
lineHeight: 22,
fontSize: 15.5,
lineHeight: 24,
margin: 0,
padding: 0
padding: 0,
letterSpacing: 0.15,
},
paragraph: {
marginBottom: 8,
marginBottom: 12,
marginTop: 0,
color: currentTheme.colors.highEmphasis
},
heading1: {
fontSize: 20,
fontSize: 22,
fontWeight: '700',
color: currentTheme.colors.highEmphasis,
marginBottom: 8,
marginTop: 0
marginBottom: 12,
marginTop: 4,
letterSpacing: -0.3,
},
heading2: {
fontSize: 18,
fontSize: 19,
fontWeight: '600',
color: currentTheme.colors.highEmphasis,
marginBottom: 6,
marginTop: 0
marginBottom: 10,
marginTop: 4,
letterSpacing: -0.2,
},
heading3: {
fontSize: 17,
fontWeight: '600',
color: currentTheme.colors.highEmphasis,
marginBottom: 8,
marginTop: 2,
},
link: {
color: currentTheme.colors.primary,
textDecorationLine: 'underline'
},
code_inline: {
backgroundColor: currentTheme.colors.elevation2,
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
backgroundColor: 'rgba(255,255,255,0.08)',
paddingHorizontal: 8,
paddingVertical: 3,
borderRadius: 6,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14,
color: currentTheme.colors.highEmphasis,
fontSize: 13.5,
color: currentTheme.colors.primary,
},
code_block: {
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 8,
padding: 12,
marginVertical: 8,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 12,
padding: 14,
marginVertical: 10,
color: currentTheme.colors.highEmphasis,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14,
fontSize: 13.5,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
},
fence: {
backgroundColor: currentTheme.colors.elevation2,
borderRadius: 8,
padding: 12,
marginVertical: 8,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: 12,
padding: 14,
marginVertical: 10,
color: currentTheme.colors.highEmphasis,
fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
fontSize: 14,
fontSize: 13.5,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.08)',
},
bullet_list: {
marginBottom: 8,
marginBottom: 10,
marginTop: 0
},
ordered_list: {
marginBottom: 8,
marginBottom: 10,
marginTop: 0
},
list_item: {
marginBottom: 4,
marginBottom: 6,
color: currentTheme.colors.highEmphasis
},
strong: {
@ -227,38 +298,39 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
},
em: {
fontStyle: 'italic',
color: currentTheme.colors.highEmphasis
color: currentTheme.colors.mediumEmphasis
},
blockquote: {
backgroundColor: currentTheme.colors.elevation1,
borderLeftWidth: 4,
backgroundColor: 'rgba(255,255,255,0.04)',
borderLeftWidth: 3,
borderLeftColor: currentTheme.colors.primary,
paddingLeft: 12,
paddingVertical: 8,
marginVertical: 8,
borderRadius: 4,
paddingLeft: 14,
paddingVertical: 10,
marginVertical: 10,
borderRadius: 6,
},
table: {
borderWidth: 1,
borderColor: currentTheme.colors.elevation2,
borderRadius: 8,
marginVertical: 8,
borderColor: 'rgba(255,255,255,0.1)',
borderRadius: 10,
marginVertical: 10,
overflow: 'hidden',
},
thead: {
backgroundColor: currentTheme.colors.elevation1,
backgroundColor: 'rgba(255,255,255,0.06)',
},
th: {
padding: 8,
padding: 10,
fontWeight: '600',
color: currentTheme.colors.highEmphasis,
borderBottomWidth: 1,
borderBottomColor: currentTheme.colors.elevation2,
borderBottomColor: 'rgba(255,255,255,0.1)',
},
td: {
padding: 8,
padding: 10,
color: currentTheme.colors.highEmphasis,
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={[
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([], {
hour: '2-digit',
@ -277,8 +349,12 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
</View>
{isUser && (
<View style={[styles.userAvatarContainer, { backgroundColor: currentTheme.colors.elevation2 }]}>
<MaterialIcons name="person" size={16} color={currentTheme.colors.primary} />
<View style={[styles.userAvatarContainer, {
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>
)}
</Animated.View>
@ -296,23 +372,60 @@ const ChatBubble: React.FC<ChatBubbleProps> = React.memo(({ message, isLast }) =
interface SuggestionChipProps {
text: string;
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 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 (
<Animated.View style={animatedStyle}>
<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}
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>
<MaterialIcons
name="arrow-forward"
size={14}
color={currentTheme.colors.mediumEmphasis}
/>
</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 = () => {
// CustomAlert state
@ -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,7 +589,7 @@ 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 || {});
@ -487,7 +600,7 @@ const AIChatScreen: React.FC = () => {
openAlert('Error', 'Failed to load content details for AI chat');
} finally {
setIsLoadingContext(false);
{/* CustomAlert at root */}
{/* CustomAlert at root */ }
<CustomAlert
visible={alertVisible}
title={alertTitle}
@ -558,7 +671,7 @@ const AIChatScreen: React.FC = () => {
requestContext = createEpisodeContext(episodeData, showData, season, episode);
}
}
} catch {}
} catch { }
}
}
@ -656,7 +769,7 @@ const AIChatScreen: React.FC = () => {
return (
<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 && (
<View style={StyleSheet.absoluteFill} pointerEvents="none">
<FastImage
@ -708,9 +821,14 @@ const AIChatScreen: React.FC = () => {
</Text>
</View>
<View style={[styles.aiIndicator, { backgroundColor: currentTheme.colors.primary }]}>
<MaterialIcons name="smart-toy" size={20} color="white" />
</View>
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}CC`]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.aiIndicator}
>
<MaterialIcons name="auto-awesome" size={22} color="white" />
</LinearGradient>
</View>
</Animated.View>
@ -734,9 +852,14 @@ const AIChatScreen: React.FC = () => {
>
{messages.length === 0 && suggestions.length > 0 && (
<View style={styles.welcomeContainer}>
<View style={[styles.welcomeIcon, { backgroundColor: currentTheme.colors.primary }]}>
<MaterialIcons name="smart-toy" size={32} color="white" />
</View>
<LinearGradient
colors={[currentTheme.colors.primary, `${currentTheme.colors.primary}99`]}
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 }]}>
Ask me anything about
</Text>
@ -757,6 +880,7 @@ const AIChatScreen: React.FC = () => {
key={index}
text={suggestion}
onPress={() => handleSuggestionPress(suggestion)}
index={index}
/>
))}
</View>
@ -774,11 +898,11 @@ const AIChatScreen: React.FC = () => {
{isLoading && (
<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.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} />
<View style={[styles.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} />
<View style={[styles.typingDot, { backgroundColor: currentTheme.colors.mediumEmphasis }]} />
<TypingDot delay={0} color={currentTheme.colors.primary} />
<TypingDot delay={150} color={currentTheme.colors.primary} />
<TypingDot delay={300} color={currentTheme.colors.primary} />
</View>
</View>
</View>
@ -822,21 +946,29 @@ const AIChatScreen: React.FC = () => {
/>
<TouchableOpacity
style={[
styles.sendButton,
{
backgroundColor: inputText.trim() ? currentTheme.colors.primary : currentTheme.colors.elevation2
}
]}
onPress={handleSendPress}
disabled={!inputText.trim() || isLoading}
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
name="send"
size={20}
color={inputText.trim() ? 'white' : currentTheme.colors.mediumEmphasis}
name="arrow-upward"
size={22}
color={currentTheme.colors.disabled}
/>
</View>
)}
</TouchableOpacity>
</View>
</Animated.View>
@ -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',
},

View file

@ -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) {
@ -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')}
@ -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,
@ -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) {
@ -409,7 +463,27 @@ 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) || []
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);
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,
};
}