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}

{title}

Year: {year_str}
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}

{title}

{year_str}

{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}

{title}

Year: {year_str}
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"""

{title}

Release Year: {year_str}
{percentile_tier}
💡 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}%
"""