amr29 commited on
Commit
ec25dc4
·
verified ·
1 Parent(s): b169cc0

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +237 -0
  2. assets/favicon.ico +0 -0
  3. assets/style.css +190 -0
  4. requirements.txt +4 -0
app.py ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dash
2
+ from dash import html, dcc, Output, Input, State
3
+ import pickle
4
+ import pandas as pd
5
+ import numpy as np
6
+ import re
7
+
8
+ # Load preprocessed data
9
+ with open('processed_data.pkl', 'rb') as f:
10
+ data = pickle.load(f)
11
+
12
+ df = data['df']
13
+ similarity_matrix = data['similarity_matrix']
14
+
15
+ # Clean genres (remove empty)
16
+ all_genres = sorted({genre for genres in df['genres'] for genre in genres if genre.strip() != ''})
17
+ genre_tabs = [{'label': genre, 'value': genre} for genre in all_genres]
18
+ genre_tabs.insert(0, {'label': 'Most Popular', 'value': '__popular__'})
19
+
20
+ # Helper for recommendations
21
+ def extract_series(name):
22
+ return re.split(r'[:\-]', name)[0].strip().lower()
23
+
24
+ def get_recommendations(game_id):
25
+ idx = df[df['id'] == game_id].index[0]
26
+ sim_scores = list(enumerate(similarity_matrix[idx]))
27
+ sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)[1:]
28
+
29
+ selected = []
30
+ seen_series = set()
31
+
32
+ for i, score in sim_scores:
33
+ name = df.iloc[i]['name']
34
+ series = extract_series(name)
35
+ if series not in seen_series:
36
+ selected.append({
37
+ 'id': int(df.iloc[i]['id']),
38
+ 'name': name,
39
+ 'image': df.iloc[i]['background_image'],
40
+ 'rating': df.iloc[i]['rating'],
41
+ 'platforms': df.iloc[i]['platforms'],
42
+ })
43
+ seen_series.add(series)
44
+ if len(selected) >= 5:
45
+ break
46
+
47
+ return selected
48
+
49
+ app = dash.Dash(__name__)
50
+ server = app.server
51
+ app.title = "Game Recommender 🎮"
52
+
53
+ # Layout
54
+ app.layout = html.Div(className="app-background", children=[
55
+ html.Div(className="layout-container", children=[
56
+
57
+ html.Div(className="sidebar-panel", children=[
58
+ html.H2("Discover Games", className="sidebar-title"),
59
+ dcc.Tabs(
60
+ id='genre-tabs',
61
+ value='__popular__',
62
+ vertical=True,
63
+ children=[
64
+ dcc.Tab(label=tab['label'], value=tab['value'],
65
+ className="tab", selected_className="selected-tab")
66
+ for tab in genre_tabs
67
+ ],
68
+ className="tabs-container"
69
+ )
70
+ ]),
71
+
72
+ html.Div(className="main-container", children=[
73
+ html.H1("🎮 Game Recommender", className="fancy-title"),
74
+
75
+ dcc.Dropdown(
76
+ id='game-dropdown',
77
+ options=[{'label': row['name'], 'value': row['id']} for _, row in df.iterrows()],
78
+ placeholder="Search for a game",
79
+ className="custom-dropdown"
80
+ ),
81
+
82
+ dcc.Store(id='mode-store', data='discovery'),
83
+ dcc.Store(id='cards-data-store'),
84
+ dcc.Store(id='recommendations-data-store'),
85
+
86
+ html.Div(id='search-output'),
87
+ html.Div(id='grid-display'),
88
+ ])
89
+ ])
90
+ ])
91
+
92
+ # 1️⃣ Render grid and store grid card metadata
93
+ @app.callback(
94
+ [Output('grid-display', 'children'),
95
+ Output('cards-data-store', 'data')],
96
+ [Input('genre-tabs', 'value'),
97
+ Input('mode-store', 'data')]
98
+ )
99
+ def render_grid(selected_tab, mode):
100
+ if mode != 'discovery':
101
+ return "", dash.no_update
102
+
103
+ if selected_tab == '__popular__':
104
+ filtered = df.sort_values(by='rating', ascending=False).head(20)
105
+ else:
106
+ filtered = df[df['genres'].apply(lambda genres: selected_tab in genres)].sort_values(by='rating', ascending=False).head(20)
107
+
108
+ cards_data = []
109
+ grid_cards = []
110
+
111
+ for idx, row in enumerate(filtered.itertuples()):
112
+ card_info = {
113
+ 'id': row.id,
114
+ 'name': row.name,
115
+ 'image': row.background_image,
116
+ 'rating': row.rating
117
+ }
118
+ cards_data.append(card_info)
119
+
120
+ grid_cards.append(
121
+ html.Div([
122
+ html.Img(src=row.background_image, className="rec-image"),
123
+ html.Div([
124
+ html.H4(row.name, className="rec-title"),
125
+ html.P(f"{row.rating} ⭐", className="rec-rating")
126
+ ], className="rec-info")
127
+ ],
128
+ className="rec-card",
129
+ n_clicks=0,
130
+ id={'type': 'grid-card', 'index': idx})
131
+ )
132
+
133
+ return [
134
+ html.H3("Games", className="subtitle"),
135
+ html.Div(grid_cards, className="recommendations-container")
136
+ ], cards_data
137
+
138
+ # 2️⃣ Render search output and store recommendation metadata
139
+ @app.callback(
140
+ [Output('search-output', 'children'),
141
+ Output('recommendations-data-store', 'data')],
142
+ [Input('game-dropdown', 'value'),
143
+ Input('mode-store', 'data')]
144
+ )
145
+ def render_search(selected_id, mode):
146
+ if mode != 'search' or selected_id is None:
147
+ return "", dash.no_update
148
+
149
+ row = df[df['id'] == selected_id].iloc[0]
150
+ platforms_display = row['platforms'][:4]
151
+ platforms_hidden = ', '.join(row['platforms'])
152
+
153
+ main_game = html.Div([
154
+ html.Div([
155
+ html.Img(src=row['background_image'], className='main-image'),
156
+ html.Div([
157
+ html.H2(row['name'], className='main-title'),
158
+ html.P(f"Rating: {row['rating']} ⭐"),
159
+ html.P(f"Metacritic: {row['metacritic']} 🎯"),
160
+ html.P(f"Genres: {', '.join(row['genres'])}"),
161
+ html.P("Platforms:"),
162
+ html.Div([
163
+ html.Span(', '.join(platforms_display), title=platforms_hidden, className="platform-text")
164
+ ])
165
+ ], className="main-details")
166
+ ], className="main-card-inner")
167
+ ], className="main-card")
168
+
169
+ recs = get_recommendations(selected_id)
170
+ rec_cards = []
171
+ rec_data = []
172
+
173
+ for idx, rec in enumerate(recs):
174
+ rec_cards.append(
175
+ html.Div([
176
+ html.Img(src=rec['image'], className="rec-image"),
177
+ html.Div([
178
+ html.H4(rec['name'], className="rec-title"),
179
+ html.P(f"{rec['rating']} ⭐", className="rec-rating")
180
+ ], className="rec-info")
181
+ ], className="rec-card",
182
+ n_clicks=0,
183
+ id={'type': 'rec-card', 'index': idx})
184
+ )
185
+ rec_data.append({'id': rec['id'], 'name': rec['name']})
186
+
187
+ return html.Div([
188
+ main_game,
189
+ html.H3("You May Also Like:", className="subtitle"),
190
+ html.Div(rec_cards, className="recommendations-container")
191
+ ]), rec_data
192
+
193
+ # 3️⃣ Unified callback handling all clicks + dropdown + tabs
194
+ @app.callback(
195
+ [Output('game-dropdown', 'value'),
196
+ Output('mode-store', 'data')],
197
+ [Input('game-dropdown', 'value'),
198
+ Input('genre-tabs', 'value'),
199
+ Input({'type': 'grid-card', 'index': dash.ALL}, 'n_clicks'),
200
+ Input({'type': 'rec-card', 'index': dash.ALL}, 'n_clicks')],
201
+ [State('cards-data-store', 'data'),
202
+ State('recommendations-data-store', 'data')]
203
+ )
204
+ def handle_inputs(dropdown_value, tab_value, grid_clicks, rec_clicks, cards_data, rec_data):
205
+ ctx = dash.callback_context
206
+
207
+ if not ctx.triggered:
208
+ raise dash.exceptions.PreventUpdate
209
+
210
+ triggered = ctx.triggered[0]['prop_id']
211
+
212
+ # Handle grid card click
213
+ if "grid-card" in triggered:
214
+ for i, clicks in enumerate(grid_clicks):
215
+ if clicks and clicks > 0:
216
+ clicked_game_id = cards_data[i]['id']
217
+ return clicked_game_id, 'search'
218
+
219
+ # Handle recommendation card click
220
+ if "rec-card" in triggered:
221
+ for i, clicks in enumerate(rec_clicks):
222
+ if clicks and clicks > 0:
223
+ clicked_game_id = rec_data[i]['id']
224
+ return clicked_game_id, 'search'
225
+
226
+ # Handle search dropdown
227
+ if 'game-dropdown' in triggered and dropdown_value is not None:
228
+ return dropdown_value, 'search'
229
+
230
+ # Handle tab change
231
+ if 'genre-tabs' in triggered:
232
+ return dash.no_update, 'discovery'
233
+
234
+ return dash.no_update, dash.no_update
235
+
236
+ if __name__ == '__main__':
237
+ app.run(debug=True)
assets/favicon.ico ADDED
assets/style.css ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ margin: 0;
3
+ padding: 0;
4
+ font-family: 'Segoe UI', sans-serif;
5
+ color: #00E0FF;
6
+ background: linear-gradient(135deg, #0D0D0D 10%, #3A3A3A 100%);
7
+ background-attachment: fixed;
8
+ }
9
+
10
+ .layout-container {
11
+ display: flex;
12
+ flex-direction: row;
13
+ }
14
+
15
+ .sidebar-panel {
16
+ width: 300px;
17
+ background-color: #1A1A1A;
18
+ padding: 20px;
19
+ border-right: 3px solid #00E0FF;
20
+ height: 100vh;
21
+ position: fixed;
22
+ left: 0;
23
+ top: 0;
24
+ overflow-y: auto;
25
+ }
26
+
27
+ .sidebar-title {
28
+ text-align: center;
29
+ font-size: 26px;
30
+ margin-bottom: 30px;
31
+ color: #FF007F;
32
+ }
33
+
34
+ .tabs-container {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: 10px;
38
+ }
39
+
40
+ .tab {
41
+ background-color: #252525;
42
+ color: #9D00FF;
43
+ font-weight: bold;
44
+ padding: 15px;
45
+ font-size: 18px;
46
+ border: 2px solid #9D00FF;
47
+ border-radius: 10px;
48
+ cursor: pointer;
49
+ text-align: center;
50
+ }
51
+ .tab:hover {
52
+ background-color: #3A3A3A;
53
+ }
54
+ .selected-tab {
55
+ background-color: #9D00FF !important;
56
+ color: #00E0FF !important;
57
+ }
58
+
59
+ .main-container {
60
+ margin-left: 500px;
61
+ padding: 30px;
62
+ max-width: 1100px;
63
+ }
64
+
65
+ .fancy-title {
66
+ text-align: center;
67
+ font-size: 50px;
68
+ margin-bottom: 40px;
69
+ background: linear-gradient(45deg, #9D00FF, #FF007F);
70
+ -webkit-background-clip: text;
71
+ -webkit-text-fill-color: transparent;
72
+ text-shadow: 2px 2px 8px rgba(157, 0, 255, 0.8);
73
+ }
74
+
75
+ .custom-dropdown .Select-control {
76
+ background-color: #3A3A3A !important;
77
+ border: 2px solid #00E0FF !important;
78
+ color: #00E0FF !important;
79
+ }
80
+ .custom-dropdown input {
81
+ color: #00E0FF !important;
82
+ font-weight: bold !important;
83
+ }
84
+
85
+ /* Main search card (top game) */
86
+ .main-card {
87
+ background-color: #252525;
88
+ border: 3px solid #9D00FF;
89
+ border-radius: 20px;
90
+ padding: 20px;
91
+ margin-top: 40px;
92
+ box-shadow: 0 8px 20px rgba(0, 224, 255, 0.4);
93
+ transition: transform 0.3s ease;
94
+ }
95
+ .main-card:hover {
96
+ transform: translateY(-8px);
97
+ }
98
+ .main-card-inner {
99
+ display: flex;
100
+ align-items: center;
101
+ }
102
+ .main-image {
103
+ width: 720px;
104
+ height: 340px;
105
+ border-radius: 15px;
106
+ box-shadow: 0 4px 10px rgba(255, 0, 127, 0.6);
107
+ }
108
+ .main-details {
109
+ padding-left: 30px;
110
+ }
111
+ .main-title {
112
+ color: #FF007F;
113
+ font-size: 28px;
114
+ margin-bottom: 15px;
115
+ }
116
+
117
+ .subtitle {
118
+ text-align: center;
119
+ font-size: 28px;
120
+ margin-top: 60px;
121
+ margin-bottom: 20px;
122
+ color: #9D00FF;
123
+ }
124
+
125
+ /* Cards container (for both grid and recommendations) */
126
+ .recommendations-container {
127
+ display: flex;
128
+ justify-content: center;
129
+ flex-wrap: wrap;
130
+ gap: 20px;
131
+ }
132
+
133
+ .rec-card {
134
+ width: 200px;
135
+ height: 340px;
136
+ background-color: #353535;
137
+ border: 2px solid #9D00FF;
138
+ border-radius: 15px;
139
+ overflow: hidden;
140
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
141
+ box-shadow: 0 4px 10px rgba(0, 224, 255, 0.5);
142
+ cursor: pointer;
143
+ }
144
+ .rec-card:hover {
145
+ transform: translateY(-12px);
146
+ box-shadow: 0 8px 16px rgba(0, 224, 255, 0.6);
147
+ }
148
+ .rec-image {
149
+ width: 100%;
150
+ height: 220px;
151
+ object-fit: cover;
152
+ }
153
+ .rec-info {
154
+ padding: 10px;
155
+ }
156
+ .rec-title {
157
+ font-size: 16px;
158
+ color: #FF007F;
159
+ margin-bottom: 5px;
160
+ white-space: nowrap;
161
+ overflow: hidden;
162
+ text-overflow: ellipsis;
163
+ text-align: center;
164
+ }
165
+ .rec-rating {
166
+ font-size: 14px;
167
+ text-align: center;
168
+ margin-top: 10px;
169
+ color: #00E0FF;
170
+ }
171
+
172
+ .platform-text {
173
+ white-space: nowrap;
174
+ overflow: hidden;
175
+ text-overflow: ellipsis;
176
+ max-width: 400px;
177
+ display: inline-block;
178
+ }
179
+
180
+ /* Mobile Responsive Layout */
181
+ @media (max-width: 768px) {
182
+ .main-card-inner {
183
+ flex-direction: column;
184
+ align-items: center;
185
+ }
186
+ .main-details {
187
+ padding-left: 0;
188
+ margin-top: 20px;
189
+ }
190
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ requests
2
+ pandas
3
+ scikit-learn
4
+ dash