ai bug fix

This commit is contained in:
tapframe 2025-09-11 19:31:28 +05:30
parent 6162c86347
commit 9410b18962
3 changed files with 219 additions and 65 deletions

View file

@ -82,7 +82,7 @@
],
"updates": {
"enabled": true,
"checkAutomatically": "ON_ERROR",
"checkAutomatically": "ON_LOAD",
"fallbackToCacheTimeout": 30000,
"url": "https://grim-reyna-tapframe-69970143.koyeb.app/api/manifest"
},

View file

@ -6,22 +6,31 @@ import {
TextInput,
TouchableOpacity,
ScrollView,
SafeAreaView,
StatusBar,
KeyboardAvoidingView,
Platform,
Dimensions,
ActivityIndicator,
Alert,
Keyboard,
} from 'react-native';
import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { Image } from 'expo-image';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, generateConversationStarters } from '../services/aiService';
// Lazy-safe community blur import (avoid bundling issues on web)
let AndroidBlurView: any = null;
if (Platform.OS === 'android') {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
AndroidBlurView = require('@react-native-community/blur').BlurView;
} catch (_) {
AndroidBlurView = null;
}
}
import { useSafeAreaInsets, SafeAreaView } from 'react-native-safe-area-context';
import { aiService, ChatMessage, ContentContext, createMovieContext, createEpisodeContext, createSeriesContext, generateConversationStarters } from '../services/aiService';
import { tmdbService } from '../services/tmdbService';
import Markdown from 'react-native-markdown-display';
import Animated, {
@ -112,9 +121,9 @@ const ChatBubble: React.FC<ChatBubbleProps> = ({ message, isLast }) => {
]}>
{!isUser && (
<View style={styles.assistantBlurBackdrop} pointerEvents="none">
{Platform.OS === 'ios'
? <ExpoBlurView intensity={70} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={16} blurRadius={8} style={StyleSheet.absoluteFill} />}
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={16} blurRadius={8} style={StyleSheet.absoluteFill} />
: <ExpoBlurView intensity={70} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(0,0,0,0.50)' }]} />
</View>
)}
@ -294,6 +303,7 @@ const AIChatScreen: React.FC = () => {
const [isLoadingContext, setIsLoadingContext] = useState(true);
const [suggestions, setSuggestions] = useState<string[]>([]);
const [backdropUrl, setBackdropUrl] = useState<string | null>(null);
const [isKeyboardVisible, setIsKeyboardVisible] = useState(false);
// Ensure Android cleans up heavy image resources when leaving the screen to avoid flash on back
useFocusEffect(
@ -319,6 +329,21 @@ const AIChatScreen: React.FC = () => {
loadContext();
}, []);
// Track keyboard to adjust bottom padding only when hidden on iOS
useEffect(() => {
const showSub = Platform.OS === 'ios'
? Keyboard.addListener('keyboardWillShow', () => setIsKeyboardVisible(true))
: Keyboard.addListener('keyboardDidShow', () => setIsKeyboardVisible(true));
const hideSub = Platform.OS === 'ios'
? Keyboard.addListener('keyboardWillHide', () => setIsKeyboardVisible(false))
: Keyboard.addListener('keyboardDidHide', () => setIsKeyboardVisible(false));
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
// Animate in on Android for full-screen modal feel
useEffect(() => {
if (Platform.OS === 'android') {
@ -373,11 +398,9 @@ const AIChatScreen: React.FC = () => {
if (!tmdbNumericId) throw new Error('Unable to resolve TMDB ID for series');
const [showData, episodeData] = await Promise.all([
const [showData, allEpisodes] = await Promise.all([
tmdbService.getTVShowDetails(tmdbNumericId),
episodeId && seasonNumber && episodeNumber ?
tmdbService.getEpisodeDetails(tmdbNumericId, seasonNumber, episodeNumber) :
null
tmdbService.getAllEpisodes(tmdbNumericId)
]);
if (!showData) throw new Error('Unable to load TV show details');
@ -386,35 +409,9 @@ const AIChatScreen: React.FC = () => {
if (path) setBackdropUrl(`https://image.tmdb.org/t/p/w780${path}`);
} catch {}
if (episodeData && seasonNumber && episodeNumber) {
const episodeContext = createEpisodeContext(
episodeData,
showData,
seasonNumber,
episodeNumber
);
setContext(episodeContext);
} else {
// Fallback: synthesize a show-level episode-like context so AI treats it as a series
const syntheticEpisode: any = {
id: `${showData?.id ?? ''}-overview`,
name: 'Series Overview',
overview: showData?.overview ?? '',
air_date: showData?.first_air_date ?? '',
runtime: undefined,
credits: {
guest_stars: [],
crew: [],
},
};
const episodeContext = createEpisodeContext(
syntheticEpisode,
showData,
0,
0
);
setContext(episodeContext);
}
if (!showData) throw new Error('Unable to load TV show details');
const seriesContext = createSeriesContext(showData, allEpisodes || {});
setContext(seriesContext);
}
} catch (error) {
if (__DEV__) console.error('Error loading context:', error);
@ -442,7 +439,14 @@ const AIChatScreen: React.FC = () => {
try {
// If series overview is loaded, parse user query for specific episode and fetch on-demand
let requestContext = context;
if ('showTitle' in context) {
if ('episodesBySeason' in (context as any)) {
// Series-wide context; optionally detect SxE patterns to focus answer, but keep series context
const sxe = messageText.match(/s(\d+)e(\d+)/i) || messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i);
if (sxe) {
// We will append a brief hint to the user question to scope, but still pass series context
messageText = `${messageText} (about Season ${sxe[1]}, Episode ${sxe[2]})`;
}
} else if ('showTitle' in (context as any)) {
const sxe = messageText.match(/s(\d+)e(\d+)/i);
const words = messageText.match(/season\s+(\d+)[^\d]+episode\s+(\d+)/i);
const seasonOnly = messageText.match(/s(\d+)(?!e)/i) || messageText.match(/season\s+(\d+)/i);
@ -543,15 +547,14 @@ const AIChatScreen: React.FC = () => {
const getDisplayTitle = () => {
if (!context) return title;
if ('showTitle' in context) {
const ep = context as any;
// For series overview (synthetic S0E0), show just the show title
if (ep.seasonNumber === 0 && ep.episodeNumber === 0) {
return ep.showTitle;
}
return `${ep.showTitle} S${ep.seasonNumber}E${ep.episodeNumber}`;
if ('episodesBySeason' in (context as any)) {
// Always show just the series title
return (context as any).title;
} else if ('showTitle' in (context as any)) {
// For episode contexts, now also only show show title to avoid episode in title per requirement
return (context as any).showTitle;
}
return context.title || title;
return ('title' in (context as any) && (context as any).title) ? (context as any).title : title;
};
const headerAnimatedStyle = useAnimatedStyle(() => ({
@ -576,7 +579,7 @@ const AIChatScreen: React.FC = () => {
return (
<Animated.View style={{ flex: 1, opacity: modalOpacity }}>
<SafeAreaView 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">
<Image
@ -585,9 +588,9 @@ const AIChatScreen: React.FC = () => {
contentFit="cover"
recyclingKey={backdropUrl || undefined}
/>
{Platform.OS === 'ios'
? <ExpoBlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={12} blurRadius={6} style={StyleSheet.absoluteFill} />}
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={12} blurRadius={6} style={StyleSheet.absoluteFill} />
: <ExpoBlurView intensity={60} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: Platform.OS === 'android' ? 'rgba(0,0,0,0.28)' : 'rgba(0,0,0,0.45)' }]} />
</View>
)}
@ -644,7 +647,7 @@ const AIChatScreen: React.FC = () => {
style={styles.messagesContainer}
contentContainerStyle={[
styles.messagesContent,
{ paddingBottom: 120 + insets.bottom }
{ paddingBottom: ((Platform.OS === 'ios' ? (isKeyboardVisible ? 120 : 190) : 120) + insets.bottom) }
]}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
@ -703,19 +706,20 @@ const AIChatScreen: React.FC = () => {
</ScrollView>
{/* Input Container */}
<SafeAreaView edges={['bottom']} style={{ backgroundColor: 'transparent' }}>
<Animated.View style={[
styles.inputContainer,
{
backgroundColor: 'transparent',
paddingBottom: 12 + insets.bottom
paddingBottom: Platform.OS === 'ios' ? (isKeyboardVisible ? 12 : 56) : 12
},
inputAnimatedStyle
]}>
<View style={[styles.inputWrapper, { backgroundColor: 'transparent' }]}>
<View style={styles.inputBlurBackdrop} pointerEvents="none">
{Platform.OS === 'ios'
? <ExpoBlurView intensity={50} tint="dark" style={StyleSheet.absoluteFill} />
: <CommunityBlurView blurAmount={10} blurRadius={4} style={StyleSheet.absoluteFill} />}
{Platform.OS === 'android' && AndroidBlurView
? <AndroidBlurView blurAmount={10} blurRadius={4} style={StyleSheet.absoluteFill} />
: <ExpoBlurView intensity={50} tint="dark" style={StyleSheet.absoluteFill} />}
<View style={[StyleSheet.absoluteFill, { backgroundColor: Platform.OS === 'android' ? 'rgba(0,0,0,0.15)' : 'rgba(0,0,0,0.25)' }]} />
</View>
<TextInput
@ -754,6 +758,7 @@ const AIChatScreen: React.FC = () => {
</TouchableOpacity>
</View>
</Animated.View>
</SafeAreaView>
</KeyboardAvoidingView>
</SafeAreaView>
</Animated.View>

View file

@ -52,7 +52,34 @@ export interface EpisodeContext {
}>;
}
export type ContentContext = MovieContext | EpisodeContext;
export interface SeriesContext {
id: string;
title: string;
overview: string;
firstAirDate: string;
lastAirDate?: string;
totalSeasons: number;
totalEpisodes: number;
genres: string[];
cast: Array<{
name: string;
character: string;
}>;
crew: Array<{
name: string;
job: string;
}>;
episodesBySeason: Record<number, Array<{
seasonNumber: number;
episodeNumber: number;
title: string;
airDate: string;
released: boolean;
overview?: string;
}>>;
}
export type ContentContext = MovieContext | EpisodeContext | SeriesContext;
interface OpenRouterResponse {
choices: Array<{
@ -101,9 +128,58 @@ class AIService {
}
private createSystemPrompt(context: ContentContext): string {
const isEpisode = 'showTitle' in context;
const isSeries = 'episodesBySeason' in (context as any);
const isEpisode = !isSeries && 'showTitle' in (context as any);
if (isEpisode) {
if (isSeries) {
const series = context as SeriesContext;
const currentDate = new Date().toISOString().split('T')[0];
const seasonsSummary = Object.keys(series.episodesBySeason)
.sort((a, b) => Number(a) - Number(b))
.map(sn => {
const episodes = series.episodesBySeason[Number(sn)] || [];
const releasedCount = episodes.filter(e => e.released).length;
return `- Season ${sn}: ${episodes.length} episodes (${releasedCount} released)`;
})
.join('\n');
return `You are an AI assistant with access to current, up-to-date information about the TV series "${series.title}" across ALL seasons and episodes.
CRITICAL: Today's date is ${currentDate}. Use ONLY the verified information provided below from our database. IGNORE any conflicting information from your training data which may be outdated.
VERIFIED CURRENT SERIES INFORMATION FROM DATABASE:
- Title: ${series.title}
- First Air Date: ${series.firstAirDate || 'Unknown'}
- Last Air Date: ${series.lastAirDate || 'Unknown'}
- Seasons: ${series.totalSeasons}
- Episodes: ${series.totalEpisodes}
- Genres: ${series.genres.join(', ') || 'Unknown'}
- Synopsis: ${series.overview || 'No synopsis available'}
Cast:
${series.cast.map(c => `- ${c.name} as ${c.character}`).join('\n')}
Crew:
${series.crew.map(c => `- ${c.name} (${c.job})`).join('\n')}
Seasons & Episode Counts:
${seasonsSummary}
CRITICAL INSTRUCTIONS:
1. Never provide spoilers under any circumstances. Keep responses spoiler-safe.
2. The information above is from our verified database and is more current than your training data.
3. You can answer questions about ANY episode or season in the series. If dates indicate unreleased episodes, do not reveal plot details and clearly state they are unreleased.
4. Compare air dates to today's date (${currentDate}) to determine if an episode has already aired.
5. Base ALL responses on the verified information above, NOT on your training knowledge.
6. If asked about release dates or availability of episodes, refer ONLY to the database information provided.
FORMATTING RULES (use Markdown):
- Use short paragraphs separated by blank lines.
- Use clear headings (## or ###) when helpful.
- Use bullet lists for points and steps.
- Add a blank line before and after lists and headings.
- Keep lines concise; avoid giant unbroken blocks of text.
- Wrap inline code/terms with backticks only when appropriate.`;
} else if (isEpisode) {
const ep = context as EpisodeContext;
const currentDate = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
return `You are an AI assistant with access to current, up-to-date information about "${ep.showTitle}" Season ${ep.seasonNumber}, Episode ${ep.episodeNumber}: "${ep.episodeTitle}".
@ -407,11 +483,83 @@ Answer questions about this movie using only the verified database information a
};
}
// Helper to create a series-wide context including all episodes
static createSeriesContext(showData: any, episodesBySeason: Record<number, any[]>): SeriesContext {
// Build flattened cast/crew from show credits
const cast = showData.credits?.cast?.slice(0, 12).map((c: any) => ({
name: c.name,
character: c.character
})) || [];
const crew = showData.credits?.crew?.slice(0, 8).map((c: any) => ({
name: c.name,
job: c.job
})) || [];
// Normalize episodes map
const normalized: SeriesContext['episodesBySeason'] = {};
Object.keys(episodesBySeason || {}).forEach(k => {
const seasonNum = Number(k);
normalized[seasonNum] = (episodesBySeason[seasonNum] || []).map((ep: any) => {
const airDate: string = ep.air_date || '';
let released = false;
try {
if (airDate) {
const parsed = new Date(airDate);
if (!isNaN(parsed.getTime())) released = parsed.getTime() <= Date.now();
}
} catch {}
if (!released) {
const hasOverview = typeof ep.overview === 'string' && ep.overview.trim().length > 40;
const hasRuntime = typeof ep.runtime === 'number' && ep.runtime > 0;
const hasVotes = typeof ep.vote_average === 'number' && ep.vote_average > 0;
if (hasOverview || hasRuntime || hasVotes) released = true;
}
return {
seasonNumber: ep.season_number ?? seasonNum,
episodeNumber: ep.episode_number,
title: ep.name || `Episode ${ep.episode_number}`,
airDate,
released,
overview: ep.overview || ''
};
});
});
const totalSeasons = Array.isArray(showData.seasons)
? showData.seasons.filter((s: any) => s.season_number > 0).length
: Object.keys(normalized).length;
const totalEpisodes = Object.values(normalized).reduce((sum, eps) => sum + (eps?.length || 0), 0);
return {
id: showData.id?.toString() || '',
title: showData.name || showData.title || '',
overview: showData.overview || '',
firstAirDate: showData.first_air_date || '',
lastAirDate: showData.last_air_date || '',
totalSeasons,
totalEpisodes,
genres: showData.genres?.map((g: any) => g.name) || [],
cast,
crew,
episodesBySeason: normalized,
};
}
// Generate conversation starter suggestions
static generateConversationStarters(context: ContentContext): string[] {
const isEpisode = 'showTitle' in context;
if (isEpisode) {
const isSeries = 'episodesBySeason' in (context as any);
const isEpisode = !isSeries && 'showTitle' in (context as any);
if (isSeries) {
const series = context as SeriesContext;
return [
`What is ${series.title} about overall?`,
`Summarize key arcs across all seasons`,
`Which episodes are the highest rated and why?`,
`List pivotal episodes for character development`,
`How did themes evolve from Season 1 onward?`
];
} else if (isEpisode) {
const ep = context as EpisodeContext;
return [
`What happened in this episode of ${ep.showTitle}?`,
@ -439,3 +587,4 @@ export const aiService = AIService.getInstance();
export const createMovieContext = AIService.createMovieContext;
export const createEpisodeContext = AIService.createEpisodeContext;
export const generateConversationStarters = AIService.generateConversationStarters;
export const createSeriesContext = AIService.createSeriesContext;