import dash from dash import html, dcc, Output, Input, State import pickle import pandas as pd import numpy as np import re import os # Load preprocessed data with open('processed_data.pkl', 'rb') as f: data = pickle.load(f) df = data['df'] similarity_matrix = data['similarity_matrix'] # Clean genres (remove empty) all_genres = sorted({genre for genres in df['genres'] for genre in genres if genre.strip() != ''}) genre_tabs = [{'label': genre, 'value': genre} for genre in all_genres] genre_tabs.insert(0, {'label': 'Most Popular', 'value': '__popular__'}) # Helper for recommendations def extract_series(name): return re.split(r'[:\-]', name)[0].strip().lower() def get_recommendations(game_id): idx = df[df['id'] == game_id].index[0] sim_scores = list(enumerate(similarity_matrix[idx])) sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:] selected = [] seen_series = set() for i, score in sim_scores: name = df.iloc[i]['name'] series = extract_series(name) if series not in seen_series: selected.append({ 'id': int(df.iloc[i]['id']), 'name': name, 'image': df.iloc[i]['background_image'], 'rating': df.iloc[i]['rating'], 'platforms': df.iloc[i]['platforms'], }) seen_series.add(series) if len(selected) >= 5: break return selected app = dash.Dash(__name__) server = app.server app.title = "Game Recommender 🎮" # Layout app.layout = html.Div(className="app-background", children=[ html.Div(className="layout-container", children=[ html.Div(className="sidebar-panel", children=[ html.H2("Discover Games", className="sidebar-title"), dcc.Tabs( id='genre-tabs', value='__popular__', vertical=True, children=[ dcc.Tab(label=tab['label'], value=tab['value'], className="tab", selected_className="selected-tab") for tab in genre_tabs ], className="tabs-container" ) ]), html.Div(className="main-container", children=[ html.H1("🎮 Game Recommender", className="fancy-title"), dcc.Dropdown( id='game-dropdown', options=[{'label': row['name'], 'value': row['id']} for _, row in df.iterrows()], placeholder="Search for a game", className="custom-dropdown" ), dcc.Store(id='mode-store', data='discovery'), dcc.Store(id='cards-data-store'), dcc.Store(id='recommendations-data-store'), html.Div(id='search-output'), html.Div(id='grid-display'), ]) ]) ]) # 1️⃣ Render grid and store grid card metadata @app.callback( [Output('grid-display', 'children'), Output('cards-data-store', 'data')], [Input('genre-tabs', 'value'), Input('mode-store', 'data')] ) def render_grid(selected_tab, mode): if mode != 'discovery': return "", dash.no_update if selected_tab == '__popular__': filtered = df.sort_values(by='rating', ascending=False).head(20) else: filtered = df[df['genres'].apply(lambda genres: selected_tab in genres)].sort_values(by='rating', ascending=False).head(20) cards_data = [] grid_cards = [] for idx, row in enumerate(filtered.itertuples()): card_info = { 'id': row.id, 'name': row.name, 'image': row.background_image, 'rating': row.rating } cards_data.append(card_info) grid_cards.append( html.Div([ html.Img(src=row.background_image, className="rec-image"), html.Div([ html.H4(row.name, className="rec-title"), html.P(f"{row.rating} ⭐", className="rec-rating") ], className="rec-info") ], className="rec-card", n_clicks=0, id={'type': 'grid-card', 'index': idx}) ) return [ html.H3("Games", className="subtitle"), html.Div(grid_cards, className="recommendations-container") ], cards_data # 2️⃣ Render search output and store recommendation metadata @app.callback( [Output('search-output', 'children'), Output('recommendations-data-store', 'data')], [Input('game-dropdown', 'value'), Input('mode-store', 'data')] ) def render_search(selected_id, mode): if mode != 'search' or selected_id is None: return "", dash.no_update row = df[df['id'] == selected_id].iloc[0] platforms_display = row['platforms'][:4] platforms_hidden = ', '.join(row['platforms']) main_game = html.Div([ html.Div([ html.Img(src=row['background_image'], className='main-image'), html.Div([ html.H2(row['name'], className='main-title'), html.P(f"Rating: {row['rating']} ⭐"), html.P(f"Metacritic: {row['metacritic']} 🎯"), html.P(f"Genres: {', '.join(row['genres'])}"), html.P("Platforms:"), html.Div([ html.Span(', '.join(platforms_display), title=platforms_hidden, className="platform-text") ]) ], className="main-details") ], className="main-card-inner") ], className="main-card") recs = get_recommendations(selected_id) rec_cards = [] rec_data = [] for idx, rec in enumerate(recs): rec_cards.append( html.Div([ html.Img(src=rec['image'], className="rec-image"), html.Div([ html.H4(rec['name'], className="rec-title"), html.P(f"{rec['rating']} ⭐", className="rec-rating") ], className="rec-info") ], className="rec-card", n_clicks=0, id={'type': 'rec-card', 'index': idx}) ) rec_data.append({'id': rec['id'], 'name': rec['name']}) return html.Div([ main_game, html.H3("You May Also Like:", className="subtitle"), html.Div(rec_cards, className="recommendations-container") ]), rec_data # 3️⃣ Unified callback handling all clicks + dropdown + tabs @app.callback( [Output('game-dropdown', 'value'), Output('mode-store', 'data')], [Input('game-dropdown', 'value'), Input('genre-tabs', 'value'), Input({'type': 'grid-card', 'index': dash.ALL}, 'n_clicks'), Input({'type': 'rec-card', 'index': dash.ALL}, 'n_clicks')], [State('cards-data-store', 'data'), State('recommendations-data-store', 'data')] ) def handle_inputs(dropdown_value, tab_value, grid_clicks, rec_clicks, cards_data, rec_data): ctx = dash.callback_context if not ctx.triggered: raise dash.exceptions.PreventUpdate triggered = ctx.triggered[0]['prop_id'] # Handle grid card click if "grid-card" in triggered: for i, clicks in enumerate(grid_clicks): if clicks and clicks > 0: clicked_game_id = cards_data[i]['id'] return clicked_game_id, 'search' # Handle recommendation card click if "rec-card" in triggered: for i, clicks in enumerate(rec_clicks): if clicks and clicks > 0: clicked_game_id = rec_data[i]['id'] return clicked_game_id, 'search' # Handle search dropdown if 'game-dropdown' in triggered and dropdown_value is not None: return dropdown_value, 'search' # Handle tab change if 'genre-tabs' in triggered: return dash.no_update, 'discovery' return dash.no_update, dash.no_update if __name__ == '__main__': app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 7860)), debug=False)