Spaces:
Sleeping
Sleeping
| 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""" | |
| <div class="movie-card" onclick="{click_js}"> | |
| <div class="movie-poster-container"> | |
| {poster_markup} | |
| </div> | |
| <div class="movie-details"> | |
| <div> | |
| <h3 class="movie-title">{title}</h3> | |
| <div class="movie-year">Year: {year_str}</div> | |
| </div> | |
| <div class="movie-meta-row"> | |
| <span class="badge-id">ID: {movie_id}</span> | |
| <span class="rating-badge">⭐ {avg_rating:.1f}</span> | |
| </div> | |
| <div class="movie-meta-row" style="margin-top: 4px;"> | |
| <span class="votes-badge">{rating_count:,} votes</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="movie-card" style="height: 440px;" onclick="{click_js}"> | |
| <div class="movie-poster-container"> | |
| <span style="position: absolute; top: 12px; left: 12px; background: var(--accent-color); color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; z-index: 10;"> | |
| Rank #{rank} | |
| </span> | |
| {poster_markup} | |
| </div> | |
| <div class="movie-details" style="padding: 16px;"> | |
| <div> | |
| <h3 class="movie-title" style="margin-bottom: 2px;">{title}</h3> | |
| <div class="movie-year">{year_str}</div> | |
| </div> | |
| <p style="color: var(--text-secondary); font-size: 12px; line-height: 1.4; margin: 8px 0; height: 38px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;"> | |
| {reason} | |
| </p> | |
| <div class="movie-meta-row" style="margin-top: auto;"> | |
| <span class="match-badge" style="background: rgba(34, 197, 94, 0.1); color: var(--success-color);">{match_pct}% Match</span> | |
| <span class="rating-badge" style="font-size: 12px; color: var(--text-secondary);"> | |
| Pred: <strong style="color: #FFAD1F; font-size: 14px;">★ {pred_rating:.2f}</strong> | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| 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""" | |
| <div class="movie-card" onclick="{click_js}"> | |
| <div class="movie-poster-container"> | |
| <span style="position: absolute; top: 12px; left: 12px; background: var(--success-color); color: #fff; font-weight: 800; font-size: 13px; padding: 4px 10px; border-radius: 6px; z-index: 10;"> | |
| {sim_pct}% Match | |
| </span> | |
| {poster_markup} | |
| </div> | |
| <div class="movie-details"> | |
| <div> | |
| <h3 class="movie-title">{title}</h3> | |
| <div class="movie-year">Year: {year_str}</div> | |
| </div> | |
| <div class="movie-meta-row"> | |
| <span class="badge-id">ID: {movie_id}</span> | |
| <span class="rating-badge">⭐ {avg_rating:.1f}</span> | |
| </div> | |
| <div class="movie-meta-row" style="margin-top: 4px;"> | |
| <span class="votes-badge">{rating_count:,} votes</span> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| def build_movie_grid_html(df): | |
| """ | |
| Generates the grid structure for the movie cards. | |
| """ | |
| if df.empty: | |
| return """ | |
| <div class="empty-state"> | |
| <div class="empty-icon">🔍</div> | |
| <div class="empty-title">No Matches Found</div> | |
| <div class="empty-desc">We couldn't find any movies matching your current search or filters. Try relaxing the rating or count criteria!</div> | |
| </div> | |
| """ | |
| cards_html = [] | |
| for _, row in df.iterrows(): | |
| cards_html.append(generate_movie_card_html(row)) | |
| return f'<div class="content-grid">{"".join(cards_html)}</div>' | |
| def build_recommendations_grid_html(df): | |
| """ | |
| Generates the grid structure for DeepFM recommendations cards. | |
| """ | |
| if df.empty: | |
| return """ | |
| <div class="empty-state"> | |
| <div class="empty-icon">🎬</div> | |
| <div class="empty-title">No Recommendations Available</div> | |
| <div class="empty-desc">Run predictions to view personalized recommendations for this profile.</div> | |
| </div> | |
| """ | |
| cards_html = [] | |
| for _, row in df.iterrows(): | |
| cards_html.append(generate_recommendation_card_html(row)) | |
| return f'<div class="content-grid">{"".join(cards_html)}</div>' | |
| def build_similar_grid_html(df): | |
| """ | |
| Generates the grid structure for similar movies cards. | |
| """ | |
| if df.empty: | |
| return """ | |
| <div class="empty-state"> | |
| <div class="empty-icon">🔍</div> | |
| <div class="empty-title">No Similar Movies Found</div> | |
| <div class="empty-desc">Search and select a movie above, then click Find Similar to view connections.</div> | |
| </div> | |
| """ | |
| cards_html = [] | |
| for _, row in df.iterrows(): | |
| cards_html.append(generate_similar_movie_card_html(row)) | |
| return f'<div class="content-grid">{"".join(cards_html)}</div>' | |
| 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 """ | |
| <div class="details-panel" style="text-align: center; padding: 50px 20px;"> | |
| <div style="font-size: 40px; margin-bottom: 15px; opacity: 0.4;">🎬</div> | |
| <h3 style="font-size: 20px; font-weight: 700; margin-bottom: 8px;">No Movie Selected</h3> | |
| <p style="color: var(--text-secondary); max-width: 400px; margin: 0 auto; font-size: 14px;"> | |
| Click on any movie card in the explorer below to load its cinematic profile, metadata audit, data quality check, and timeline distribution charts. | |
| </p> | |
| </div> | |
| """ | |
| 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('<span class="quality-badge quality-good">✓ Valid Year</span>') | |
| else: | |
| quality_badges.append('<span class="quality-badge quality-warning">⚠ Missing Year</span>') | |
| if rating_count >= 1000: | |
| quality_badges.append('<span class="quality-badge quality-good">✓ High Sample Size</span>') | |
| else: | |
| quality_badges.append('<span class="quality-badge quality-warning">⚠ Low Sample Size</span>') | |
| if rating_std < 1.0: | |
| quality_badges.append('<span class="quality-badge quality-good">✓ Stable Consensus</span>') | |
| else: | |
| quality_badges.append('<span class="quality-badge quality-warning">⚠ Polarizing Receptions</span>') | |
| 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""" | |
| <div class="details-panel"> | |
| <div class="details-header"> | |
| <div class="details-title-row"> | |
| <div> | |
| <h2 class="details-movie-title">{title}</h2> | |
| <div class="details-movie-year">Release Year: {year_str}</div> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div class="details-id-badge" style="color: {percentile_color};"> | |
| {percentile_tier} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div style="background: rgba(99, 102, 241, 0.05); padding: 18px; border-radius: 10px; margin-bottom: 24px; border: 1px solid rgba(99, 102, 241, 0.15);"> | |
| <div style="font-weight: 800; font-size: 13px; text-transform: uppercase; color: var(--accent-color); letter-spacing: 0.1em; margin-bottom: 6px;"> | |
| 💡 Why Users Enjoy This Movie (AI Summary) | |
| </div> | |
| <p style="color: var(--text-primary); font-size: 14px; margin: 0; line-height: 1.5;"> | |
| {summary} | |
| </p> | |
| </div> | |
| <div class="rating-progress-container"> | |
| <div style="display: flex; justify-content: space-between; font-weight: 600; font-size: 15px;"> | |
| <span>Average User Rating</span> | |
| <span style="color: #FFAD1F;">★ {avg_rating:.2f} / 5.00</span> | |
| </div> | |
| <div class="rating-progress-bar"> | |
| <div class="rating-progress-fill" style="width: {rating_pct}%;"></div> | |
| </div> | |
| </div> | |
| <h3 style="font-size: 15px; font-weight: 700; margin: 24px 0 12px 0;">Quality & Metadata Checks</h3> | |
| <div class="data-quality-grid"> | |
| {badges_html} | |
| </div> | |
| <h3 style="font-size: 15px; font-weight: 700; margin: 24px 0 12px 0;">Cinematographic Statistics</h3> | |
| <div class="details-stats-grid"> | |
| <div class="details-stat-card"> | |
| <div class="details-stat-label">Total Votes</div> | |
| <div class="details-stat-value">{rating_count:,}</div> | |
| </div> | |
| <div class="details-stat-card"> | |
| <div class="details-stat-label">Consensus Dev (Std)</div> | |
| <div class="details-stat-value">± {rating_std:.2f}</div> | |
| </div> | |
| <div class="details-stat-card"> | |
| <div class="details-stat-label">Discovery Index</div> | |
| <div class="details-stat-value">{popularity_score:.2f}</div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| 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"<b>{c_title}</b> (Query)<br>Avg Rating: {c_rating:.2f} ★<br>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"<b>{row['title']}</b><br>Match: {similarity*100:.1f}%<br>Avg Rating: {row['avg_rating']:.2f} ★<br>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('<br>')[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}<br>Count: %{y:,}<extra></extra>" | |
| ) | |
| ]) | |
| 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}<br>Total Votes: %{y:,}<extra></extra>" | |
| ) | |
| ]) | |
| 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""" | |
| <div style="background-color: var(--surface-color); padding: 30px; border-radius: 16px; line-height: 1.6;"> | |
| <h2 class="section-title" style="margin-bottom: 15px;">📊 Live System Diagnostics</h2> | |
| <p style="color: var(--text-secondary); margin-bottom: 25px; font-size: 14px;"> | |
| Real-time performance metrics, internal cache audits, and DeepFM recommendation model state tracking. | |
| </p> | |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 30px;"> | |
| <div style="background-color: var(--card-color); padding: 20px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px);"> | |
| <h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px; color: var(--accent-color);">🧠 Recommendation Model State</h3> | |
| <table style="width: 100%; border-collapse: collapse; font-size: 13px;"> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Model Status</td><td style="text-align: right; font-weight: 700;">{model_status}</td></tr> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Latent Embeddings Shape</td><td style="text-align: right; font-weight: 700;">{embedding_shape}</td></tr> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Encoded User Count</td><td style="text-align: right; font-weight: 700;">{encoded_users:,}</td></tr> | |
| <tr><td style="padding: 8px 0; color: var(--text-secondary);">Encoded Movie Count</td><td style="text-align: right; font-weight: 700;">{encoded_movies:,}</td></tr> | |
| </table> | |
| </div> | |
| <div style="background-color: var(--card-color); padding: 20px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.05); backdrop-filter: blur(10px);"> | |
| <h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px; color: #10B981;">🖼️ Poster API & Caching Telemetry</h3> | |
| <table style="width: 100%; border-collapse: collapse; font-size: 13px;"> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Total Cached Posters</td><td style="text-align: right; font-weight: 700;">{total_cached:,}</td></tr> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Cache Hits</td><td style="text-align: right; font-weight: 700; color: #10B981;">{hits:,}</td></tr> | |
| <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"><td style="padding: 8px 0; color: var(--text-secondary);">Cache Misses</td><td style="text-align: right; font-weight: 700; color: #FFAD1F;">{misses:,}</td></tr> | |
| <tr><td style="padding: 8px 0; color: var(--text-secondary);">Cache Hit Rate</td><td style="text-align: right; font-weight: 700;">{hit_rate:.1f}%</td></tr> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |