import pandas as pd
import numpy as np
import plotly.express as px
import plotly.graph_objects as go
from poster_service import poster_service
def generate_movie_card_html(movie):
"""
Renders HTML for a single movie card with an inline javascript click handler
to notify Gradio about selection changes.
"""
movie_id = movie["movie_id"]
title = str(movie["title"]).replace('"', '"')
year = int(movie["year"])
year_str = str(year) if year > 0 else "N/A"
avg_rating = float(movie["avg_rating"])
rating_count = int(movie["rating_count"])
poster_markup = poster_service.get_poster_markup(movie_id, movie["title"], year)
click_js = f"""
var container = document.getElementById('hidden_movie_select');
var input = container ? container.querySelector('textarea, input') : null;
if (input) {{
var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (setter) {{
setter.call(input, '{movie_id}');
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
}} else {{
input.value = '{movie_id}';
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
}}
var target = document.getElementById('movie-details-anchor');
if (target) {{
target.scrollIntoView({{ behavior: 'smooth' }});
}}
"""
return f"""
{poster_markup}
ID: {movie_id}
⭐ {avg_rating:.1f}
{rating_count:,} votes
"""
def generate_recommendation_card_html(movie):
"""
Renders HTML for a premium DeepFM recommendation card.
"""
movie_id = movie["movie_id"]
title = str(movie["title"]).replace('"', '"')
year = int(movie["year"])
year_str = str(year) if year > 0 else "N/A"
avg_rating = float(movie["avg_rating"])
pred_rating = float(movie.get("pred_rating", avg_rating))
rating_count = int(movie["rating_count"])
rank = int(movie["rank"])
match_pct = int(movie["match_score"] * 100)
reason = str(movie["reason"])
poster_markup = poster_service.get_poster_markup(movie_id, movie["title"], year)
click_js = f"""
var container = document.getElementById('hidden_movie_select');
var input = container ? container.querySelector('textarea, input') : null;
if (input) {{
var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (setter) {{
setter.call(input, '{movie_id}');
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
}}
var target = document.getElementById('movie-details-anchor');
if (target) {{
target.scrollIntoView({{ behavior: 'smooth' }});
}}
"""
return f"""
Rank #{rank}
{poster_markup}
{reason}
{match_pct}% Match
Pred: ★ {pred_rating:.2f}
"""
def generate_similar_movie_card_html(movie):
"""
Renders HTML for a Similar Movie card, including similarity score badge.
"""
movie_id = movie["movie_id"]
title = str(movie["title"]).replace('"', '"')
year = int(movie["year"])
year_str = str(year) if year > 0 else "N/A"
avg_rating = float(movie["avg_rating"])
rating_count = int(movie["rating_count"])
sim_pct = int(movie["similarity_score"] * 100)
poster_markup = poster_service.get_poster_markup(movie_id, movie["title"], year)
click_js = f"""
var container = document.getElementById('hidden_movie_select');
var input = container ? container.querySelector('textarea, input') : null;
if (input) {{
var setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|| Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
if (setter) {{
setter.call(input, '{movie_id}');
input.dispatchEvent(new Event('input', {{ bubbles: true }}));
input.dispatchEvent(new Event('change', {{ bubbles: true }}));
}}
}}
var target = document.getElementById('movie-details-anchor');
if (target) {{
target.scrollIntoView({{ behavior: 'smooth' }});
}}
"""
return f"""
{sim_pct}% Match
{poster_markup}
ID: {movie_id}
⭐ {avg_rating:.1f}
{rating_count:,} votes
"""
def build_movie_grid_html(df):
"""
Generates the grid structure for the movie cards.
"""
if df.empty:
return """
🔍
No Matches Found
We couldn't find any movies matching your current search or filters. Try relaxing the rating or count criteria!
"""
cards_html = []
for _, row in df.iterrows():
cards_html.append(generate_movie_card_html(row))
return f'{"".join(cards_html)}
'
def build_recommendations_grid_html(df):
"""
Generates the grid structure for DeepFM recommendations cards.
"""
if df.empty:
return """
🎬
No Recommendations Available
Run predictions to view personalized recommendations for this profile.
"""
cards_html = []
for _, row in df.iterrows():
cards_html.append(generate_recommendation_card_html(row))
return f'{"".join(cards_html)}
'
def build_similar_grid_html(df):
"""
Generates the grid structure for similar movies cards.
"""
if df.empty:
return """
🔍
No Similar Movies Found
Search and select a movie above, then click Find Similar to view connections.
"""
cards_html = []
for _, row in df.iterrows():
cards_html.append(generate_similar_movie_card_html(row))
return f'{"".join(cards_html)}
'
def build_movie_details_html(profile):
"""
Generates premium HTML content for the Movie Details Panel (Movie Profile).
Incorporates global percentiles, rating consensus variance, and AI summaries.
"""
if profile is None:
return """
🎬
No Movie Selected
Click on any movie card in the explorer below to load its cinematic profile, metadata audit, data quality check, and timeline distribution charts.
"""
title = str(profile["title"]).replace('"', '"')
year = int(profile["year"])
year_str = str(year) if year > 0 else "N/A"
avg_rating = float(profile["avg_rating"])
rating_count = int(profile["rating_count"])
percentile = float(profile["percentile"])
summary = str(profile["summary"])
rating_std = float(profile["std"])
popularity_score = avg_rating * np.log1p(rating_count)
rating_pct = (avg_rating / 5.0) * 100
# Perform Data Quality checks
quality_badges = []
if year > 1900:
quality_badges.append('✓ Valid Year')
else:
quality_badges.append('⚠ Missing Year')
if rating_count >= 1000:
quality_badges.append('✓ High Sample Size')
else:
quality_badges.append('⚠ Low Sample Size')
if rating_std < 1.0:
quality_badges.append('✓ Stable Consensus')
else:
quality_badges.append('⚠ Polarizing Receptions')
badges_html = "".join(quality_badges)
# Calculate top percentile tier representation
percentile_tier = f"Top {100.0 - percentile:.1f}% Rank" if percentile > 50.0 else f"Bottom {percentile:.1f}% Rank"
percentile_color = "var(--success-color)" if percentile > 75.0 else "var(--text-secondary)"
return f"""
💡 Why Users Enjoy This Movie (AI Summary)
{summary}
Average User Rating
★ {avg_rating:.2f} / 5.00
Quality & Metadata Checks
{badges_html}
Cinematographic Statistics
Total Votes
{rating_count:,}
Consensus Dev (Std)
± {rating_std:.2f}
Discovery Index
{popularity_score:.2f}
"""
def get_rating_distribution_chart(df):
"""
Renders a dark-themed Plotly Express histogram showing rating distribution.
"""
if df.empty:
return go.Figure()
fig = px.histogram(
df,
x="avg_rating",
nbins=20,
title="Distribution of Average Movie Ratings",
labels={"avg_rating": "Average Rating", "count": "Number of Movies"},
color_discrete_sequence=["#6366F1"]
)
fig.update_layout(
paper_bgcolor="#111827",
plot_bgcolor="#111827",
font_color="#FFFFFF",
font_family="Outfit",
hoverlabel=dict(bgcolor="#1F2937", font_size=13, font_family="Outfit"),
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(showgrid=False, zeroline=False, gridcolor="rgba(255, 255, 255, 0.05)"),
yaxis=dict(showgrid=True, gridcolor="rgba(255, 255, 255, 0.05)")
)
return fig
def get_year_distribution_chart(df):
"""
Renders a dark-themed Plotly Express histogram showing movie release timelines.
"""
valid_df = df[df["year"] > 0]
if valid_df.empty:
return go.Figure()
fig = px.histogram(
valid_df,
x="year",
nbins=30,
title="Release Timeline Distribution",
labels={"year": "Release Year", "count": "Number of Movies"},
color_discrete_sequence=["#10B981"]
)
fig.update_layout(
paper_bgcolor="#111827",
plot_bgcolor="#111827",
font_color="#FFFFFF",
font_family="Outfit",
hoverlabel=dict(bgcolor="#1F2937", font_size=13, font_family="Outfit"),
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(showgrid=False, zeroline=False, gridcolor="rgba(255, 255, 255, 0.05)"),
yaxis=dict(showgrid=True, gridcolor="rgba(255, 255, 255, 0.05)")
)
return fig
def get_predictions_score_chart(df):
"""
Renders a dark-themed Plotly Express histogram showing score distribution of recommendations.
"""
if df.empty or "match_score" not in df.columns:
return go.Figure()
fig = px.histogram(
df,
x="match_score",
nbins=10,
title="Score Distribution of Recommendations",
labels={"match_score": "Match Confidence Score", "count": "Count"},
color_discrete_sequence=["#8B5CF6"]
)
fig.update_layout(
paper_bgcolor="#111827",
plot_bgcolor="#111827",
font_color="#FFFFFF",
font_family="Outfit",
hoverlabel=dict(bgcolor="#1F2937", font_size=13, font_family="Outfit"),
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(showgrid=False, zeroline=False, tickformat=".0%"),
yaxis=dict(showgrid=True, gridcolor="rgba(255, 255, 255, 0.05)")
)
return fig
def get_movie_relationship_graph(selected_movie, similar_movies_df):
"""
Generates a radial network visualization using Plotly Scatter representing
the movie-to-movie similarity linkages.
"""
if similar_movies_df.empty:
return go.Figure()
c_title = selected_movie.get("title", "Selected Movie")
c_rating = float(selected_movie.get("avg_rating", 0.0))
c_votes = int(selected_movie.get("rating_count", 0))
node_x = [0.0]
node_y = [0.0]
node_text = [f"{c_title} (Query)
Avg Rating: {c_rating:.2f} ★
Votes: {c_votes:,}"]
node_size = [30.0]
node_color = [c_rating]
edge_x = []
edge_y = []
N = len(similar_movies_df)
for i, (_, row) in enumerate(similar_movies_df.iterrows()):
theta = (2.0 * np.pi * i) / N
similarity = float(row["similarity_score"])
radius = max(0.4, 1.2 - similarity)
x = radius * np.cos(theta)
y = radius * np.sin(theta)
node_x.append(x)
node_y.append(y)
node_text.append(
f"{row['title']}
Match: {similarity*100:.1f}%
Avg Rating: {row['avg_rating']:.2f} ★
Votes: {row['rating_count']:,}"
)
node_size.append(15.0 + 5.0 * np.log1p(row["rating_count"] / 1000.0))
node_color.append(row["avg_rating"])
edge_x.extend([0.0, x, None])
edge_y.extend([0.0, y, None])
edge_trace = go.Scatter(
x=edge_x, y=edge_y,
line=dict(width=1.5, color='rgba(255, 255, 255, 0.15)'),
hoverinfo='none',
mode='lines'
)
node_trace = go.Scatter(
x=node_x, y=node_y,
mode='markers+text',
hoverinfo='text',
text=[t.split('
')[0] for t in node_text],
textposition="bottom center",
hovertext=node_text,
marker=dict(
showscale=True,
colorscale='Viridis',
color=node_color,
size=node_size,
colorbar=dict(
title='Rating',
thickness=15,
x=1.02,
len=0.8,
tickfont=dict(color='#FFFFFF')
),
line=dict(width=2, color='rgba(255,255,255,0.6)')
)
)
fig = go.Figure(data=[edge_trace, node_trace])
fig.update_layout(
title=dict(
text=f"Movie Relationship Network (Centered on {c_title})",
font=dict(color="#FFFFFF", size=16)
),
paper_bgcolor="#111827",
plot_bgcolor="#111827",
showlegend=False,
hovermode='closest',
margin=dict(b=40, l=40, r=40, t=60),
xaxis=dict(showgrid=False, zeroline=False, showticklabels=False),
yaxis=dict(showgrid=False, zeroline=False, showticklabels=False)
)
return fig
def get_single_movie_rating_distribution(distribution_dict, title):
"""
Renders a bar chart showing specific rating counts (1 to 5 stars) for this movie.
"""
if not distribution_dict:
return go.Figure()
x_val = ["1 Star", "2 Stars", "3 Stars", "4 Stars", "5 Stars"]
y_val = [distribution_dict.get(str(i), 0) for i in range(1, 6)]
fig = go.Figure(data=[
go.Bar(
x=x_val,
y=y_val,
marker_color="#6366F1",
hovertemplate="Rating: %{x}
Count: %{y:,}"
)
])
fig.update_layout(
title=dict(
text=f"Ratings Distribution for \"{title}\"",
font=dict(color="#FFFFFF", size=14)
),
paper_bgcolor="#111827",
plot_bgcolor="#111827",
font_color="#FFFFFF",
font_family="Outfit",
hoverlabel=dict(bgcolor="#1F2937", font_size=13, font_family="Outfit"),
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(showgrid=False, zeroline=False),
yaxis=dict(showgrid=True, gridcolor="rgba(255, 255, 255, 0.05)")
)
return fig
def get_single_movie_popularity_timeline(timeline_df, title):
"""
Renders a timeline line plot showing cumulative votes count growth over time.
"""
if timeline_df.empty:
return go.Figure()
fig = go.Figure(data=[
go.Scatter(
x=timeline_df["date"],
y=timeline_df["cumulative_votes"],
mode="lines",
line=dict(color="#10B981", width=3),
name="Cumulative Votes",
hovertemplate="Date: %{x|%Y-%m-%d}
Total Votes: %{y:,}"
)
])
fig.update_layout(
title=dict(
text=f"Cumulative Votes Timeline for \"{title}\"",
font=dict(color="#FFFFFF", size=14)
),
paper_bgcolor="#111827",
plot_bgcolor="#111827",
font_color="#FFFFFF",
font_family="Outfit",
hoverlabel=dict(bgcolor="#1F2937", font_size=13, font_family="Outfit"),
margin=dict(l=40, r=40, t=60, b=40),
xaxis=dict(showgrid=False, zeroline=False),
yaxis=dict(showgrid=True, gridcolor="rgba(255, 255, 255, 0.05)")
)
return fig
def get_diagnostics_html(recommender, poster_service):
"""
Renders live system statistics and DeepFM model parameters.
"""
total_cached = len(poster_service.cache)
hits = poster_service.hits
misses = poster_service.misses
total_calls = hits + misses
hit_rate = (hits / total_calls * 100) if total_calls > 0 else 0.0
model_status = "Online (deepfm_full.keras)" if recommender.model is not None else "Offline (Cold-Start Fallbacks)"
embedding_shape = str(recommender.movie_embeddings.shape) if recommender.movie_embeddings is not None else "N/A"
encoded_users = len(recommender.user_encoder.classes_) if recommender.user_encoder is not None else 0
encoded_movies = len(recommender.movie_encoder.classes_) if recommender.movie_encoder is not None else 0
return f"""
📊 Live System Diagnostics
Real-time performance metrics, internal cache audits, and DeepFM recommendation model state tracking.
🧠 Recommendation Model State
| Model Status | {model_status} |
| Latent Embeddings Shape | {embedding_shape} |
| Encoded User Count | {encoded_users:,} |
| Encoded Movie Count | {encoded_movies:,} |
🖼️ Poster API & Caching Telemetry
| Total Cached Posters | {total_cached:,} |
| Cache Hits | {hits:,} |
| Cache Misses | {misses:,} |
| Cache Hit Rate | {hit_rate:.1f}% |
"""