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

🎉 Congratulations!

-
Today Participants
-
Today Success Rate
-
Today Attempts
-
Total Participants
-
Total Success Rate
-
Total Attempts
-
""" @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(); }""" )