SJLee-0525
[TEST] test45
ab1ab06
"""
์›ฐ์ปด ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ - YouTube ํŠœํ† ๋ฆฌ์–ผ ์˜์ƒ ํŒ์—…
์ฒซ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™์œผ๋กœ ํ‘œ์‹œ, "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ" ๊ธฐ๋Šฅ ์ง€์›
localStorage ํ‚ค: 'welcome_modal_dismissed' (๊ฒŒ์ž„ ์ƒํƒœ์™€ ๋…๋ฆฝ์ )
"""
import gradio as gr
class WelcomeModal:
"""
์›ฐ์ปด ๋ชจ๋‹ฌ - YouTube ํŠœํ† ๋ฆฌ์–ผ ์˜์ƒ ํ‘œ์‹œ
- ์ฒซ ๋ฐฉ๋ฌธ ์‹œ ์ž๋™ ํ‘œ์‹œ
- "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ" ํด๋ฆญ ์‹œ localStorage์— ์ €์žฅ
- ๊ฒŒ์ž„ ์ดˆ๊ธฐํ™”์™€ ๋…๋ฆฝ์ ์œผ๋กœ ์„ค์ • ์œ ์ง€
"""
# YouTube ์˜์ƒ ID
YOUTUBE_VIDEO_ID = "YbCf6x0B3fU"
# localStorage ํ‚ค (๊ฒŒ์ž„ ์ƒํƒœ์™€ ๋ณ„๋„)
STORAGE_KEY = "welcome_modal_dismissed"
HTML_TEMPLATE = """
${value && value.visible ? `
<div class="welcome-modal-overlay" data-welcome-overlay="true">
<div class="welcome-modal-container">
<div class="welcome-modal-content">
<h2 class="welcome-modal-title">Welcome to Voice Sementle!</h2>
<div class="welcome-modal-video">
<iframe
src="https://www.youtube.com/embed/${value.videoId || 'YbCf6x0B3fU'}?rel=0&modestbranding=1"
title="Voice Sementle Tutorial"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
referrerpolicy="strict-origin-when-cross-origin"
allowfullscreen>
</iframe>
</div>
<div class="welcome-modal-actions">
<button class="welcome-modal-btn welcome-modal-btn-primary" data-welcome-close="true">
Start Playing
</button>
<button class="welcome-modal-btn welcome-modal-btn-secondary" data-welcome-dismiss="true">
Don't show again
</button>
</div>
</div>
</div>
</div>
` : ''}
"""
CSS_TEMPLATE = """
/* ์›ฐ์ปด ๋ชจ๋‹ฌ - ์ „์—ญ ์Šคํƒ€์ผ์— ์˜ํ–ฅ ์—†๋„๋ก ์Šค์ฝ”ํ•‘ */
.welcome-modal-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
background-color: rgba(0, 0, 0, 0.7) !important;
backdrop-filter: blur(4px);
z-index: 9999 !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding: 20px;
box-sizing: border-box;
font-family: inherit;
}
.welcome-modal-container {
width: 100%;
max-width: 1280px;
max-height: 90vh;
overflow-y: auto;
position: relative;
animation: welcomeModalFadeIn 0.3s ease-out;
font-family: inherit;
border-radius: 20px;
}
@keyframes welcomeModalFadeIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.welcome-modal-close {
position: absolute;
top: 16px;
right: 16px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
transition: all 0.2s ease;
z-index: 10;
padding: 0;
font-family: inherit;
}
.welcome-modal-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.welcome-modal-content {
padding: 32px;
text-align: center;
font-family: inherit;
}
.welcome-modal-title {
font-size: 60px;
font-weight: 700;
color: #ffffff;
margin: 0 0 24px 0;
font-family: inherit;
}
.welcome-modal-subtitle {
font-size: 16px;
color: rgba(255, 255, 255, 0.7);
margin: 0 0 24px 0;
font-family: inherit;
}
.welcome-modal-video {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
border-radius: 16px;
overflow: hidden;
background: #000;
margin-bottom: 24px;
}
.welcome-modal-video iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.welcome-modal-actions {
display: flex;
justify-content: center;
gap: 12px;
align-items: center;
}
.welcome-modal-btn {
padding: 14px 32px;
border-radius: 20px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
border: none;
min-width: 200px;
font-family: inherit;
}
.welcome-modal-btn-primary {
background: #4db8ff !important;
color: #ffffff !important;
}
.welcome-modal-btn-primary:hover {
transform: translateY(-2px);
}
.welcome-modal-btn-secondary {
background: transparent;
color: rgba(255, 255, 255, 0.6);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.welcome-modal-btn-secondary:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.9);
}
/* Mobile responsive */
@media (max-width: 600px) {
.welcome-modal-container {
max-width: 100%;
border-radius: 16px;
}
.welcome-modal-content {
padding: 24px 16px;
}
.welcome-modal-title {
font-size: 22px;
}
.welcome-modal-btn {
width: 100%;
min-width: unset;
}
}
"""
JS_ON_LOAD = """
const STORAGE_KEY = 'welcome_modal_dismissed';
const videoId = props.value?.videoId || 'YbCf6x0B3fU';
// ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ localStorage ์ฒดํฌ
const isDismissed = localStorage.getItem(STORAGE_KEY) === 'true';
// "๋‹ค์‹œ ๋ณด์ง€ ์•Š๊ธฐ"๋ฅผ ์„ ํƒํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ๋ชจ๋‹ฌ ํ‘œ์‹œ
if (!isDismissed) {
// ํฐํŠธ๊ฐ€ ์™„์ „ํžˆ ๋กœ๋“œ๋œ ํ›„ ๋ชจ๋‹ฌ ํ‘œ์‹œ
document.fonts.ready.then(() => {
// ํฐํŠธ ๋กœ๋“œ ํ›„ ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด ์ถ”๊ฐ€ (๋ Œ๋”๋ง ์•ˆ์ •ํ™”)
setTimeout(() => {
props.value = { visible: true, videoId: videoId };
}, 100);
});
}
// ํด๋ฆญ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
element.addEventListener('click', (e) => {
// ๋ฐฐ๊ฒฝ ํด๋ฆญ์œผ๋กœ ๋‹ซ๊ธฐ
if (e.target.dataset.welcomeOverlay === 'true') {
props.value = { visible: false, videoId: props.value?.videoId };
}
// X ๋ฒ„ํŠผ ๋˜๋Š” "Start Playing" ๋ฒ„ํŠผ ํด๋ฆญ
if (e.target.closest('[data-welcome-close="true"]')) {
props.value = { visible: false, videoId: props.value?.videoId };
}
// "Don't show again" ๋ฒ„ํŠผ ํด๋ฆญ
if (e.target.closest('[data-welcome-dismiss="true"]')) {
localStorage.setItem(STORAGE_KEY, 'true');
props.value = { visible: false, videoId: props.value?.videoId };
}
});
// ESC ํ‚ค๋กœ ๋‹ซ๊ธฐ
const handleKeydown = (e) => {
if (e.key === 'Escape' && props.value && props.value.visible) {
props.value = { visible: false, videoId: props.value?.videoId };
}
};
document.addEventListener('keydown', handleKeydown);
"""
def __init__(self, video_id: str = None):
"""
์›ฐ์ปด ๋ชจ๋‹ฌ ์ดˆ๊ธฐํ™”
Args:
video_id: YouTube ์˜์ƒ ID (๊ธฐ๋ณธ๊ฐ’: YbCf6x0B3fU)
"""
self.video_id = video_id or self.YOUTUBE_VIDEO_ID
self.component = None
def render(self) -> gr.HTML:
"""
์›ฐ์ปด ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง
Returns:
gr.HTML: ๋ Œ๋”๋ง๋œ ๋ชจ๋‹ฌ ์ปดํฌ๋„ŒํŠธ
"""
# ์ดˆ๊ธฐ๊ฐ’์€ visible: False๋กœ ์„ค์ • (JS์—์„œ ๋กœ๋“œ ํ›„ ํ‘œ์‹œ)
self.component = gr.HTML(
value={"visible": False, "videoId": self.video_id},
html_template=self.HTML_TEMPLATE,
css_template=self.CSS_TEMPLATE,
js_on_load=self.JS_ON_LOAD,
elem_id="welcome-modal",
)
return self.component
@staticmethod
def show(video_id: str = "YbCf6x0B3fU") -> dict:
"""๋ชจ๋‹ฌ ์—ด๊ธฐ"""
return {"visible": True, "videoId": video_id}
@staticmethod
def hide() -> dict:
"""๋ชจ๋‹ฌ ๋‹ซ๊ธฐ"""
return {"visible": False, "videoId": ""}