ak0601 commited on
Commit
28bb73a
·
verified ·
1 Parent(s): 013c046

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +903 -40
src/streamlit_app.py CHANGED
@@ -1,40 +1,903 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ import json
4
+ from typing import List, Dict, Optional
5
+ from dotenv import load_dotenv
6
+ from groq import Groq
7
+ import spotipy
8
+ from spotipy.oauth2 import SpotifyOAuth
9
+ import time
10
+
11
+ # Load environment variables
12
+ load_dotenv()
13
+
14
+ # Page configuration
15
+ st.set_page_config(
16
+ page_title="VibeSync - AI Playlist Generator",
17
+ page_icon="🎵",
18
+ layout="wide",
19
+ initial_sidebar_state="collapsed"
20
+ )
21
+
22
+ # Custom CSS for premium styling
23
+ st.markdown("""
24
+ <style>
25
+ @import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&family=Inter:wght@300;400;500;600&family=Space+Grotesk:wght@300;400;500;600;700&display=swap');
26
+
27
+ :root {
28
+ --primary: #6366f1;
29
+ --primary-glow: rgba(99, 102, 241, 0.5);
30
+ --secondary: #a855f7;
31
+ --accent: #f472b6;
32
+ --bg-dark: #0f172a;
33
+ --card-bg: rgba(255, 255, 255, 0.05);
34
+ --text-main: #f8fafc;
35
+ --text-muted: #94a3b8;
36
+ }
37
+
38
+ * {
39
+ font-family: 'Inter', sans-serif;
40
+ }
41
+
42
+ h1, h2, h3, .hero-title {
43
+ font-family: 'Space Grotesk', sans-serif;
44
+ }
45
+
46
+ .stApp {
47
+ background: radial-gradient(circle at 0% 0%, #1e1b4b 0%, #0f172a 100%);
48
+ background-attachment: fixed;
49
+ }
50
+
51
+ /* Mesh Gradient Overlay */
52
+ .stApp::before {
53
+ content: "";
54
+ position: fixed;
55
+ top: 0;
56
+ left: 0;
57
+ width: 100%;
58
+ height: 100%;
59
+ background:
60
+ radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%),
61
+ radial-gradient(at 100% 0%, rgba(168, 85, 247, 0.15) 0px, transparent 50%),
62
+ radial-gradient(at 100% 100%, rgba(244, 114, 182, 0.15) 0px, transparent 50%),
63
+ radial-gradient(at 0% 100%, rgba(99, 102, 241, 0.15) 0px, transparent 50%);
64
+ pointer-events: none;
65
+ z-index: 0;
66
+ }
67
+
68
+ .main-container {
69
+ background: rgba(15, 23, 42, 0.6);
70
+ backdrop-filter: blur(20px) saturate(180%);
71
+ -webkit-backdrop-filter: blur(20px) saturate(180%);
72
+ border: 1px solid rgba(255, 255, 255, 0.1);
73
+ border-radius: 32px;
74
+ padding: 4rem;
75
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
76
+ margin: 2rem auto;
77
+ max-width: 1000px;
78
+ position: relative;
79
+ z-index: 1;
80
+ }
81
+
82
+ .hero-title {
83
+ font-size: 4.5rem;
84
+ font-weight: 800;
85
+ background: linear-gradient(to right, #818cf8, #c084fc, #f472b6);
86
+ -webkit-background-clip: text;
87
+ -webkit-text-fill-color: transparent;
88
+ text-align: center;
89
+ margin-bottom: 0.5rem;
90
+ letter-spacing: -0.05em;
91
+ animation: titleReveal 1.2s cubic-bezier(0.16, 1, 0.3, 1);
92
+ }
93
+
94
+ .hero-subtitle {
95
+ font-size: 1.4rem;
96
+ color: var(--text-muted);
97
+ text-align: center;
98
+ margin-bottom: 3rem;
99
+ font-weight: 400;
100
+ letter-spacing: 0.02em;
101
+ animation: fadeIn 1.5s ease-out;
102
+ }
103
+
104
+ .song-card {
105
+ background: rgba(30, 41, 59, 0.4);
106
+ backdrop-filter: blur(10px);
107
+ border-radius: 20px;
108
+ padding: 1.5rem;
109
+ margin: 1.2rem 0;
110
+ border: 1px solid rgba(255, 255, 255, 0.05);
111
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
112
+ display: flex;
113
+ align-items: center;
114
+ gap: 1.5rem;
115
+ position: relative;
116
+ overflow: hidden;
117
+ }
118
+
119
+ .song-card:hover {
120
+ transform: scale(1.02) translateY(-5px);
121
+ background: rgba(30, 41, 59, 0.6);
122
+ border-color: rgba(129, 140, 248, 0.3);
123
+ box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5), 0 0 20px rgba(99, 102, 241, 0.1);
124
+ }
125
+
126
+ .song-card::after {
127
+ content: '';
128
+ position: absolute;
129
+ top: 0;
130
+ right: 0;
131
+ width: 100%;
132
+ height: 100%;
133
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.03), transparent);
134
+ transform: translateX(-100%);
135
+ transition: 0.6s;
136
+ }
137
+
138
+ .song-card:hover::after {
139
+ transform: translateX(100%);
140
+ }
141
+
142
+ .song-index {
143
+ font-family: 'Space Grotesk', sans-serif;
144
+ font-size: 2.5rem;
145
+ font-weight: 700;
146
+ color: rgba(129, 140, 248, 0.2);
147
+ min-width: 60px;
148
+ text-align: center;
149
+ }
150
+
151
+ .song-title {
152
+ font-size: 1.4rem;
153
+ font-weight: 700;
154
+ color: var(--text-main);
155
+ margin-bottom: 0.2rem;
156
+ }
157
+
158
+ .song-artist {
159
+ font-size: 1.1rem;
160
+ color: #818cf8;
161
+ font-weight: 500;
162
+ }
163
+
164
+ .tag {
165
+ background: rgba(129, 140, 248, 0.1);
166
+ color: #818cf8;
167
+ padding: 0.4rem 1rem;
168
+ border-radius: 100px;
169
+ font-size: 0.75rem;
170
+ font-weight: 600;
171
+ text-transform: uppercase;
172
+ letter-spacing: 0.05em;
173
+ border: 1px solid rgba(129, 140, 248, 0.2);
174
+ }
175
+
176
+ .question-card {
177
+ background: rgba(30, 41, 59, 0.3);
178
+ border-radius: 24px;
179
+ padding: 3rem;
180
+ margin: 2rem 0;
181
+ border: 1px solid rgba(255, 255, 255, 0.05);
182
+ animation: slideUp 0.6s cubic-bezier(0.16, 1, 0.3, 1);
183
+ }
184
+
185
+ .question-text {
186
+ font-size: 1.8rem;
187
+ font-weight: 700;
188
+ color: var(--text-main);
189
+ margin-bottom: 2rem;
190
+ line-height: 1.3;
191
+ }
192
+
193
+ /* Custom Radio Styling */
194
+ [data-testid="stRadio"] > div {
195
+ background: transparent !important;
196
+ padding: 0 !important;
197
+ gap: 0.8rem;
198
+ }
199
+
200
+ [data-testid="stRadio"] label {
201
+ background: rgba(255, 255, 255, 0.03) !important;
202
+ border: 1px solid rgba(255, 255, 255, 0.05) !important;
203
+ padding: 1.2rem 1.5rem !important;
204
+ border-radius: 16px !important;
205
+ transition: all 0.3s ease !important;
206
+ color: var(--text-main) !important;
207
+ margin-bottom: 0.5rem !important;
208
+ width: 100%;
209
+ }
210
+
211
+ [data-testid="stRadio"] label:hover {
212
+ background: rgba(255, 255, 255, 0.08) !important;
213
+ border-color: rgba(129, 140, 248, 0.3) !important;
214
+ transform: translateX(10px);
215
+ }
216
+
217
+ [data-testid="stRadio"] label p {
218
+ color: var(--text-main) !important;
219
+ font-size: 1.1rem !important;
220
+ font-weight: 500 !important;
221
+ }
222
+
223
+ /* Multiselect Styling */
224
+ .stMultiSelect div[data-baseweb="select"] {
225
+ background: rgba(255, 255, 255, 0.03) !important;
226
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
227
+ border-radius: 16px !important;
228
+ color: white !important;
229
+ }
230
+
231
+ .stMultiSelect span[data-baseweb="tag"] {
232
+ background: var(--primary) !important;
233
+ border-radius: 8px !important;
234
+ }
235
+
236
+ /* Button Styling */
237
+ div.stButton > button {
238
+ background: linear-gradient(135deg, #6366f1 0%, #a855f7 100%) !important;
239
+ color: white !important;
240
+ border: none !important;
241
+ padding: 1rem 2rem !important;
242
+ border-radius: 16px !important;
243
+ font-size: 1.2rem !important;
244
+ font-weight: 700 !important;
245
+ letter-spacing: 0.02em !important;
246
+ transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1) !important;
247
+ box-shadow: 0 10px 20px -5px rgba(99, 102, 241, 0.4) !important;
248
+ text-transform: uppercase;
249
+ }
250
+
251
+ div.stButton > button:hover {
252
+ transform: translateY(-3px) scale(1.02) !important;
253
+ box-shadow: 0 20px 30px -10px rgba(99, 102, 241, 0.6) !important;
254
+ filter: brightness(1.1);
255
+ }
256
+
257
+ div.stButton > button:active {
258
+ transform: translateY(0) scale(0.98) !important;
259
+ }
260
+
261
+ .ai-reasoning {
262
+ background: rgba(129, 140, 248, 0.05);
263
+ border-left: 3px solid #818cf8;
264
+ padding: 1rem;
265
+ margin-top: 1rem;
266
+ border-radius: 0 12px 12px 0;
267
+ font-size: 0.95rem;
268
+ color: var(--text-muted);
269
+ line-height: 1.5;
270
+ }
271
+
272
+ .success-box {
273
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, rgba(20, 184, 166, 0.1) 100%);
274
+ border: 1px solid rgba(34, 197, 94, 0.2);
275
+ padding: 2rem;
276
+ border-radius: 24px;
277
+ text-align: center;
278
+ color: #4ade80;
279
+ font-weight: 700;
280
+ font-size: 1.2rem;
281
+ }
282
+
283
+ /* Animations */
284
+ @keyframes titleReveal {
285
+ from { opacity: 0; transform: translateY(40px) scale(0.95); filter: blur(10px); }
286
+ to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
287
+ }
288
+
289
+ @keyframes slideUp {
290
+ from { opacity: 0; transform: translateY(30px); }
291
+ to { opacity: 1; transform: translateY(0); }
292
+ }
293
+
294
+ @keyframes fadeIn {
295
+ from { opacity: 0; }
296
+ to { opacity: 1; }
297
+ }
298
+
299
+ /* Progress Bar */
300
+ .stProgress > div > div > div > div {
301
+ background: linear-gradient(to right, #6366f1, #a855f7) !important;
302
+ }
303
+
304
+ /* Scrollbar */
305
+ ::-webkit-scrollbar {
306
+ width: 8px;
307
+ }
308
+ ::-webkit-scrollbar-track {
309
+ background: var(--bg-dark);
310
+ }
311
+ ::-webkit-scrollbar-thumb {
312
+ background: #334155;
313
+ border-radius: 10px;
314
+ }
315
+ ::-webkit-scrollbar-thumb:hover {
316
+ background: #475569;
317
+ }
318
+ </style>
319
+
320
+ """, unsafe_allow_html=True)
321
+
322
+ # Quiz Questions
323
+ QUIZ_QUESTIONS = [
324
+ {
325
+ "question": "How would you describe your current mood?",
326
+ "options": ["Happy & Upbeat 😄", "Calm & Relaxed 😌", "Reflective & Thoughtful 🤔", "Energized & Motivated 💪", "Romantic & Dreamy 💕"],
327
+ "key": "mood"
328
+ },
329
+ {
330
+ "question": "What's your ideal Friday night?",
331
+ "options": ["Dancing at a party 🎉", "Netflix and chill 📺", "Deep conversations with friends 💬", "Working out or sports 🏋️", "Candlelit dinner 🕯️"],
332
+ "key": "friday_night"
333
+ },
334
+ {
335
+ "question": "Pick a weather that matches your vibe:",
336
+ "options": ["Sunny & warm ☀️", "Cloudy & cool ☁️", "Rainy & cozy 🌧️", "Storm & intense ⛈️", "Sunset & peaceful 🌅"],
337
+ "key": "weather"
338
+ },
339
+ {
340
+ "question": "What's your energy level right now?",
341
+ "options": ["Super high! ⚡", "Relaxed & steady 🌊", "Low & contemplative 🌙", "Ready to conquer! 🏆", "Soft & gentle 🦋"],
342
+ "key": "energy"
343
+ },
344
+ {
345
+ "question": "Choose a color that speaks to you:",
346
+ "options": ["Bright Yellow 💛", "Ocean Blue 💙", "Deep Purple 💜", "Fiery Red ❤️", "Soft Pink 💗"],
347
+ "key": "color"
348
+ },
349
+ {
350
+ "question": "What kind of lyrics do you prefer?",
351
+ "options": ["Uplifting & positive ✨", "Mellow & smooth 🎵", "Deep & meaningful 📖", "Powerful & inspiring 💥", "Sweet & emotional 💌"],
352
+ "key": "lyrics"
353
+ },
354
+ {
355
+ "question": "If your life was a movie, what genre would it be?",
356
+ "options": ["Comedy 😂", "Indie Drama 🎬", "Psychological Thriller 🧠", "Action Adventure 🎯", "Romance 💑"],
357
+ "key": "movie"
358
+ },
359
+ {
360
+ "question": "What time of day do you feel most alive?",
361
+ "options": ["Morning & fresh ☀️", "Afternoon & steady 🌤️", "Late night & introspective 🌙", "Peak hours & busy 📈", "Golden hour & magical ✨"],
362
+ "key": "time_of_day"
363
+ },
364
+ {
365
+ "question": "Which languages are you familiar with? (Select all that apply)",
366
+ "options": ["English", "Spanish", "French", "German", "Italian", "Portuguese", "Japanese", "Korean", "Chinese","Punjabi", "Hindi","Bengali", "Arabic", "Russian", "Turkish", "Indonesian"],
367
+ "key": "languages",
368
+ "type": "multiselect"
369
+ }
370
+ ]
371
+
372
+ # Initialize session state
373
+ if 'stage' not in st.session_state:
374
+ st.session_state.stage = 'landing'
375
+ if 'quiz_index' not in st.session_state:
376
+ st.session_state.quiz_index = 0
377
+ if 'quiz_answers' not in st.session_state:
378
+ st.session_state.quiz_answers = {}
379
+ if 'playlist' not in st.session_state:
380
+ st.session_state.playlist = []
381
+ if 'spotify_connected' not in st.session_state:
382
+ st.session_state.spotify_connected = False
383
+ if 'spotify_playlist_id' not in st.session_state:
384
+ st.session_state.spotify_playlist_id = None
385
+
386
+ # API Configuration
387
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY")
388
+ SPOTIFY_CLIENT_ID = os.getenv("SPOTIFY_CLIENT_ID")
389
+ SPOTIFY_CLIENT_SECRET = os.getenv("SPOTIFY_CLIENT_SECRET")
390
+ SPOTIFY_REDIRECT_URI = os.getenv("SPOTIFY_REDIRECT_URI")
391
+
392
+ # --- MULTI-USER TOKEN MANAGEMENT ---
393
+ class StreamlitSessionCacheHandler(spotipy.cache_handler.CacheHandler):
394
+ """
395
+ Custom cache handler for Spotify tokens that stores them in Streamlit session state.
396
+ This is essential for web deployment to isolate tokens between different users.
397
+ """
398
+ def __init__(self):
399
+ if 'spotify_token' not in st.session_state:
400
+ st.session_state.spotify_token = None
401
+
402
+ def get_cached_token(self):
403
+ return st.session_state.spotify_token
404
+
405
+ def save_token_to_cache(self, token_info):
406
+ st.session_state.spotify_token = token_info
407
+
408
+ def get_groq_client():
409
+ """Initialize Groq client"""
410
+ if GROQ_API_KEY:
411
+ return Groq(api_key=GROQ_API_KEY)
412
+ return None
413
+
414
+ def get_spotify_oauth():
415
+ """Initialize Spotify OAuth object with session-based cache"""
416
+ scope = "playlist-modify-public playlist-modify-private"
417
+ return SpotifyOAuth(
418
+ client_id=SPOTIFY_CLIENT_ID,
419
+ client_secret=SPOTIFY_CLIENT_SECRET,
420
+ redirect_uri=SPOTIFY_REDIRECT_URI,
421
+ scope=scope,
422
+ cache_handler=StreamlitSessionCacheHandler(),
423
+ open_browser=False
424
+ )
425
+
426
+ def get_spotify_client():
427
+ """Initialize Spotify client using session-based cache"""
428
+ if SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET:
429
+ try:
430
+ sp_oauth = get_spotify_oauth()
431
+
432
+ # Check for cached token in session state
433
+ token_info = sp_oauth.get_cached_token()
434
+
435
+ if token_info:
436
+ # Refresh token if expired
437
+ if sp_oauth.is_token_expired(token_info):
438
+ token_info = sp_oauth.refresh_access_token(token_info['refresh_token'])
439
+
440
+ return spotipy.Spotify(auth=token_info['access_token'])
441
+
442
+ # Check if we got a code from the redirect
443
+ query_params = st.query_params
444
+ if 'code' in query_params:
445
+ code = query_params['code']
446
+ try:
447
+ token_info = sp_oauth.get_access_token(code, as_dict=True, check_cache=False)
448
+ if token_info:
449
+ # Clear the code from URL
450
+ st.query_params.clear()
451
+ return spotipy.Spotify(auth=token_info['access_token'])
452
+ except Exception as e:
453
+ st.error(f"Error getting token: {str(e)}")
454
+ return None
455
+
456
+ return None
457
+
458
+ except Exception as e:
459
+ st.error(f"Spotify authentication error: {str(e)}")
460
+ return None
461
+ return None
462
+
463
+ def generate_playlist_with_groq(quiz_answers: Dict, count: int = 20) -> List[Dict]:
464
+ """Generate playlist using Groq AI based on quiz answers"""
465
+ groq_client = get_groq_client()
466
+
467
+ if not groq_client:
468
+ st.warning("⚠️ Groq API not configured. Using demo mode.")
469
+ return get_demo_playlist()
470
+
471
+ # Create prompt from quiz answers
472
+ languages = quiz_answers.get('languages', ['English'])
473
+ if isinstance(languages, list):
474
+ languages_str = ", ".join(languages)
475
+ else:
476
+ languages_str = str(languages)
477
+
478
+ prompt = f"""Based on the following personality quiz responses, recommend {count} songs that match this person's vibe perfectly.
479
+
480
+ IMPORTANT: The user is familiar with these languages: {languages_str}.
481
+ You MUST ONLY recommend songs in one of these languages. Do not include songs in other languages.
482
+
483
+ Quiz Responses:
484
+ {json.dumps(quiz_answers, indent=2)}
485
+
486
+ Please analyze the responses and recommend songs that match their mood, energy, and preferences. For each song, provide:
487
+ 1. Song title
488
+ 2. Artist name
489
+ 3. Genre
490
+ 4. A brief reason why this song matches their vibe (MAX 10 words)
491
+
492
+ Format your response as a JSON array with this structure:
493
+ [
494
+ {{
495
+ "title": "Song Title",
496
+ "artist": "Artist Name",
497
+ "genre": "Genre",
498
+ "reasoning": "Why this song matches their vibe"
499
+ }}
500
+ ]
501
+
502
+ Only return the JSON array, no additional text."""
503
+
504
+ try:
505
+ with st.spinner("✨ AI is curating your perfect playlist..."):
506
+ response = groq_client.chat.completions.create(
507
+ model="llama-3.3-70b-versatile",
508
+ messages=[
509
+ {"role": "system", "content": "You are a music expert who understands personality and creates perfect playlists. Always respond with valid JSON."},
510
+ {"role": "user", "content": prompt}
511
+ ],
512
+ temperature=0.5,
513
+ max_tokens=6000
514
+ )
515
+
516
+ content = response.choices[0].message.content.strip()
517
+
518
+ # Extract JSON if wrapped in code blocks
519
+ if "```json" in content:
520
+ content = content.split("```json")[1].split("```")[0].strip()
521
+ elif "```" in content:
522
+ content = content.split("```")[1].split("```")[0].strip()
523
+
524
+ try:
525
+ songs = json.loads(content)
526
+ except json.JSONDecodeError as je:
527
+ # If JSON is truncated, try to fix the last entry
528
+ if "Expecting value" in str(je) or "Unterminated string" in str(je):
529
+ # Find the last complete song object
530
+ last_bracket = content.rfind('}')
531
+ if last_bracket != -1:
532
+ fixed_content = content[:last_bracket+1] + ']'
533
+ try:
534
+ songs = json.loads(fixed_content)
535
+ st.warning("⚠️ Some songs were omitted due to length limits.")
536
+ except:
537
+ raise je
538
+ else:
539
+ raise je
540
+ else:
541
+ raise je
542
+
543
+ return songs[:count]
544
+
545
+ except Exception as e:
546
+ print(f"Error generating playlist with AI: {str(e)}")
547
+ st.error(f"Error generating playlist with AI: {str(e)}")
548
+ return get_demo_playlist()
549
+
550
+ def get_demo_playlist() -> List[Dict]:
551
+ """Fallback demo playlist"""
552
+ return [
553
+ {"title": "Blinding Lights", "artist": "The Weeknd", "genre": "Pop", "reasoning": "High energy and upbeat vibe"},
554
+ {"title": "Levitating", "artist": "Dua Lipa", "genre": "Pop", "reasoning": "Perfect for happy moods"},
555
+ {"title": "Good 4 U", "artist": "Olivia Rodrigo", "genre": "Pop", "reasoning": "Energetic and powerful"},
556
+ {"title": "Shivers", "artist": "Ed Sheeran", "genre": "Pop", "reasoning": "Romantic and catchy"},
557
+ {"title": "Heat Waves", "artist": "Glass Animals", "genre": "Indie", "reasoning": "Chill yet engaging"},
558
+ {"title": "Stay", "artist": "The Kid LAROI & Justin Bieber", "genre": "Pop", "reasoning": "Emotional and melodic"},
559
+ {"title": "Peaches", "artist": "Justin Bieber", "genre": "R&B", "reasoning": "Smooth and relaxed"},
560
+ {"title": "Montero", "artist": "Lil Nas X", "genre": "Hip-Hop", "reasoning": "Bold and confident"},
561
+ {"title": "drivers license", "artist": "Olivia Rodrigo", "genre": "Pop", "reasoning": "Deep emotional resonance"},
562
+ {"title": "Save Your Tears", "artist": "The Weeknd", "genre": "Pop", "reasoning": "Uplifting with depth"},
563
+ {"title": "Positions", "artist": "Ariana Grande", "genre": "R&B", "reasoning": "Sweet and romantic"},
564
+ {"title": "Willow", "artist": "Taylor Swift", "genre": "Pop", "reasoning": "Dreamy and enchanting"}
565
+ ]
566
+
567
+ def search_and_add_to_spotify_playlist(songs: List[Dict], playlist_id: str):
568
+ """Search for songs on Spotify and add them to playlist"""
569
+ sp = get_spotify_client()
570
+ if not sp:
571
+ return False
572
+
573
+ track_uris = []
574
+
575
+ with st.spinner("🔍 Finding songs on Spotify..."):
576
+ for song in songs:
577
+ try:
578
+ query = f"{song['title']} {song['artist']}"
579
+ results = sp.search(q=query, type='track', limit=1)
580
+
581
+ if results['tracks']['items']:
582
+ track_uris.append(results['tracks']['items'][0]['uri'])
583
+ time.sleep(0.1) # Rate limiting
584
+
585
+ except Exception as e:
586
+ st.warning(f"Couldn't find '{song['title']}' on Spotify")
587
+ continue
588
+
589
+ # Add tracks to playlist in batches of 100
590
+ try:
591
+ for i in range(0, len(track_uris), 100):
592
+ sp.playlist_add_items(playlist_id, track_uris[i:i+100])
593
+ return True
594
+ except Exception as e:
595
+ st.error(f"Error adding songs to playlist: {str(e)}")
596
+ return False
597
+
598
+ def create_spotify_playlist(playlist_name: str) -> Optional[str]:
599
+ """Create a new Spotify playlist"""
600
+ sp = get_spotify_client()
601
+ if not sp:
602
+ return None
603
+
604
+ try:
605
+ user_id = sp.current_user()['id']
606
+ playlist = sp.user_playlist_create(
607
+ user=user_id,
608
+ name=playlist_name,
609
+ public=True,
610
+ description="Created by VibeSync - AI-powered playlist generator"
611
+ )
612
+ return playlist['id']
613
+ except Exception as e:
614
+ st.error(f"Error creating Spotify playlist: {str(e)}")
615
+ return None
616
+
617
+ def display_song_card(song: Dict, index: int):
618
+ """Display a song in a beautiful card format"""
619
+ reasoning_html = f'<div class="ai-reasoning">“{song.get("reasoning", "Perfectly curated for your vibe.")}”</div>' if song.get("reasoning") else ""
620
+
621
+ st.markdown(f"""
622
+ <div class="song-card">
623
+ <div class="song-index">{index:02d}</div>
624
+ <div style="flex: 1;">
625
+ <div class="song-title">{song['title']}</div>
626
+ <div class="song-artist">{song['artist']}</div>
627
+ <div style="margin-top: 0.8rem; display: flex; align-items: center; gap: 0.5rem;">
628
+ <span class="tag">{song.get('genre', 'Music')}</span>
629
+ </div>
630
+ {reasoning_html}
631
+ </div>
632
+ </div>
633
+ """, unsafe_allow_html=True)
634
+
635
+ # Check Spotify connection
636
+ def check_spotify_auth():
637
+ """Check if Spotify is authenticated"""
638
+ sp = get_spotify_client()
639
+ if sp:
640
+ try:
641
+ user = sp.current_user()
642
+ st.session_state.spotify_connected = True
643
+ return user
644
+ except:
645
+ st.session_state.spotify_connected = False
646
+ return None
647
+ return None
648
+
649
+ # Landing Page
650
+ if st.session_state.stage == 'landing':
651
+ st.markdown('<div class="main-container">', unsafe_allow_html=True)
652
+ st.markdown('<h1 class="hero-title">VibeSync</h1>', unsafe_allow_html=True)
653
+ st.markdown('<p class="hero-subtitle">Your personality, translated into sound.</p>', unsafe_allow_html=True)
654
+
655
+ # Spotify Connection Status
656
+ user = check_spotify_auth()
657
+ if user:
658
+ st.markdown(f'<div style="text-align: center; margin-bottom: 2rem;"><div class="spotify-badge">✓ Connected as {user["display_name"]}</div></div>', unsafe_allow_html=True)
659
+ elif SPOTIFY_CLIENT_ID:
660
+ st.markdown('<div style="text-align: center; margin: 1rem 0;"><p style="color: var(--text-muted);">Connect Spotify to export your magic.</p></div>', unsafe_allow_html=True)
661
+ col1, col2, col3 = st.columns([1, 1, 1])
662
+ with col2:
663
+ if st.button("🎵 Connect Spotify", key="connect_spotify"):
664
+ # Generate auth URL
665
+ sp_oauth = get_spotify_oauth()
666
+ auth_url = sp_oauth.get_authorize_url()
667
+
668
+ st.markdown(f"""
669
+ <div style="background: rgba(255,255,255,0.05); padding: 2rem; border-radius: 24px; margin: 1rem 0; border: 1px solid rgba(255,255,255,0.1); text-align: center;">
670
+ <h4 style="color: white; margin-bottom: 1rem;">Spotify Integration</h4>
671
+ <a href="{auth_url}" target="_blank" style="
672
+ display: inline-block;
673
+ background: #1DB954;
674
+ color: white;
675
+ padding: 1rem 2.5rem;
676
+ border-radius: 100px;
677
+ text-decoration: none;
678
+ font-weight: 700;
679
+ margin: 1rem 0;
680
+ transition: 0.3s;
681
+ ">Login with Spotify</a>
682
+ </div>
683
+ """, unsafe_allow_html=True)
684
+
685
+ st.markdown("""
686
+ <div style="margin: 3rem 0; padding: 2rem; background: rgba(255,255,255,0.02); border-radius: 24px; border: 1px solid rgba(255,255,255,0.05);">
687
+ <div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 2rem; text-align: center;">
688
+ <div>
689
+ <div style="font-size: 2rem; margin-bottom: 0.5rem;">🧠</div>
690
+ <div style="color: white; font-weight: 600;">AI Analysis</div>
691
+ <div style="color: var(--text-muted); font-size: 0.9rem;">Deep mood mapping</div>
692
+ </div>
693
+ <div>
694
+ <div style="font-size: 2rem; margin-bottom: 0.5rem;">✨</div>
695
+ <div style="color: white; font-weight: 600;">Curated Vibes</div>
696
+ <div style="color: var(--text-muted); font-size: 0.9rem;">20+ perfect tracks</div>
697
+ </div>
698
+ <div>
699
+ <div style="font-size: 2rem; margin-bottom: 0.5rem;">🎧</div>
700
+ <div style="color: white; font-weight: 600;">Instant Export</div>
701
+ <div style="color: var(--text-muted); font-size: 0.9rem;">Direct to Spotify</div>
702
+ </div>
703
+ </div>
704
+ </div>
705
+ """, unsafe_allow_html=True)
706
+
707
+ col1, col2, col3 = st.columns([1, 2, 1])
708
+ with col2:
709
+ if st.button("🚀 Start Your Journey", key="start_quiz"):
710
+ st.session_state.stage = 'quiz'
711
+ st.rerun()
712
+ st.markdown('</div>', unsafe_allow_html=True)
713
+
714
+ # Quiz Stage
715
+ elif st.session_state.stage == 'quiz':
716
+ current_q = QUIZ_QUESTIONS[st.session_state.quiz_index]
717
+
718
+ st.markdown('<div class="main-container">', unsafe_allow_html=True)
719
+ st.markdown('<h1 class="hero-title">VibeSync Quiz</h1>', unsafe_allow_html=True)
720
+ st.markdown(f'<p style="text-align: center; color: var(--text-muted); margin-bottom: 2rem;">Question {st.session_state.quiz_index + 1} of {len(QUIZ_QUESTIONS)}</p>', unsafe_allow_html=True)
721
+
722
+ # Progress bar
723
+ progress = (st.session_state.quiz_index) / len(QUIZ_QUESTIONS)
724
+ st.progress(progress)
725
+
726
+ st.markdown('<div class="question-card">', unsafe_allow_html=True)
727
+ st.markdown(f'<p class="question-text">{current_q["question"]}</p>', unsafe_allow_html=True)
728
+
729
+ if current_q.get("type") == "multiselect":
730
+ answer = st.multiselect("Choose languages:", current_q["options"], key=f"q_{st.session_state.quiz_index}")
731
+ else:
732
+ answer = st.radio("Choose one:", current_q["options"], key=f"q_{st.session_state.quiz_index}", label_visibility="collapsed")
733
+
734
+ col1, col2, col3 = st.columns([1, 2, 1])
735
+ with col2:
736
+ if st.button("Next Question ➡️", key=f"next_{st.session_state.quiz_index}"):
737
+ # Save answer
738
+ st.session_state.quiz_answers[current_q["key"]] = answer
739
+
740
+ if st.session_state.quiz_index < len(QUIZ_QUESTIONS) - 1:
741
+ st.session_state.quiz_index += 1
742
+ st.rerun()
743
+ else:
744
+ # Quiz completed, generate playlist with AI
745
+ st.session_state.playlist = generate_playlist_with_groq(st.session_state.quiz_answers)
746
+ st.session_state.stage = 'results'
747
+ st.rerun()
748
+
749
+ st.markdown('</div></div>', unsafe_allow_html=True)
750
+
751
+ # Results Stage
752
+ elif st.session_state.stage == 'results':
753
+ st.markdown('<div class="main-container">', unsafe_allow_html=True)
754
+ st.markdown('<h1 class="hero-title">Your Sonic Identity</h1>', unsafe_allow_html=True)
755
+ st.markdown(f"<p style='text-align: center; color: var(--text-muted); margin-bottom: 3rem;'>A {len(st.session_state.playlist)}-track journey curated by AI.</p>", unsafe_allow_html=True)
756
+
757
+ # Display playlist
758
+ for idx, song in enumerate(st.session_state.playlist, 1):
759
+ display_song_card(song, idx)
760
+
761
+ # Add more songs with AI
762
+ st.markdown("<br><br>", unsafe_allow_html=True)
763
+ st.markdown("<h3 style='color: #818cf8; text-align: center; font-family: \"Space Grotesk\", sans-serif;'>Curious for more? 🎵</h3>", unsafe_allow_html=True)
764
+
765
+ user_request = st.text_input("Describe what kind of songs you'd like to add:", placeholder="e.g., more upbeat songs, slower tempo, different genre...")
766
+
767
+ if st.button("✨ Get AI Suggestions"):
768
+ if user_request:
769
+ groq_client = get_groq_client()
770
+ if groq_client:
771
+ prompt = f"""Based on the user's request and their current playlist, suggest 5 additional songs.
772
+
773
+ User's request: {user_request}
774
+
775
+ Current playlist context:
776
+ {json.dumps(st.session_state.playlist[:3], indent=2)}
777
+
778
+ Recommend 5 songs that match their request. Format as JSON array:
779
+ [
780
+ {{
781
+ "title": "Song Title",
782
+ "artist": "Artist Name",
783
+ "genre": "Genre",
784
+ "reasoning": "Why this matches their request (MAX 10 words)"
785
+ }}
786
+ ]"""
787
+
788
+ try:
789
+ with st.spinner("✨ AI is finding more gems..."):
790
+ response = groq_client.chat.completions.create(
791
+ model="llama-3.3-70b-versatile",
792
+ messages=[
793
+ {"role": "system", "content": "You are a music expert. Always respond with valid JSON."},
794
+ {"role": "user", "content": prompt}
795
+ ],
796
+ temperature=0.8,
797
+ max_tokens=1000
798
+ )
799
+
800
+ content = response.choices[0].message.content.strip()
801
+ if "```json" in content:
802
+ content = content.split("```json")[1].split("```")[0].strip()
803
+ elif "```" in content:
804
+ content = content.split("```")[1].split("```")[0].strip()
805
+
806
+ new_songs = json.loads(content)
807
+ st.session_state.temp_suggestions = new_songs
808
+
809
+ except Exception as e:
810
+ st.error(f"Error getting AI suggestions: {str(e)}")
811
+ else:
812
+ st.warning("⚠️ Groq API not configured")
813
+
814
+ # Display suggestions if they exist
815
+ if 'temp_suggestions' in st.session_state:
816
+ st.markdown("<h4 style='color: #818cf8; margin-top: 2rem; font-family: \"Space Grotesk\", sans-serif;'>AI Suggestions:</h4>", unsafe_allow_html=True)
817
+ for idx, song in enumerate(st.session_state.temp_suggestions, 1):
818
+ col1, col2 = st.columns([5, 1])
819
+ with col1:
820
+ st.markdown(f"""
821
+ <div class="song-card" style="margin: 0.5rem 0; padding: 1rem;">
822
+ <div style="flex: 1;">
823
+ <div class="song-title" style="font-size: 1.1rem;">{song['title']}</div>
824
+ <div class="song-artist" style="font-size: 0.9rem;">{song['artist']}</div>
825
+ <div class="ai-reasoning" style="font-size: 0.8rem; padding: 0.5rem;">“{song.get('reasoning', 'Matches your request.')}”</div>
826
+ </div>
827
+ </div>
828
+ """, unsafe_allow_html=True)
829
+ with col2:
830
+ st.markdown("<div style='height: 20px;'></div>", unsafe_allow_html=True)
831
+ if st.button("➕", key=f"add_{idx}_{song['title'][:10]}"):
832
+ st.session_state.playlist.append(song)
833
+
834
+ # Add to Spotify if connected
835
+ if st.session_state.spotify_playlist_id:
836
+ sp = get_spotify_client()
837
+ if sp:
838
+ try:
839
+ query = f"{song['title']} {song['artist']}"
840
+ results = sp.search(q=query, type='track', limit=1)
841
+ if results['tracks']['items']:
842
+ sp.playlist_add_items(
843
+ st.session_state.spotify_playlist_id,
844
+ [results['tracks']['items'][0]['uri']]
845
+ )
846
+ st.toast(f"✅ Added {song['title']} to Spotify!")
847
+ except:
848
+ pass
849
+
850
+ # Remove from suggestions after adding
851
+ st.session_state.temp_suggestions.pop(idx-1)
852
+ if not st.session_state.temp_suggestions:
853
+ del st.session_state.temp_suggestions
854
+ st.rerun()
855
+
856
+ # Export to Spotify
857
+ st.markdown("<br><br>", unsafe_allow_html=True)
858
+
859
+ if st.session_state.spotify_connected and not st.session_state.spotify_playlist_id:
860
+ st.markdown("""
861
+ <div class="success-box">
862
+ <h3 style="margin: 0; color: #4ade80;">🎧 Ready to export to Spotify!</h3>
863
+ </div>
864
+ """, unsafe_allow_html=True)
865
+
866
+ col1, col2, col3 = st.columns([1, 2, 1])
867
+ with col2:
868
+ playlist_name = st.text_input("Playlist Name:", value="My VibeSync Playlist")
869
+ if st.button("📤 Create Spotify Playlist"):
870
+ playlist_id = create_spotify_playlist(playlist_name)
871
+ if playlist_id:
872
+ if search_and_add_to_spotify_playlist(st.session_state.playlist, playlist_id):
873
+ st.session_state.spotify_playlist_id = playlist_id
874
+ st.success("✅ Playlist created and songs added to your Spotify account!")
875
+ st.balloons()
876
+ st.rerun()
877
+
878
+ elif st.session_state.spotify_playlist_id:
879
+ st.markdown("""
880
+ <div class="success-box">
881
+ <h3 style="margin: 0; color: #4ade80;">✅ Playlist exported to Spotify!</h3>
882
+ <p style="margin: 0.5rem 0 0 0; color: var(--text-muted);">Check your Spotify account</p>
883
+ </div>
884
+ """, unsafe_allow_html=True)
885
+
886
+ # Action buttons
887
+ st.markdown("<br><br>", unsafe_allow_html=True)
888
+ col1, col2 = st.columns(2)
889
+ with col1:
890
+ if st.button("🔄 Start Over"):
891
+ st.session_state.stage = 'landing'
892
+ st.session_state.quiz_index = 0
893
+ st.session_state.quiz_answers = {}
894
+ st.session_state.playlist = []
895
+ st.session_state.spotify_playlist_id = None
896
+ if 'temp_suggestions' in st.session_state:
897
+ del st.session_state.temp_suggestions
898
+ st.rerun()
899
+ with col2:
900
+ if st.button("✅ I'm Happy with My Playlist"):
901
+ st.balloons()
902
+ st.success(f"🎉 Awesome! Your playlist of {len(st.session_state.playlist)} songs is ready!")
903
+ st.markdown('</div>', unsafe_allow_html=True)