Spaces:
Sleeping
Sleeping
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title> | |
| μμ μΆμ² μ±λ΄ | |
| </title> | |
| <!-- Tailwind CSS λ‘λ --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Google Fonts (Inter) λ‘λ --> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* κΈ°λ³Έ ν°νΈ λ° μ€ν¬λ‘€λ° μ€νμΌλ§ */ | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| /* 컀μ€ν μ€ν¬λ‘€λ° (μ ν μ¬ν) */ | |
| ::-webkit-scrollbar { width: 8px; } | |
| ::-webkit-scrollbar-track { background: #1e293b; } | |
| ::-webkit-scrollbar-thumb { background: #4f46e5; border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #6366f1; } | |
| /* β β β β β μΆκ°λ λΆλΆ: 컀μ€ν μ¬λΌμ΄λ μ€νμΌ β β β β β */ | |
| /* Webkit κ³μ΄ λΈλΌμ°μ (Chrome, Safari) */ | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| background: #ffffff; | |
| border: 2px solid #4f46e5; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| margin-top: -7px; /* thumb μμΉ λ³΄μ */ | |
| } | |
| /* Firefox λΈλΌμ°μ */ | |
| input[type=range]::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| background: #ffffff; | |
| border: 2px solid #4f46e5; | |
| border-radius: 50%; | |
| cursor: pointer; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-900 text-white antialiased"> | |
| <!-- μ 체 λ μ΄μμ 컨ν μ΄λ --> | |
| <div class="flex flex-col items-center justify-center min-h-screen p-4 bg-gradient-to-br from-slate-900 via-slate-900 to-indigo-900/30"> | |
| <nav class="absolute top-4 right-4 flex gap-4"> | |
| <a href="/" class="bg-indigo-500 text-white font-semibold py-2 px-4 rounded-lg">μμ μΆμ²</a> | |
| <a href="/flower" class="bg-slate-700 hover:bg-slate-600 text-slate-300 font-semibold py-2 px-4 rounded-lg">κ½ μΆμ²</a> | |
| </nav> | |
| <!-- λ©μΈ μΉ΄λ --> | |
| <main class="w-full max-w-2xl bg-slate-800/50 backdrop-blur-sm border border-slate-700 rounded-2xl shadow-2xl shadow-indigo-500/10 p-8 space-y-8"> | |
| <!-- ν€λ --> | |
| <div class="text-center"> | |
| <div class="flex items-center justify-center gap-3 mb-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-indigo-400" viewBox="0 0 20 20" fill="currentColor"> | |
| <path d="M18 3a1 1 0 00-1.196-.98l-10 2A1 1 0 006 5v9.114A4.369 4.369 0 005 14c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3V7.82l8-1.6v5.894A4.37 4.37 0 0015 12c-1.657 0-3 1.343-3 3s1.343 3 3 3 3-1.343 3-3V4a1 1 0 00-1-1z" /> | |
| </svg> | |
| <h1 class="text-3xl font-bold text-slate-100">μμ μΆμ² μ±λ΄</h1> | |
| </div> | |
| <p class="text-slate-400">λΉμ μ μ§κΈ κΈ°λΆμ΄λ μν©μ μλ €μ£Όμλ©΄, μ΄μΈλ¦¬λ λ Έλλ₯Ό μ°Ύμλ릴κ²μ.</p> | |
| </div> | |
| <!-- μ λ ₯ μΉμ --> | |
| <div class="space-y-4"> | |
| <textarea id="userInput" rows="4" class="w-full bg-slate-900 border border-slate-700 rounded-lg p-4 text-slate-300 placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition duration-200 resize-none" placeholder="μ) μ€λ ν€μ΄μ Έμ λ무 μ¬νλ°, λΉκΉμ§ μ€λ€..."></textarea> | |
| <!-- β β β β β μΆκ°λ λΆλΆ: κ°μ€μΉ μ‘°μ μ¬λΌμ΄λ β β β β β --> | |
| <div class="space-y-3 pt-2"> | |
| <label class="text-sm font-medium text-slate-400">μΆμ² κ°μ€μΉ μ‘°μ </label> | |
| <div class="flex items-center gap-4"> | |
| <span id="emotionLabel" class="font-mono text-sm text-indigo-300 w-20 text-center">κ°μ± 40%</span> | |
| <input id="weightSlider" type="range" min="0" max="100" value="60" class="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-indigo-500"> | |
| <span id="contextLabel" class="font-mono text-sm text-indigo-300 w-20 text-center">λ¬Έλ§₯ 60%</span> | |
| </div> | |
| </div> | |
| <!-- β β β β β μΆκ° λ β β β β β --> | |
| <button onclick="getRecommendation()" id="recommendButton" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-3 px-4 rounded-lg transition duration-200 flex items-center justify-center gap-2 disabled:bg-slate-500 disabled:cursor-not-allowed"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> | |
| <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" /> | |
| </svg> | |
| <span>μμ μΆμ²λ°κΈ°</span> | |
| </button> | |
| </div> | |
| <!-- λ‘λ© μΈλμΌμ΄ν° (μ΄κΈ°μλ μ¨κΉ) --> | |
| <div id="loadingIndicator" class="hidden flex flex-col items-center justify-center text-center py-8 space-y-3"> | |
| <svg class="animate-spin h-8 w-8 text-indigo-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"> | |
| <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> | |
| <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> | |
| </svg> | |
| <p class="text-slate-400">λΉμ μ κ°μ μ λΆμνκ³ μμ΄μ...</p> | |
| </div> | |
| <!-- κ²°κ³Ό νμ μΉμ --> | |
| <div id="results" class="space-y-6"> | |
| <!-- κ²°κ³Όκ° μ¬κΈ°μ λμ μΌλ‘ μΆκ°λ©λλ€. --> | |
| </div> | |
| </main> | |
| <!-- νΈν° --> | |
| <footer class="mt-8 text-center text-slate-500 text-sm"> | |
| <p>Powered by AI. Created with Hugging Face & Flask.</p> | |
| </footer> | |
| </div> | |
| <script> | |
| const userInput = document.getElementById('userInput'); | |
| const recommendButton = document.getElementById('recommendButton'); | |
| const loadingIndicator = document.getElementById('loadingIndicator'); | |
| const resultsDiv = document.getElementById('results'); | |
| // β β β β β μΆκ°λ λΆλΆ: μ¬λΌμ΄λ μμ κ°μ Έμ€κΈ° β β β β β | |
| const weightSlider = document.getElementById('weightSlider'); | |
| const emotionLabel = document.getElementById('emotionLabel'); | |
| const contextLabel = document.getElementById('contextLabel'); | |
| // β β β β β μμ λ λΆλΆ: μ¬λΌμ΄λ λΌλ²¨ λ° κ·ΈλΌλ°μ΄μ μ λ°μ΄νΈ ν¨μ β β β β β | |
| function updateSliderAppearance() { | |
| // μ΄μ μ¬λΌμ΄λμ valueλ 'κ°μ±'μ λΉμ¨μ μ§μ λνλ λλ€. | |
| const emotionWeightPercent = parseInt(weightSlider.value); | |
| const contextWeightPercent = 100 - emotionWeightPercent; | |
| emotionLabel.textContent = `κ°μ± ${emotionWeightPercent}%`; | |
| contextLabel.textContent = `λ¬Έλ§₯ ${contextWeightPercent}%`; | |
| emotionLabel.style.opacity = 0.5 + (emotionWeightPercent / 100) * 0.5; | |
| contextLabel.style.opacity = 0.5 + (contextWeightPercent / 100) * 0.5; | |
| const roseColor = '#fb7185'; // rose-400 | |
| const indigoColor = '#818cf8'; // indigo-400 | |
| // κ·ΈλΌλ°μ΄μ μ κ²½κ³μ (emotionWeightPercent)μ΄ μ΄μ 컀μμ μμΉ(value)μ μΌμΉν©λλ€. | |
| const gradient = `linear-gradient(to right, ${roseColor} ${emotionWeightPercent}%, ${indigoColor} ${emotionWeightPercent}%)`; | |
| weightSlider.style.background = gradient; | |
| // 컀μμ ν λ리 μμλ νμ¬ μμΉμ μμκ³Ό μΌμΉμν΅λλ€. | |
| const thumbBorderColor = emotionWeightPercent >= 50 ? roseColor : indigoColor; | |
| weightSlider.style.setProperty('--thumb-border-color', thumbBorderColor); | |
| } | |
| weightSlider.addEventListener('input', updateSliderAppearance); | |
| document.addEventListener('DOMContentLoaded', updateSliderAppearance); | |
| // β β β β β μμ λ β β β β β | |
| async function getRecommendation() { | |
| const text = userInput.value.trim(); | |
| if (!text) { alert('κΈ°λΆμ΄λ μν©μ μ λ ₯ν΄μ£ΌμΈμ!'); return; } | |
| // β β β β β μμ λ λΆλΆ: μ¬λΌμ΄λμμ νμ¬ κ°μ€μΉ κ° κ°μ Έμ€κΈ° β β β β β | |
| const emotionWeight = weightSlider.value / 100; | |
| const contextWeight = 1 - emotionWeight; | |
| // β β β β β μμ λ β β β β β | |
| recommendButton.disabled = true; | |
| recommendButton.textContent = 'λΆμ μ€...'; | |
| resultsDiv.innerHTML = ''; | |
| loadingIndicator.classList.remove('hidden'); | |
| try { | |
| // β β β β β μμ λ λΆλΆ: API μμ²μ κ°μ€μΉ κ° ν¬ν¨μν€κΈ° β β β β β | |
| const response = await fetch('/recommend_music', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| text: text, | |
| emotion_weight: emotionWeight, | |
| context_weight: contextWeight | |
| }) | |
| }); | |
| // β β β β β μμ λ β β β β β | |
| if (!response.ok) { throw new Error(`μλ² μ€λ₯: ${response.statusText}`); } | |
| const data = await response.json(); | |
| displayResults(data); | |
| } catch (error) { | |
| resultsDiv.innerHTML = `<div class="text-red-300 p-4 rounded-lg text-center"><p>μ€λ₯κ° λ°μνμ΅λλ€. μ μ ν λ€μ μλν΄μ£ΌμΈμ.</p></div>`; | |
| } finally { | |
| loadingIndicator.classList.add('hidden'); | |
| recommendButton.disabled = false; | |
| recommendButton.textContent = 'μμ μΆμ²λ°κΈ°'; | |
| } | |
| } | |
| userInput.addEventListener('keydown', (event) => { | |
| if (event.key === 'Enter' && !event.shiftKey) { | |
| event.preventDefault(); | |
| getRecommendation(); | |
| } | |
| }); | |
| function displayResults(data) { | |
| if (data.error) { | |
| resultsDiv.innerHTML = `<p class="text-center text-red-400">${data.error}</p>`; | |
| return; | |
| } | |
| const emotionHtml = `<div class="text-center"><p class="text-slate-400 text-sm mb-1">λΆμλ κ°μ </p><span class="bg-indigo-500/20 text-indigo-300 text-md font-medium px-4 py-1 rounded-full">${data.emotion}</span></div>`; | |
| let recommendationsHtml = ''; | |
| if (data.recommendations && data.recommendations.length > 0) { | |
| recommendationsHtml = data.recommendations.map(song => { | |
| const youtubeUrl = `https://youtube.com/watch?v=${song.videoId}`; | |
| return ` | |
| <div class="bg-slate-700/50 p-4 rounded-lg border border-slate-600 flex items-center gap-4 transition hover:bg-slate-700"> | |
| <div class="text-2xl font-bold text-slate-500">${song.rank}</div> | |
| <div class="flex-grow"> | |
| <a href="${youtubeUrl}" target="_blank" rel="noopener noreferrer" class="font-semibold text-slate-200 hover:text-indigo-400 transition-colors">${song.title}</a> | |
| <p class="text-sm text-slate-400">${song.artist}</p> | |
| </div> | |
| <div class="text-right"> | |
| <p class="text-xs text-slate-500">μ μ¬λ</p> | |
| <p class="font-mono text-indigo-400">${song.score.toFixed(4)}</p> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| } else { | |
| recommendationsHtml = `<p class="text-center text-slate-400">μμ½μ§λ§, μ΄μΈλ¦¬λ λ Έλλ₯Ό μ°Ύμ§ λͺ»νμ΄μ.</p>`; | |
| } | |
| resultsDiv.innerHTML = `${emotionHtml}<div class="space-y-3 pt-4">${recommendationsHtml}</div>`; | |
| } | |
| </script> | |
| </body> | |
| </html> | |