|
|
""" |
|
|
포기 화면 컴포넌트 |
|
|
검증 포기 시 전체 화면 전환 |
|
|
|
|
|
통합 버전: Backend API 대신 Gradio file URL 사용 |
|
|
""" |
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
|
|
|
class GiveUpScreenComponent: |
|
|
"""포기 화면 컴포넌트""" |
|
|
|
|
|
|
|
|
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("") |
|
|
|
|
|
|
|
|
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(); |
|
|
}""" |
|
|
) |
|
|
|