SJLee-0525
[CHORE] test28
6d34043
"""
포기 화면 컴포넌트
검증 포기 시 전체 화면 전환
통합 버전: Backend API 대신 Gradio file URL 사용
"""
import gradio as gr
class GiveUpScreenComponent:
"""포기 화면 컴포넌트"""
# HTML 템플릿 (data-* 속성으로 값 식별)
HTML_TEMPLATE = """
<div class="giveup-content-wrapper" id="giveup-content-inner">
<h1 class="giveup-title">😢 Game Over</h1>
<!-- 정답 세션 -->
<div class="audio-compare-section">
<div class="answer-word" id="giveup-answer-word">-</div>
<!-- 정답 음성 -->
<div class="audio-card">
<audio controls preload="auto" id="giveup-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="giveup-today-participants">-</div>
</div>
<div class="stat-card stat-green">
<div class="stat-label">Today Success Rate</div>
<div class="stat-value" id="giveup-today-success-rate">-</div>
</div>
<div class="stat-card stat-yellow">
<div class="stat-label">Today Attempts</div>
<div class="stat-value" id="giveup-today-attempts">-</div>
</div>
<!-- Row 2: 전체 통계 -->
<div class="stat-card stat-indigo">
<div class="stat-label">Total Participants</div>
<div class="stat-value" id="giveup-total-participants">-</div>
</div>
<div class="stat-card stat-emerald">
<div class="stat-label">Total Success Rate</div>
<div class="stat-value" id="giveup-total-success-rate">-</div>
</div>
<div class="stat-card stat-orange">
<div class="stat-label">Total Attempts</div>
<div class="stat-value" id="giveup-total-attempts">-</div>
</div>
</div>
</div>
"""
@staticmethod
def get_js_on_load():
"""JS 코드 - game_state에서 데이터 가져오기 (통합 버전)"""
return """
// MutationObserver로 giveup-screen이 visible 될 때 감지
const giveupScreen = document.getElementById('giveup-screen');
if (giveupScreen) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
// visible 상태 체크 (Gradio가 hidden 클래스 제거할 때)
const isVisible = !giveupScreen.classList.contains('hidden') &&
giveupScreen.style.display !== 'none';
if (isVisible) {
updateGiveupScreen();
}
}
});
});
observer.observe(giveupScreen, { attributes: true, attributeFilter: ['class', 'style'] });
}
// API에서 실시간 stats 데이터 가져오기 (Gradio API 사용)
async function updateGiveupScreen() {
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('[GIVEUP SCREEN] Stats:', stats);
// Answer Word 설정
const answerWordEl = document.getElementById('giveup-answer-word');
if (answerWordEl && stats.answer_word) {
answerWordEl.textContent = stats.answer_word;
}
// Correct Audio 설정 (절대 경로)
const correctAudio = document.getElementById('giveup-correct-audio');
if (correctAudio && stats.reference_audio_path) {
const audioUrl = '/gradio_api/file=' + stats.reference_audio_path;
correctAudio.src = audioUrl;
correctAudio.load();
console.log('[GIVEUP SCREEN] Audio URL:', audioUrl);
}
// 통계 업데이트
const todayParticipants = document.getElementById('giveup-today-participants');
const todaySuccessRate = document.getElementById('giveup-today-success-rate');
const todayAttempts = document.getElementById('giveup-today-attempts');
const totalParticipants = document.getElementById('giveup-total-participants');
const totalSuccessRate = document.getElementById('giveup-total-success-rate');
const totalAttempts = document.getElementById('giveup-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;
} catch (e) {
console.error('[GIVEUP SCREEN] Error:', e);
}
}
// 초기 로드 시에도 한번 호출 (visible 상태면)
if (giveupScreen && !giveupScreen.classList.contains('hidden')) {
updateGiveupScreen();
}
"""
def __init__(self):
self.giveup_screen = None
self.giveup_content = None
self.restart_btn = None
def render(self, stats: dict = None):
"""
포기 화면 UI 렌더링
Args:
stats: 통계 데이터 딕셔너리 (초기값, JS가 실시간 업데이트)
Returns:
tuple: (giveup_screen, giveup_content, restart_btn)
"""
self.giveup_screen = gr.Column(visible=False, elem_id="giveup-screen")
with self.giveup_screen:
gr.Markdown("")
# HTML + JS로 실시간 통계 업데이트
self.giveup_content = gr.HTML(
value=self.HTML_TEMPLATE,
js_on_load=self.get_js_on_load(),
elem_id="giveup-content"
)
self.restart_btn = gr.Button(
"Restart",
variant="primary",
size="lg",
elem_id="restart-btn"
)
return self.giveup_screen, self.giveup_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();
}"""
)