mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 16:51:57 +00:00
updated AI model
This commit is contained in:
parent
01a041aebf
commit
76310dae1b
2 changed files with 741 additions and 467 deletions
|
|
@ -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',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue