amr29's picture
Update app.py
f7f0934 verified
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)