1+ // Optimized urMusicSection with working Gemini integration
2+ // Copy this to replace the existing urMusicSection.js
3+
14function urMusicSection ( ) {
25 const section = document . createElement ( "section" ) ;
36 section . classList . add ( "urmusic-section" , "py-5" ) ;
@@ -7,46 +10,23 @@ function urMusicSection() {
710 <div class="col-lg-8">
811 <div class="text-center mb-4">
912 <h2 class="display-3 text-white">urMUSIC <span class="text-accent">AI</span></h2>
10- <p class="text-white lead">Ceritain mood kamu, sistem akan memproses rekomendasi. </p>
13+ <p class="text-white lead">Ceritain mood kamu biar aku tau lagu yang pas buat kamu. </p
1114 </div>
1215
1316 <div class="form-container-neo">
14- <div class="form-header">
15- <i class="fas fa-terminal me-2"></i> INPUT_PARAMETERS
16- </div>
17+
1718 <div class="p-4">
1819 <form id="moodForm">
1920 <div class="mb-4">
20- <label class="label-neo">1. CURRENT MOOD:</label>
21- <div class="d-flex flex-wrap gap-2 mt-2">
22- ${ createRadio ( 'happy' , '😊 HAPPY' ) }
23- ${ createRadio ( 'sad' , '😔 SAD' ) }
24- ${ createRadio ( 'energetic' , '⚡ ENERGY' ) }
25- ${ createRadio ( 'chill' , '😌 CHILL' ) }
26- ${ createRadio ( 'focused' , '🎯 FOCUS' ) }
27- </div>
28- </div>
29-
30- <div class="mb-4">
31- <label class="label-neo">2. ACTIVITY:</label>
32- <select class="form-select input-neo" id="activity" name="activity">
33- <option value="">-- SELECT ACTIVITY --</option>
34- <option value="studying">Studying / Nugas</option>
35- <option value="coding">Coding</option>
36- <option value="working">Working</option>
37- <option value="relaxing">Rebahan</option>
38- <option value="gaming">Gaming</option>
39- </select>
40- </div>
41-
42- <div class="mb-4">
43- <label class="label-neo">3. CONTEXT (OPTIONAL):</label>
44- <textarea class="form-control input-neo" id="description" rows="2"
45- placeholder="Misal: Lagi error kodingan 100 baris..."></textarea>
21+ <label class="label-neo">DESCRIBE YOUR MOOD </label>
22+ <textarea class="form-control input-neo" id="moodDescription" name="moodDescription" rows="4"
23+ placeholder="ceritain ajaa gapapa.."
24+ required></textarea>
25+ <small class="text-muted mt-2 d-block">💡 Tip: Ceritakan mood, aktivitas, dan preferensi musik kamu secara bebas</small>
4626 </div>
4727
4828 <button type="submit" class="btn btn-generate w-100">
49- INITIALIZE RECOMMENDATION <i class="fas fa-cogs ms-2"></i>
29+ GENERATE MUSIC RECOMMENDATIONS <i class="fas fa-music ms-2"></i>
5030 </button>
5131 </form>
5232 </div>
@@ -58,7 +38,7 @@ function urMusicSection() {
5838 <div class="row justify-content-center">
5939 <div class="col-lg-8">
6040 <div class="result-box">
61- <h3 class="text-center mb-4 bg-black text-white py-2">/// OUTPUT GENERATED</h3>
41+
6242 <div id="recommendationContent"></div>
6343 <div class="text-center mt-4">
6444 <button class="btn btn-outline-black" onclick="document.getElementById('moodForm').scrollIntoView()">
@@ -151,6 +131,11 @@ function urMusicSection() {
151131 border-color: var(--neo-black);
152132 box-shadow: 5px 5px 0px var(--neo-black);
153133 }
134+
135+ .btn-generate:disabled {
136+ opacity: 0.7;
137+ cursor: not-allowed;
138+ }
154139
155140 /* Result Styles */
156141 .result-box {
@@ -170,6 +155,25 @@ function urMusicSection() {
170155 align-items: center;
171156 }
172157
158+ .btn-spotify {
159+ background: #1db954;
160+ color: white;
161+ border: 2px solid var(--neo-black);
162+ font-weight: bold;
163+ padding: 8px 16px;
164+ text-decoration: none;
165+ border-radius: 0;
166+ box-shadow: 3px 3px 0px var(--neo-black);
167+ transition: all 0.2s;
168+ }
169+
170+ .btn-spotify:hover {
171+ color: white;
172+ background: #1ed760;
173+ transform: translate(-2px, -2px);
174+ box-shadow: 5px 5px 0px var(--neo-black);
175+ }
176+
173177 .btn-outline-black {
174178 border: 3px solid var(--neo-black);
175179 font-weight: bold;
@@ -180,10 +184,20 @@ function urMusicSection() {
180184 background: var(--neo-black);
181185 color: var(--neo-white);
182186 }
187+
188+ .loading-indicator {
189+ display: inline-block;
190+ animation: spin 1s linear infinite;
191+ }
192+
193+ @keyframes spin {
194+ 0% { transform: rotate(0deg); }
195+ 100% { transform: rotate(360deg); }
196+ }
183197 </style>
184198 ` ;
185199
186- // Attach Event Listener Logic (Keep existing logic but wrapped safely)
200+ // Attach Event Listener
187201 setTimeout ( ( ) => {
188202 const form = document . getElementById ( "moodForm" ) ;
189203 if ( form ) {
@@ -194,62 +208,197 @@ function urMusicSection() {
194208 return section ;
195209}
196210
197- // Helper functions for template
211+ // Helper function for radio buttons
198212function createRadio ( value , label ) {
199- return `
213+ return `
200214 <input type="radio" class="btn-check" name="mood" id="${ value } " value="${ value } ">
201215 <label class="btn-radio" for="${ value } ">${ label } </label>
202- ` ;
216+ ` ;
203217}
204218
205- // --- KEEPING ORIGINAL LOGIC FUNCTIONS ---
206- function handleMoodSubmit ( e ) {
219+ // Enhanced form submission handler
220+ async function handleMoodSubmit ( e ) {
207221 e . preventDefault ( ) ;
208222 const formData = new FormData ( e . target ) ;
209- const mood = formData . get ( "mood" ) ;
210- const activity = formData . get ( "activity" ) ;
211- const description = formData . get ( "description" ) ;
223+ const moodDescription = formData . get ( "moodDescription" ) ;
212224
213- if ( ! mood ) {
214- alert ( "⚠️ ERROR: PARAMETER 'MOOD' MISSING." ) ;
225+ if ( ! moodDescription || moodDescription . trim ( ) . length < 10 ) {
226+ showErrorMessage (
227+ "Mohon ceritakan mood dan situasi kamu lebih detail (minimal 10 karakter)"
228+ ) ;
215229 return ;
216230 }
217231
218- const recommendation = generateMusicRecommendation ( mood , activity , description ) ;
219- displayRecommendation ( recommendation ) ;
232+ // Enhanced loading state
233+ const submitBtn = e . target . querySelector ( 'button[type="submit"]' ) ;
234+ const originalText = submitBtn . innerHTML ;
235+ submitBtn . innerHTML =
236+ 'AI SEDANG BERPIKIR... <i class="loading-indicator fas fa-spinner ms-2"></i>' ;
237+ submitBtn . disabled = true ;
238+
239+ try {
240+ const recommendation = await generateMusicRecommendation ( moodDescription ) ;
241+ displayRecommendation ( recommendation ) ;
242+ } catch ( error ) {
243+ showErrorMessage (
244+ "🤖 AI sedang tidak terhubung. Coba lagi dalam beberapa saat ya!"
245+ ) ;
246+ } finally {
247+ // Restore button state
248+ submitBtn . innerHTML = originalText ;
249+ submitBtn . disabled = false ;
250+ }
220251}
221252
222- function generateMusicRecommendation ( mood , activity , description ) {
223- // ... (Logika data lagu sama persis dengan file asli, disingkat biar fit) ...
224- // Anggaplah data lagu di sini masih sama dengan original file
225- const recommendations = {
226- happy : [ { title : "Happy" , artist : "Pharrell" , reason : "Classic mood booster." } , { title : "Uptown Funk" , artist : "Bruno Mars" , reason : "Funky beats." } ] ,
227- sad : [ { title : "Glimpse of Us" , artist : "Joji" , reason : "Deep feelings." } , { title : "Fix You" , artist : "Coldplay" , reason : "Healing vibes." } ] ,
228- energetic : [ { title : "Centuries" , artist : "Fall Out Boy" , reason : "Power anthem." } , { title : "Eye of the Tiger" , artist : "Survivor" , reason : "Gym vibes." } ] ,
229- chill : [ { title : "Location" , artist : "Khalid" , reason : "Smooth vibes." } , { title : "Weightless" , artist : "Marconi Union" , reason : "Pure relax." } ] ,
230- focused : [ { title : "Lofi Study" , artist : "Lofi Girl" , reason : "Concentration." } , { title : "Interstellar" , artist : "Hans Zimmer" , reason : "Deep focus." } ]
231- } ;
232-
233- let final = recommendations [ mood ] || recommendations . happy ;
234- return { mood, activity, songs : final } ;
253+ // Optimized Gemini API integration
254+ async function generateMusicRecommendation ( moodDescription ) {
255+ const API_KEY = "AIzaSyA3Acf5VYEFtWxVq_8KAhtGDlUibernA9M" ;
256+ const API_URL =
257+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent" ;
258+
259+ // Build optimized prompt
260+ const prompt = `Berdasarkan deskripsi mood dan situasi berikut: "${ moodDescription } "
261+
262+ Berikan 3-5 rekomendasi lagu yang cocok dalam format JSON yang valid:
263+ {
264+ "recommendations": [
265+ {
266+ "title": "Judul Lagu",
267+ "artist": "Nama Artist",
268+ "reason": "Alasan singkat kenapa cocok (max 60 karakter)",
269+ "genre": "Genre musik",
270+ "spotify_search": "judul lagu artist"
271+ }
272+ ]
273+ }
274+
275+ Pastikan lagu-lagu populer dan mudah ditemukan di Spotify. Untuk field spotify_search, gunakan format yang mudah dicari di Spotify.` ;
276+
277+ try {
278+ const response = await fetch ( `${ API_URL } ?key=${ API_KEY } ` , {
279+ method : "POST" ,
280+ headers : {
281+ "Content-Type" : "application/json" ,
282+ } ,
283+ body : JSON . stringify ( {
284+ contents : [
285+ {
286+ parts : [
287+ {
288+ text : prompt ,
289+ } ,
290+ ] ,
291+ } ,
292+ ] ,
293+ generationConfig : {
294+ temperature : 0.7 ,
295+ topK : 40 ,
296+ topP : 0.95 ,
297+ maxOutputTokens : 2048 ,
298+ } ,
299+ } ) ,
300+ } ) ;
301+
302+ if ( ! response . ok ) {
303+ throw new Error ( `API Error: ${ response . status } ` ) ;
304+ }
305+
306+ const data = await response . json ( ) ;
307+
308+ // Robust response validation
309+ if (
310+ ! data . candidates ||
311+ ! data . candidates [ 0 ] ||
312+ ! data . candidates [ 0 ] . content ||
313+ ! data . candidates [ 0 ] . content . parts
314+ ) {
315+ throw new Error ( "Invalid response structure from Gemini" ) ;
316+ }
317+
318+ const generatedText = data . candidates [ 0 ] . content . parts [ 0 ] . text ;
319+
320+ // Enhanced JSON parsing
321+ let parsedResponse ;
322+ try {
323+ // Remove markdown code blocks if present
324+ const cleanedText = generatedText
325+ . replace ( / ` ` ` j s o n \n ? | \n ? ` ` ` / g, "" )
326+ . trim ( ) ;
327+
328+ // Try direct parsing first
329+ try {
330+ parsedResponse = JSON . parse ( cleanedText ) ;
331+ } catch {
332+ // Fallback: extract JSON with regex
333+ const jsonMatch = cleanedText . match ( / \{ [ \s \S ] * \} / ) ;
334+ if ( jsonMatch ) {
335+ parsedResponse = JSON . parse ( jsonMatch [ 0 ] ) ;
336+ } else {
337+ throw new Error ( "No valid JSON found" ) ;
338+ }
339+ }
340+
341+ // Validate structure
342+ if (
343+ ! parsedResponse . recommendations ||
344+ ! Array . isArray ( parsedResponse . recommendations )
345+ ) {
346+ throw new Error ( "Invalid recommendations structure" ) ;
347+ }
348+ } catch ( parseError ) {
349+ throw new Error ( "Gagal memproses respon AI" ) ;
350+ }
351+
352+ return {
353+ moodDescription,
354+ songs : parsedResponse . recommendations ,
355+ source : "gemini" ,
356+ } ;
357+ } catch ( error ) {
358+ throw error ;
359+ }
235360}
236361
362+ // Enhanced display function
237363function displayRecommendation ( rec ) {
238364 const resultDiv = document . getElementById ( "recommendationResult" ) ;
239365 const contentDiv = document . getElementById ( "recommendationContent" ) ;
240366
241- let html = `<p class="lead fw-bold mb-4">DETECTED MOOD: ${ rec . mood . toUpperCase ( ) } // ACTIVITY: ${ rec . activity ? rec . activity . toUpperCase ( ) : 'N/A' } </p>` ;
367+ let html = `
368+ <div class="d-flex justify-content-between align-items-center mb-4">
369+ <div>
370+ <p class="lead fw-bold mb-1">REKOMENDASI MUSIK UNTUK KAMU</p>
371+ <p class="mb-0 text-muted small">"${ rec . moodDescription . substring (
372+ 0 ,
373+ 100
374+ ) } ${ rec . moodDescription . length > 100 ? "..." : "" } "</p>
375+ </div>
376+ </div>
377+ ` ;
242378
243379 rec . songs . forEach ( ( song , idx ) => {
380+ const spotifyQuery = song . spotify_search || `${ song . title } ${ song . artist } ` ;
381+ const spotifyUrl = `https://open.spotify.com/search/${ encodeURIComponent (
382+ spotifyQuery
383+ ) } `;
384+
244385 html += `
245- <div class="song-item-neo">
246- <div>
247- <div class="fw-bold h5 mb-0">#${ idx + 1 } ${ song . title } </div>
248- <div class="small text-muted">${ song . artist } </div>
249- <div class="small mt-1 fst-italic">"${ song . reason } "</div>
250- </div>
251- <i class="fas fa-play-circle fa-2x"></i>
386+ <div class="song-item-neo">
387+ <div class="flex-grow-1">
388+ <div class="fw-bold h5 mb-1">#${ idx + 1 } ${ song . title } </div>
389+ <div class="small text-muted mb-1">oleh ${ song . artist } </div>
390+ <div class="small fst-italic text-success mb-1">"${ song . reason } "</div>
391+ ${
392+ song . genre ? `<span class="badge bg-dark">${ song . genre } </span>` : ""
393+ }
252394 </div>
395+ <div class="text-end ms-3">
396+ <a href="${ spotifyUrl } " target="_blank" class="btn btn-spotify">
397+ <i class="fab fa-spotify me-1"></i>
398+ Spotify
399+ </a>
400+ </div>
401+ </div>
253402 ` ;
254403 } ) ;
255404
@@ -258,4 +407,30 @@ function displayRecommendation(rec) {
258407 resultDiv . scrollIntoView ( { behavior : "smooth" } ) ;
259408}
260409
261- export default urMusicSection ;
410+ // Error message display function
411+ function showErrorMessage ( message ) {
412+ const resultDiv = document . getElementById ( "recommendationResult" ) ;
413+ const contentDiv = document . getElementById ( "recommendationContent" ) ;
414+
415+ contentDiv . innerHTML = `
416+ <div class="text-center py-5">
417+ <div class="mb-3">
418+ <i class="fas fa-robot fa-3x text-muted"></i>
419+ </div>
420+ <h4 class="text-muted">${ message } </h4>
421+ <p class="text-muted small mt-3">Pastikan koneksi internet stabil dan coba lagi</p>
422+ </div>
423+ ` ;
424+
425+ resultDiv . style . display = "block" ;
426+ resultDiv . scrollIntoView ( { behavior : "smooth" } ) ;
427+ }
428+
429+ // Bonus: Music search function (kept for backward compatibility but not used in UI)
430+ function searchMusic ( title , artist ) {
431+ const query = encodeURIComponent ( `${ title } ${ artist } ` ) ;
432+ const spotifyUrl = `https://open.spotify.com/search/${ query } ` ;
433+ window . open ( spotifyUrl , "_blank" ) ;
434+ }
435+
436+ export default urMusicSection ;
0 commit comments