Yrdksam's picture
Upload 18 files
8c96c3b verified
<!DOCTYPE html>
<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>