SJLee-0525
[CHORE] test28
6d34043
"""
์„ฑ๊ณต ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ
๊ฒ€์ฆ ์„ฑ๊ณต ์‹œ ์ „์ฒด ํ™”๋ฉด ์ „ํ™˜
ํ†ตํ•ฉ ๋ฒ„์ „: Backend API ๋Œ€์‹  Gradio file URL ์‚ฌ์šฉ
"""
import gradio as gr
class SuccessScreenComponent:
"""์„ฑ๊ณต ํ™”๋ฉด ์ปดํฌ๋„ŒํŠธ"""
# HTML ํ…œํ”Œ๋ฆฟ (data-* ์†์„ฑ์œผ๋กœ ๊ฐ’ ์‹๋ณ„)
HTML_TEMPLATE = """
<div class="success-content-wrapper" id="success-content-inner">
<h1 class="success-title">๐ŸŽ‰ Congratulations!</h1>
<!-- ์ •๋‹ต ์„ธ์…˜ -->
<div class="audio-compare-section">
<div class="answer-word" id="success-answer-word">-</div>
<!-- ์ •๋‹ต ์Œ์„ฑ -->
<div class="audio-card">
<audio controls preload="auto" id="success-correct-audio" src=""></audio>
</div>
</div>
<!-- ํ†ต๊ณ„ ๊ทธ๋ฆฌ๋“œ (3x2) -->
<div class="stats-grid">
<!-- Row 1: ์˜ค๋Š˜ ํ†ต๊ณ„ -->
<div class="stat-card stat-blue">
<div class="stat-label">Today Participants</div>
<div class="stat-value" id="success-today-participants">-</div>
</div>
<div class="stat-card stat-green">
<div class="stat-label">Today Success Rate</div>
<div class="stat-value" id="success-today-success-rate">-</div>
</div>
<div class="stat-card stat-yellow">
<div class="stat-label">Today Attempts</div>
<div class="stat-value" id="success-today-attempts">-</div>
</div>
<!-- Row 2: ์ „์ฒด ํ†ต๊ณ„ -->
<div class="stat-card stat-indigo">
<div class="stat-label">Total Participants</div>
<div class="stat-value" id="success-total-participants">-</div>
</div>
<div class="stat-card stat-emerald">
<div class="stat-label">Total Success Rate</div>
<div class="stat-value" id="success-total-success-rate">-</div>
</div>
<div class="stat-card stat-orange">
<div class="stat-label">Total Attempts</div>
<div class="stat-value" id="success-total-attempts">-</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js_on_load():
"""JS ์ฝ”๋“œ - game_state์—์„œ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (ํ†ตํ•ฉ ๋ฒ„์ „)"""
return """
// MutationObserver๋กœ success-screen์ด visible ๋  ๋•Œ ๊ฐ์ง€
const successScreen = document.getElementById('success-screen');
if (successScreen) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
// visible ์ƒํƒœ ์ฒดํฌ (Gradio๊ฐ€ hidden ํด๋ž˜์Šค ์ œ๊ฑฐํ•  ๋•Œ)
const isVisible = !successScreen.classList.contains('hidden') &&
successScreen.style.display !== 'none';
if (isVisible) {
updateSuccessScreen();
}
}
});
});
observer.observe(successScreen, { attributes: true, attributeFilter: ['class', 'style'] });
}
// API์—์„œ ์‹ค์‹œ๊ฐ„ stats ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ (Gradio API ์‚ฌ์šฉ)
async function updateSuccessScreen() {
try {
// Gradio API ํ˜ธ์ถœ (event_id ๋ฐ›๊ณ  ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ)
const callResponse = await fetch('/gradio_api/call/dashboard', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ data: [] })
});
const { event_id } = await callResponse.json();
// SSE๋กœ ๊ฒฐ๊ณผ ๊ฐ€์ ธ์˜ค๊ธฐ
const resultResponse = await fetch('/gradio_api/call/dashboard/' + event_id);
const text = await resultResponse.text();
const lines = text.trim().split('\\n');
const dataLine = lines.find(l => l.startsWith('data:'));
const stats = JSON.parse(dataLine.replace('data: ', ''))[0];
console.log('[SUCCESS SCREEN] Stats:', stats);
// Answer Word ์„ค์ •
const answerWordEl = document.getElementById('success-answer-word');
if (answerWordEl && stats.answer_word) {
answerWordEl.textContent = stats.answer_word;
}
// Correct Audio ์„ค์ • (์ ˆ๋Œ€ ๊ฒฝ๋กœ)
const correctAudio = document.getElementById('success-correct-audio');
if (correctAudio && stats.reference_audio_path) {
const audioUrl = '/gradio_api/file=' + stats.reference_audio_path;
correctAudio.src = audioUrl;
correctAudio.load();
console.log('[SUCCESS SCREEN] Audio URL:', audioUrl);
}
// ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
const todayParticipants = document.getElementById('success-today-participants');
const todaySuccessRate = document.getElementById('success-today-success-rate');
const todayAttempts = document.getElementById('success-today-attempts');
const totalParticipants = document.getElementById('success-total-participants');
const totalSuccessRate = document.getElementById('success-total-success-rate');
const totalAttempts = document.getElementById('success-total-attempts');
if (todayParticipants) todayParticipants.textContent = stats.today_participants || 0;
if (todaySuccessRate) todaySuccessRate.textContent = (stats.today_success_rate || 0) + '%';
if (todayAttempts) todayAttempts.textContent = stats.today_attempts || 0;
if (totalParticipants) totalParticipants.textContent = stats.total_participants || 0;
if (totalSuccessRate) totalSuccessRate.textContent = (stats.total_success_rate || 0) + '%';
if (totalAttempts) totalAttempts.textContent = stats.total_attempts || 0;
// User Audio ์„ค์ • (game_state์—์„œ)
const gameStateStr = localStorage.getItem('game_state');
if (gameStateStr) {
const gameState = JSON.parse(gameStateStr);
const guesses = gameState.guesses || [];
const lastGuess = guesses.length > 0 ? guesses[guesses.length - 1] : null;
const userAudio = document.getElementById('success-user-audio');
if (userAudio && lastGuess && lastGuess.audioFile) {
userAudio.src = '/gradio_api/file=' + lastGuess.audioFile;
userAudio.load();
}
}
} catch (e) {
console.error('[SUCCESS SCREEN] Error:', e);
}
}
// ์ดˆ๊ธฐ ๋กœ๋“œ ์‹œ์—๋„ ํ•œ๋ฒˆ ํ˜ธ์ถœ (visible ์ƒํƒœ๋ฉด)
if (successScreen && !successScreen.classList.contains('hidden')) {
updateSuccessScreen();
}
"""
def __init__(self):
self.success_screen = None
self.success_content = None
self.restart_btn = None
def render(self, stats: dict = None):
"""
์„ฑ๊ณต ํ™”๋ฉด UI ๋ Œ๋”๋ง
Args:
stats: ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ๋”•์…”๋„ˆ๋ฆฌ (์ดˆ๊ธฐ๊ฐ’, JS๊ฐ€ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ)
Returns:
tuple: (success_screen, success_content, restart_btn)
"""
self.success_screen = gr.Column(visible=False, elem_id="success-screen")
with self.success_screen:
gr.Markdown("")
# HTML + JS๋กœ ์‹ค์‹œ๊ฐ„ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ
self.success_content = gr.HTML(
value=self.HTML_TEMPLATE,
js_on_load=self.get_js_on_load(),
elem_id="success-content"
)
self.restart_btn = gr.Button(
"Restart",
variant="primary",
size="lg",
elem_id="restart-btn"
)
return self.success_screen, self.success_content, self.restart_btn
def setup_events(self):
"""์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ๋ฒ„ํŠผ ์ด๋ฒคํŠธ ๋ฐ”์ธ๋”ฉ (๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์ดˆ๊ธฐํ™” ํ›„ ์ƒˆ๋กœ๊ณ ์นจ)"""
self.restart_btn.click(
fn=None,
js="""() => {
// ๋‚ ์งœ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ์™€ ๋™์ผํ•˜๊ฒŒ ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ์ดˆ๊ธฐํ™”
const today = new Date().toISOString().split('T')[0];
const newStorage = {
date: today,
failures: [],
successes: []
};
// Gradio BrowserState ํ‚ค๋กœ ์ €์žฅ
localStorage.setItem('audio_validation_history', JSON.stringify(newStorage));
localStorage.removeItem('game_state');
localStorage.removeItem('dashboard_stats');
// ํŽ˜์ด์ง€ ์ƒˆ๋กœ๊ณ ์นจ
window.location.reload();
}"""
)