import streamlit as st
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from [Link] import cosine_similarity
import numpy as np
import requests
import json
import time
from [Link] import quote
# --- 1. API Configuration & Constants ---
# ⚠️ ACTION REQUIRED: Insert your TMDB API Key here ⚠️
# I have reset this to a different placeholder. Please insert your actual TMDB key
here.
TMDB_API_KEY = "3a2fbb7f9c9ab9c091c6b434df05a4e7"
# ⚠️ ACTION REQUIRED: Insert your Gemini API Key here (or leave "" if running in an
environment that injects it) ⚠️
GEMINI_API_KEY = "AIzaSyC7zam6niVf5dCMWUmn6W88rwlwVdvAPYM"
BASE_URL = '[Link]
IMAGE_BASE_URL = '[Link]
YOUTUBE_EMBED_URL = '[Link]
# LLM Configuration (Cine Bot)
GEMINI_API_URL = "[Link]
2.5-flash-preview-09-2025:generateContent"
# Structured output schema for the Cine Bot recommendation
CINE_BOT_SCHEMA = {
"type": "OBJECT",
"properties": {
"title": {"type": "STRING", "description": "The title of the recommended
movie (must be real)."},
"reason": {"type": "STRING", "description": "A concise explanation (under
50 words) why this movie matches the user's request."},
},
"propertyOrdering": ["title", "reason"]
}
# --- 2. API Call Helper Functions ---
@st.cache_data(ttl=3600 * 24 * 7) # Cache genre map for 7 days
def fetch_tmdb_genres(api_key):
"""Fetches the TMDB genre ID to Name mapping."""
if not api_key: return {}
try:
url = f"{BASE_URL}/genre/movie/list?api_key={api_key}"
response = [Link](url, timeout=5)
response.raise_for_status()
# Returns { '28': 'Action', '12': 'Adventure', ... }
genre_map = {str(g['id']): g['name'] for g in [Link]().get('genres',
[])}
return genre_map
except [Link] as e:
# Silently fail genre fetch, as we can still display movie data (with ID
fallback)
print(f"Error fetching TMDB genres: {e}")
return {}
@st.cache_data(ttl=3600) # Cache movie data for 1 hour to reduce TMDB calls
def fetch_tmdb_movies(api_key):
"""Fetches trending movies from TMDB and enriches them with trailer keys."""
if not api_key:
[Link]("TMDB API Key is missing. Please set TMDB_API_KEY.")
return [Link]()
try:
# 1. Fetch Trending Movies (initial dataset)
url = f"{BASE_URL}/trending/movie/week?api_key={api_key}"
response = [Link](url, timeout=15)
response.raise_for_status()
data = [Link]().get('results', [])
movies = []
for item in data:
if not [Link]('poster_path') or not [Link]('overview'):
continue
# Fetch trailer key for each movie (can be slow, but cached)
trailer_key = get_movie_trailer_key(item['id'], api_key)
[Link]({
'title': [Link]('title'),
'genre': ', '.join([str(g) for g in [Link]('genre_ids', [])]), #
Store as IDs internally
'overview': [Link]('overview'),
'rating': [Link]('vote_average'),
'poster_url': f"{IMAGE_BASE_URL}{item['poster_path']}",
'id': item['id'],
'trailer_key': trailer_key,
'release_date': [Link]('release_date', 'N/A')
})
return [Link](movies)
except [Link] as e:
[Link](f"Error fetching TMDB data: {e}")
return [Link]()
@st.cache_data(ttl=3600 * 24) # Cache trailer keys for 24 hours
def get_movie_trailer_key(movie_id, api_key):
"""Fetches the main trailer key for a given movie ID."""
if not api_key: return None
try:
url = f"{BASE_URL}/movie/{movie_id}/videos?api_key={api_key}"
response = [Link](url, timeout=5)
response.raise_for_status()
videos = [Link]().get('results', [])
# Look for the official trailer
for video in videos:
if [Link]('site') == 'YouTube' and 'Trailer' in [Link]('type'):
return video['key']
return None
except [Link]:
return None
def call_gemini_api(prompt, schema=None):
"""Calls the Gemini API with structured output configuration and exponential
backoff."""
if not GEMINI_API_KEY and not st.session_state.get('is_local'):
# Allow running without key if environment injects it
pass
elif not GEMINI_API_KEY:
[Link]("Gemini API Key is missing for Cine Bot.")
return "Cine Bot is currently offline because the API key is missing."
headers = {'Content-Type': 'application/json'}
payload = {
"contents": [{"parts": [{"text": prompt}]}],
}
if schema:
payload["generationConfig"] = {
"responseMimeType": "application/json",
"responseSchema": schema
}
url = f"{GEMINI_API_URL}?key={GEMINI_API_KEY}"
for attempt in range(3):
try:
response = [Link](url, headers=headers,
data=[Link](payload), timeout=15)
response.raise_for_status()
result = [Link]()
candidate = [Link]('candidates', [{}])[0]
if candidate and [Link]('content') and
candidate['content'].get('parts'):
text_part = candidate['content']['parts'][0].get('text', '{}')
if schema:
try:
return [Link](text_part)
except [Link]:
[Link](f"Cine Bot received an unparseable JSON response:
{text_part}")
return None
else:
return text_part
return None
except [Link] as e:
if response.status_code == 429 and attempt < 2:
[Link](2 ** (attempt + 1))
continue
return f"Cine Bot API Error: HTTP {response.status_code}. Details:
{[Link]}"
except [Link] as e:
return f"Cine Bot API Request Failed: {e}"
return "Cine Bot failed after multiple retries."
# --- 3. Initial Data Loading and ML/AI Setup ---
@st.cache_resource
def load_and_process_data():
"""Load data, calculate TF-IDF, and set up state."""
# 1a. Fetch Genre Map for human-readable names
GENRE_ID_TO_NAME = fetch_tmdb_genres(TMDB_API_KEY)
GENRE_NAME_TO_ID = {v: k for k, v in GENRE_ID_TO_NAME.items()}
# 1b. Fetch real TMDB movie data
df = fetch_tmdb_movies(TMDB_API_KEY)
if [Link]:
# Fallback to a single dummy movie if API fails
df = [Link]({
'title': ['Data Load Error Movie'],
'genre': ['12, 14, 28'],
'overview': ["Could not load data from TMDB. Please check your API key
and network connection."],
'rating': [0.0],
'poster_url': ['[Link]
text=API+Error'],
'id': [0],
'trailer_key': [None],
'release_date': ['N/A']
})
st.session_state['data_loaded'] = False
else:
st.session_state['data_loaded'] = True
# 2. ML Setup (TF-IDF on overviews)
tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(df['overview'].fillna(''))
indices = [Link]([Link], index=df['title']).drop_duplicates()
# 3. Create genre lists using names for UI
# Get all unique genre IDs present in the loaded movies
all_genres_ids = sorted(list(set([Link]() for genres in df['genre'] for g in
[Link](','))))
# Map IDs to names for the UI list
ALL_GENRE_NAMES = sorted([
GENRE_ID_TO_NAME.get(g, f"ID {g}")
for g in all_genres_ids if GENRE_ID_TO_NAME.get(g) # Only include genres we
have names for
])
# Return the data, ML artifacts, human-readable names, and the conversion map
return df, tfidf_matrix, indices, ALL_GENRE_NAMES, GENRE_ID_TO_NAME,
GENRE_NAME_TO_ID
# Load all required resources upfront
df, tfidf_matrix, indices, ALL_GENRE_NAMES, GENRE_ID_TO_NAME, GENRE_NAME_TO_ID =
load_and_process_data()
# --- 4. Streamlit UI/Theming and State Management ---
CUSTOM_CSS = """
<style>
/* Global Streamlit overrides for Dark Navy Theme */
.stApp {
background-color: #0b172a;
color: #ffffff;
font-family: 'Inter', sans-serif;
}
h1, h2, h3, h4, .stMarkdown {
color: #e0e0e0;
}
/* Custom Styling for Movie Cards */
.movie-card-container {
position: relative;
background-color: #1a2c42;
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
transition: transform 0.3s, box-shadow 0.3s;
overflow: hidden; /* Important for clean card edges */
}
.movie-card-container:hover {
transform: translateY(-5px);
box-shadow: 0 8px 16px rgba(10, 10, 10, 0.6);
}
/* Trailer Overlay (The Hover Effect) */
.trailer-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95); /* Semi-transparent black */
display: flex;
justify-content: center;
align-items: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.4s ease, visibility 0.4s;
z-index: 100; /* Ensure it covers the content */
padding: 15px; /* Padding for the iframe inside the overlay */
}
/* Show overlay on card hover (Only works for custom HTML cards, kept for
Predictor Tab) */
.movie-card-container:hover .trailer-overlay {
opacity: 1;
visibility: visible;
}
.trailer-iframe {
width: 100%;
/* Adjusted height for a cinematic 16:9 aspect ratio */
height: 80%;
border: none;
border-radius: 8px;
}
/* Explore Grid Item Styling (Simplified for native [Link]) */
.explore-item {
width: 100%;
text-align: center;
margin-bottom: 20px;
padding: 10px;
border-radius: 12px;
background-color: #1a2c42;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.explore-item img {
border-radius: 8px;
}
.stButton>button {
background-color: #e50914;
color: white;
border-radius: 8px;
border: none;
padding: 8px 16px;
font-weight: bold;
transition: background-color 0.2s;
}
.stButton>button:hover {
background-color: #ff2d38;
}
.explanation-box {
background-color: #2a4059;
padding: 10px;
border-radius: 6px;
margin-top: 10px;
font-size: 0.9em;
border-left: 3px solid #00c6ff;
}
/* Custom tab appearance */
.stTabs [data-baseweb="tab-list"] {
gap: 24px;
border-bottom: 3px solid #2a4059;
justify-content: space-around;
}
.stTabs [data-baseweb="tab"] {
background-color: #1a2c42;
border-radius: 8px 8px 0 0;
padding: 10px 20px;
color: #fff;
font-weight: 600;
}
.stTabs [aria-selected="true"] {
background-color: #e50914;
color: white;
}
.spinner {
border: 4px solid rgba(255, 255, 255, 0.1);
border-top: 4px solid #e50914;
border-radius: 50%;
width: 25px;
height: 25px;
animation: spin 1s linear infinite;
display: inline-block;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
"""
[Link](CUSTOM_CSS, unsafe_allow_html=True)
# Initialize Session State
if 'user_profile' not in st.session_state:
# Use tfidf_matrix.shape to ensure pref_vector is correctly sized, even if
empty
pref_vector_size = tfidf_matrix.shape[1] if tfidf_matrix.shape else 0
st.session_state['user_profile'] = {
'likes': [],
'dislikes': [],
'watched': [],
'wishlist': [],
'pref_vector': [Link](pref_vector_size),
'pref_genres': {}
}
if 'chat_history' not in st.session_state:
st.session_state.chat_history = []
if 'is_local' not in st.session_state:
st.session_state['is_local'] = (GEMINI_API_KEY != "") # Simple check if running
locally or externally
# --- 5. Recommendation Logic Functions ---
def update_user_preferences(movie_title, interaction_type):
"""Updates user profile and preference vector based on likes/dislikes (online
learning style)."""
# Guard clause if data didn't load properly
if not st.session_state.get('data_loaded', True) or [Link]:
[Link]("Cannot update profile: Movie data is not fully loaded.")
return
profile = st.session_state['user_profile']
# 1. Update lists
if interaction_type == 'like' and movie_title not in profile['likes']:
profile['likes'].append(movie_title)
if movie_title in profile['dislikes']:
profile['dislikes'].remove(movie_title)
elif interaction_type == 'dislike' and movie_title not in profile['dislikes']:
profile['dislikes'].append(movie_title)
if movie_title in profile['likes']:
profile['likes'].remove(movie_title)
# 2. Update preference vector (Content-Based Update)
try:
idx = indices[movie_title]
movie_vector = tfidf_matrix[idx]
# Adjust preference vector based on interaction
weight = 0.1
if interaction_type == 'like':
profile['pref_vector'] = profile['pref_vector'] + (movie_vector *
weight)
elif interaction_type == 'dislike':
profile['pref_vector'] = profile['pref_vector'] - (movie_vector *
weight)
# 3. Update genre preferences (Using genre IDs as keys, as they are stable)
# The 'genre' column in df contains the ID strings
movie_genres = [Link][df['title'] == movie_title, 'genre'].iloc[0].split(',
')
for genre_id in movie_genres:
score_change = 0.5 if interaction_type == 'like' else -0.5
profile['pref_genres'][genre_id] = profile['pref_genres'].get(genre_id,
0) + score_change
st.session_state['user_profile'] = profile # Save updated state
except KeyError:
[Link](f"Could not find {movie_title} in data for preference update.")
def get_recommendations_df(genre_names_pref, exclude_list, num_recs=10):
"""Generates recommendations based on genre names, user profile, and TF-IDF."""
# Convert human-readable names back to TMDB IDs for internal filtering
target_genres_ids = set()
for name in genre_names_pref:
id_val = GENRE_NAME_TO_ID.get(name)
if id_val:
target_genres_ids.add(id_val)
# Guard clause if data didn't load properly
if not st.session_state.get('data_loaded', True) or [Link]:
return [Link](num_recs) # Return first few movies as fallback
target_genres = target_genres_ids
# Filter by genre ID match (at least one matching ID)
# The 'genre' column in df contains comma-separated IDs, so we check if any
selected ID is in the string.
filtered_df = df[df['genre'].apply(lambda x: any(g in x for g in
target_genres))].copy()
if filtered_df.empty:
filtered_df = [Link]() # Fallback to all movies if genre filter yields
nothing
# 1. Content-Based Similarity (Hybrid)
user_vector = st.session_state['user_profile']['pref_vector']
base_sim = [Link](len(filtered_df))
if [Link](user_vector) != 0:
user_vector_normalized = user_vector / [Link](user_vector)
filtered_indices = filtered_df.index
if not filtered_indices.empty:
filtered_tfidf = tfidf_matrix[filtered_indices]
# Calculate similarity (dot product of user vector and movie vectors)
base_sim =
filtered_tfidf.dot(user_vector_normalized.T).toarray().flatten()
filtered_df['sim_score'] = base_sim
# 2. Apply Penalties/Exclusions
filtered_df['final_score'] = filtered_df['sim_score'] + filtered_df['rating'] *
0.1
# Exclude already watched/suggested movies
exclude_titles = set(exclude_list) | set(st.session_state['user_profile']
['watched'])
filtered_df = filtered_df[~filtered_df['title'].isin(exclude_titles)]
# Sort and return
recommended_df = filtered_df.sort_values(by='final_score',
ascending=False).head(num_recs)
return recommended_df
def render_movie_card(movie, explanation=None, show_trailer_button=False):
"""Renders a single movie card with details and poster/trailer."""
# This card uses complex HTML which is suitable for the main predictor tab
[Link](f"<div class='movie-card-container'>", unsafe_allow_html=True)
col1, col2 = [Link]([1, 3])
with col1:
# Custom HTML for image with trailer hover overlay
trailer_html = ""
if movie['trailer_key']:
trailer_html = f"""
<div class='trailer-overlay'>
<iframe
class='trailer-iframe'
src='{YOUTUBE_EMBED_URL}{movie['trailer_key']}?
autoplay=1&mute=1&controls=0'
title='YouTube video player'
frameborder='0'
allow='autoplay; encrypted-media; gyroscope; picture-in-
picture'
allowfullscreen
></iframe>
</div>
"""
title_escaped = movie['title'].replace("'", "\\'")
# NOTE: This custom HTML structure works reliably in the main body
(Predictor tab)
card_image_html = f"""
<div style="position: relative;">
{trailer_html}
<img
src="{movie['poster_url']}"
alt="{title_escaped}"
style="border-radius: 8px; width: 100%; height: auto; display:
block;"
onerror="[Link]=null;
[Link]='[Link]
>
</div>
"""
[Link](card_image_html, unsafe_allow_html=True)
if show_trailer_button and movie['trailer_key']:
[Link](f"<a href='{YOUTUBE_EMBED_URL}{movie['trailer_key']}'
target='_blank' style='text-decoration: none;'><button class='stButton'>▶️ Play
Trailer (New Tab)</button></a>", unsafe_allow_html=True)
elif show_trailer_button:
[Link]("No trailer found.")
with col2:
[Link](f"### {movie['title']}")
[Link](f"**Rating:** ⭐ {movie['rating']:.1f} / 10 | **Release:**
{movie['release_date']}")
# Display genre names instead of raw IDs (requires a quick conversion)
genre_ids = movie['genre'].split(', ')
genre_names = [GENRE_ID_TO_NAME.get(g, f"ID {g}") for g in genre_ids]
[Link](f"**Genres:** {', '.join(genre_names)}")
[Link](f"**Overview:** {movie['overview']}")
if explanation:
[Link](f"<div class='explanation-box'>**AI Reason:**
{explanation}</div>", unsafe_allow_html=True)
# Add like/dislike buttons for profile learning
like_col, dislike_col = [Link](2)
with like_col:
if [Link]("👍 Like", key=f"like_{movie['id']}_{[Link]()}"):
update_user_preferences(movie['title'], 'like')
[Link](f"You liked '{movie['title']}'! Profile updated.",
icon='👍')
with dislike_col:
if [Link]("👎 Dislike",
key=f"dislike_{movie['id']}_{[Link]()}"):
update_user_preferences(movie['title'], 'dislike')
[Link](f"You disliked '{movie['title']}'. We will avoid similar
movies.", icon='👎')
[Link]("</div>", unsafe_allow_html=True)
[Link]("---")
# --- 6. Tab Implementations ---
def tab_movie_predictor():
"""Tab 1: Movie Predictor (MCQ, Cine Bot, Watch Together)"""
[Link]("🎬 Movie Predictor")
[Link]("---")
if not st.session_state.get('data_loaded', True) or [Link]:
[Link]("Cannot load features: Please check your TMDB API Key and reload
the app.")
return
# 1. Standard Recommender (MCQ-style)
[Link]("🔍 Personalized Recommendation")
with [Link]("recommender_form"):
col1, col2 = [Link](2)
with col1:
# Use ALL_GENRE_NAMES (e.g., 'Action', 'Comedy') for user selection
genre_select = [Link](
"Select Primary Genres",
options=ALL_GENRE_NAMES,
default=ALL_GENRE_NAMES[:2] if ALL_GENRE_NAMES else []
)
with col2:
watched_movies = [Link]("Watched Movies (to exclude)",
options=df['title'].tolist())
num_recs = [Link]("Number of Recommendations", min_value=3,
max_value=10, value=5)
submitted = st.form_submit_button("Generate Cine-Match")
if submitted and genre_select:
with [Link]("Analyzing preferences and searching the cinematic
universe..."):
# genre_select is now a list of Names, handled by
get_recommendations_df
recs_df = get_recommendations_df(genre_select, watched_movies,
num_recs)
if not recs_df.empty:
[Link](f"Top {len(recs_df)} Matches for Your Selection:")
selected_names_str = ', '.join(genre_select)
for _, movie in recs_df.iterrows():
explanation = f"Suggested due to your preference for the selected
genres ({selected_names_str}) and its content similarity based on past
interactions."
render_movie_card(movie, explanation, show_trailer_button=True)
else:
[Link]("No new movies match your criteria. Try adjusting your
selections!")
[Link]("---")
# 2. Cine Bot (LLM Chatbot)
[Link]("🤖 Cine Bot: Chat for a Recommendation")
[Link]("Ask the bot anything like: 'I just watched Interstellar, what
should I watch next?'")
# Display chat history
for message in st.session_state.chat_history:
st.chat_message(message["role"]).markdown(message["content"])
if prompt := st.chat_input("Ask Cine Bot..."):
st.session_state.chat_history.append({"role": "user", "content": prompt})
st.chat_message("user").markdown(prompt)
with [Link]([Link](f"<div class='spinner'></div> Thinking...",
unsafe_allow_html=True)):
# System instruction for the bot
all_movie_titles = ', '.join(df['title'].tolist())
system_instruction = (
f"You are an expert movie recommender bot. Based on the user's
query, recommend exactly ONE movie title that is likely to be a real, well-known
movie. "
"You MUST respond ONLY with a JSON object conforming to the
provided schema. "
f"For context, the available titles in the database are:
{all_movie_titles}. If possible, pick one of these."
)
# API call to Gemini
gemini_prompt = f"User query: '{prompt}'. Recommend a movie and explain
why it matches (under 50 words)."
structured_response = call_gemini_api(
prompt=gemini_prompt,
schema=CINE_BOT_SCHEMA
)
if isinstance(structured_response, str):
st.chat_message("assistant").error(structured_response)
st.session_state.chat_history.append({"role": "assistant", "content":
structured_response})
elif structured_response:
try:
title = structured_response['title']
reason = structured_response['reason']
movie_match = df[df['title'] == title]
with st.chat_message("assistant"):
if not movie_match.empty:
[Link](f"**Cine Bot says:** Here is a perfect match
for you!")
movie_data = movie_match.iloc[0].to_dict()
render_movie_card([Link](movie_data),
explanation=reason, show_trailer_button=True)
else:
[Link](f"Cine Bot recommended **{title}**, but I
couldn't find it in the trending catalog. **Reason:** {reason}")
[Link]("Try asking for a more popular movie!")
st.session_state.chat_history.append({"role": "assistant",
"content": f"**Cine Bot recommends:** {title}. **Reason:** {reason}"})
except Exception as e:
[Link](f"Error processing Cine Bot response: {e}")
st.session_state.chat_history.append({"role": "assistant",
"content": "I apologize, I ran into an error generating that recommendation. Could
you try asking again?"})
else:
st.session_state.chat_history.append({"role": "assistant", "content":
"I apologize, I could not connect to my AI brain. Please try again in a moment."})
[Link]("---")
# 3. Watch Together (Placeholder)
[Link]("👥 Watch Together: Group Recommendation (Advanced Feature)")
[Link]("This feature would blend user profiles using collaborative filtering
and content scoring. For now, it blends genre names selected by multiple users.")
with [Link]("Configure Group Preferences"):
u_genres = []
u_genres.append([Link]("User 1 Preferences (Genres)",
options=ALL_GENRE_NAMES, key='u1g', default=ALL_GENRE_NAMES[:2] if ALL_GENRE_NAMES
else []))
u_genres.append([Link]("User 2 Preferences (Genres)",
options=ALL_GENRE_NAMES, key='u2g', default=ALL_GENRE_NAMES[2:4] if
len(ALL_GENRE_NAMES) > 4 else []))
group_submitted = [Link]("Find Group Match", key='group_match')
if group_submitted:
all_genres = set()
for g_list in u_genres:
all_genres.update(g_list)
if all_genres:
with [Link]("Blending tastes for a group consensus..."):
# Pass the genre names to the function
group_recs_df = get_recommendations_df(list(all_genres), [],
num_recs=6)
if not group_recs_df.empty:
[Link](f"Group Picks:")
for _, movie in group_recs_df.iterrows():
# Display the genre name in the explanation
first_genre_id = movie['genre'].split(',')[0].strip()
first_genre_name = GENRE_ID_TO_NAME.get(first_genre_id,
f"ID {first_genre_id}")
explanation = f"Chosen because it contains a wide array of
popular genres, including {first_genre_name}."
render_movie_card(movie, explanation,
show_trailer_button=True)
else:
[Link]("Could not find a match for the combined group
preferences.")
else:
[Link]("Please select genres for the users.")
def tab_explore():
"""Tab 2: Explore (Trending/Top Picks, Like/Dislike)"""
[Link](" Explore the Catalog")
[Link]("Discover the latest trending movies from **TMDB**. Click any
poster to see its details in the Predictor tab!")
if not st.session_state.get('data_loaded', True) or [Link]:
[Link]("Cannot load data: Please check your TMDB API Key and reload the
app.")
return
[Link]("🔥 Trending This Week")
# Simulate an infinite scroll/grid layout
[Link]("<div class='movie-grid'>", unsafe_allow_html=True)
# Display all loaded movies (up to 20 from TMDB trending)
cols_per_row = 4
for i in range(0, len(df), cols_per_row):
cols = [Link](cols_per_row)
for j, movie in [Link][i:i + cols_per_row].iterrows():
with cols[j % cols_per_row]:
# --- FIX APPLIED: Using native [Link] for reliable rendering ---
[Link]("<div class='explore-item'>", unsafe_allow_html=True)
# Use [Link] for the poster display
[Link](
movie['poster_url'],
caption=None,
use_column_width='always',
output_format='PNG',
# Using a placeholder image for fall back is complicated with
[Link]
)
# Add title and rating using markdown
[Link](f"""
<div style="text-align: center; margin-top: -10px; padding-
bottom: 10px;">
<p style="margin: 0; font-weight: bold; font-size: 0.9em;
color: #fff;">{movie['title']}</p>
<p style="margin: 0; font-size: 0.8em; color: #aaa;">⭐
{movie['rating']:.1f}</p>
</div>
""", unsafe_allow_html=True)
[Link]("</div>", unsafe_allow_html=True)
# --- END FIX ---
# Interaction buttons
like_col, dislike_col = [Link](2)
with like_col:
if [Link]("👍", key=f"E_like_{movie['id']}"):
update_user_preferences(movie['title'], 'like')
[Link](f"Liked '{movie['title']}'! Profile updated.",
icon='👍')
with dislike_col:
if [Link]("👎", key=f"E_dislike_{movie['id']}"):
update_user_preferences(movie['title'], 'dislike')
[Link](f"Disliked '{movie['title']}'. Profile updated.",
icon='👎')
[Link]("</div>", unsafe_allow_html=True)
def tab_personality_profile():
"""Tab 3: Movie Personality Profile (User Data)"""
[Link]("👤 Movie Personality Profile")
[Link]("This is your personalized cinematic taste profile, constantly
updated by the AI based on your interactions.")
profile = st.session_state['user_profile']
[Link]("Taste Summary")
# Calculate top genres
if profile['pref_genres']:
# pref_genres stores ID:Score, so we sort by score
sorted_genres = sorted(profile['pref_genres'].items(), key=lambda item:
item[1], reverse=True)
# Convert ID (key) to Name for display
top_genres_names = [
# Fallback to 'ID X' if the genre name couldn't be fetched
f"{GENRE_ID_TO_NAME.get(g, f'ID {g}')} (Score: {s:.1f})"
for g, s in sorted(sorted_genres, key=lambda item: item[1],
reverse=True) if s > 0
]
if top_genres_names:
# Display genre names
[Link](f"Your cinematic personality leans towards these Genres: **{',
'.join(top_genres_names[:5])}**.")
else:
[Link]("Start interacting (Like/Dislike) to discover your
personality!")
else:
[Link]("Start interacting (Like/Dislike) to discover your personality!")
[Link]("Activity Log")
col_l, col_d = [Link](2)
with col_l:
[Link]("👍 Liked Movies")
if profile['likes']:
[Link](f"**Total:** {len(profile['likes'])}")
[Link]("- " + "\n- ".join(profile['likes']))
else:
[Link]("*No liked movies yet.*")
with col_d:
[Link]("👎 Disliked Movies")
if profile['dislikes']:
[Link](f"**Total:** {len(profile['dislikes'])}")
[Link]("- " + "\n- ".join(profile['dislikes']))
else:
[Link]("*No disliked movies yet.*")
[Link]("---")
[Link]("Edit Preferences")
if [Link]("Reset All User Interactions", help="This will clear your likes,
dislikes, and learned profile vector."):
# Re-initialize the preference vector size size correctly
pref_vector_size = tfidf_matrix.shape[1] if tfidf_matrix.shape else 0
st.session_state['user_profile'] = {
'likes': [],
'dislikes': [],
'watched': [],
'wishlist': [],
'pref_vector': [Link](pref_vector_size),
'pref_genres': {}
}
st.session_state.chat_history = []
[Link]("Your profile has been reset. Time to build a new cinematic
personality!")
# --- 7. Main Application Structure ---
def main():
st.set_page_config(
page_title="CineMate – Movie Recommendation App",
layout="wide",
initial_sidebar_state="collapsed"
)
[Link]("CineMate 🍿")
[Link]("AI-Powered Cinematic Recommendations from TMDB")
if not TMDB_API_KEY or TMDB_API_KEY == "YOUR_TMDB_API_KEY_HERE":
[Link]("🚨 **TMDB API Key Missing:** Please replace
`YOUR_TMDB_API_KEY_HERE` with your actual TMDB key to load movie data.")
[Link]()
if not st.session_state.get('data_loaded', True) or [Link]:
[Link]("⚠️ Data loading failed. Check your API key or network
connection and try reloading the app.")
# Custom tab creation to mimic bottom navigation
tabs = [Link]([" Movie Predictor", "🗺️Explore", "👤 Movie Personality
Profile"])
with tabs[0]:
tab_movie_predictor()
with tabs[1]:
tab_explore()
with tabs[2]:
tab_personality_profile()
if __name__ == "__main__":
main()