"""
성공 화면 컴포넌트
검증 성공 시 전체 화면 전환
통합 버전: Backend API 대신 Gradio file URL 사용
"""
import gradio as gr
class SuccessScreenComponent:
"""성공 화면 컴포넌트"""
# HTML 템플릿 (data-* 속성으로 값 식별)
HTML_TEMPLATE = """
"""
@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();
}"""
)