Skip to content

Commit bccf063

Browse files
committed
refactor urMusicSection.js to enhance user interaction, optimize Gemini API integration, and improve music recommendation display
1 parent 53179cf commit bccf063

1 file changed

Lines changed: 242 additions & 67 deletions

File tree

app/urMusicSection.js

Lines changed: 242 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Optimized urMusicSection with working Gemini integration
2+
// Copy this to replace the existing urMusicSection.js
3+
14
function 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
198212
function 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(/```json\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
237363
function 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

Comments
 (0)