mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-14 05:50:43 +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,
|
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',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue