Spaces:
Paused
Paused
import os | |
import re | |
import random | |
import time | |
import html | |
import base64 | |
import string | |
import json | |
import asyncio | |
import requests | |
import anthropic | |
import openai | |
from http import HTTPStatus | |
from typing import Dict, List, Optional, Tuple | |
from functools import partial | |
import gradio as gr | |
import modelscope_studio.components.base as ms | |
import modelscope_studio.components.legacy as legacy | |
import modelscope_studio.components.antd as antd | |
class Role: | |
SYSTEM = "system" | |
USER = "user" | |
ASSISTANT = "assistant" | |
DEMO_LIST = [ | |
{"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요..."}, | |
{"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요..."}, | |
# ... 중략 (게임 템플릿) ... | |
] | |
SystemPrompt = """# GameCraft 시스템 프롬프트 | |
(중략) | |
""" | |
History = List[Tuple[str, str]] | |
Messages = List[Dict[str, str]] | |
IMAGE_CACHE = {} | |
def get_image_base64(image_path): | |
if image_path in IMAGE_CACHE: | |
return IMAGE_CACHE[image_path] | |
try: | |
with open(image_path, "rb") as image_file: | |
encoded_string = base64.b64encode(image_file.read()).decode() | |
IMAGE_CACHE[image_path] = encoded_string | |
return encoded_string | |
except: | |
return IMAGE_CACHE.get('default.png', '') | |
def history_to_messages(history: History, system: str) -> Messages: | |
messages = [{'role': Role.SYSTEM, 'content': system}] | |
for h in history: | |
messages.append({'role': Role.USER, 'content': h[0]}) | |
messages.append({'role': Role.ASSISTANT, 'content': h[1]}) | |
return messages | |
def messages_to_history(messages: Messages) -> History: | |
assert messages[0]['role'] == Role.SYSTEM | |
history = [] | |
for q, r in zip(messages[1::2], messages[2::2]): | |
history.append([q['content'], r['content']]) | |
return history | |
YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip() | |
YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip() | |
claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN) | |
openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN) | |
async def try_claude_api(system_message, claude_messages, timeout=15): | |
try: | |
system_message_with_limit = system_message + "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 말 것..." | |
start_time = time.time() | |
with claude_client.messages.stream( | |
model="claude-2", | |
max_tokens=19800, | |
system=system_message_with_limit, | |
messages=claude_messages, | |
temperature=0.3, | |
) as stream: | |
collected_content = "" | |
for chunk in stream: | |
current_time = time.time() | |
if current_time - start_time > timeout: | |
raise TimeoutError("Claude API timeout") | |
if chunk.type == "content_block_delta": | |
collected_content += chunk.delta.text | |
yield collected_content | |
await asyncio.sleep(0) | |
start_time = current_time | |
except Exception as e: | |
raise e | |
async def try_openai_api(openai_messages): | |
try: | |
if openai_messages and openai_messages[0]["role"] == "system": | |
openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 말 것..." | |
stream = openai_client.chat.completions.create( | |
model="gpt-4", | |
messages=openai_messages, | |
stream=True, | |
max_tokens=19800, | |
temperature=0.2 | |
) | |
collected_content = "" | |
for chunk in stream: | |
if chunk.choices[0].delta.content is not None: | |
collected_content += chunk.choices[0].delta.content | |
yield collected_content | |
except Exception as e: | |
raise e | |
def load_json_data(): | |
data_list = [] | |
for item in DEMO_LIST: | |
data_list.append({ | |
"name": f"[게임] {item['description'][:20]}...", | |
"prompt": item['description'] | |
}) | |
return data_list | |
def create_template_html(title, items): | |
import html as html_lib | |
html_content = r""" | |
<style> | |
.prompt-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 16px; | |
padding: 12px; | |
} | |
.prompt-card { | |
background: white; | |
border: 1px solid #eee; | |
border-radius: 12px; | |
padding: 12px; | |
cursor: pointer; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.05); | |
transition: all 0.3s ease; | |
} | |
.prompt-card:hover { | |
transform: translateY(-4px); | |
box-shadow: 0 6px 12px rgba(0,0,0,0.1); | |
} | |
.card-name { | |
font-weight: bold; | |
margin-bottom: 8px; | |
font-size: 13px; | |
color: #444; | |
} | |
.card-prompt { | |
font-size: 11px; | |
line-height: 1.4; | |
color: #666; | |
display: -webkit-box; | |
-webkit-line-clamp: 7; | |
-webkit-box-orient: vertical; | |
overflow: hidden; | |
height: 84px; | |
background-color: #f8f9fa; | |
padding: 8px; | |
border-radius: 6px; | |
} | |
</style> | |
<div class="prompt-grid"> | |
""" | |
for item in items: | |
card_html = f""" | |
<div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html_lib.escape(item.get('prompt', ''))}"> | |
<div class="card-name">{html_lib.escape(item.get('name', ''))}</div> | |
<div class="card-prompt">{html_lib.escape(item.get('prompt', ''))}</div> | |
</div> | |
""" | |
html_content += card_html | |
html_content += r""" | |
</div> | |
<script> | |
function copyToInput(card) { | |
const prompt = card.dataset.prompt; | |
const textarea = document.querySelector('.ant-input-textarea-large textarea'); | |
if (textarea) { | |
textarea.value = prompt; | |
textarea.dispatchEvent(new Event('input', { bubbles: true })); | |
// 템플릿 Drawer 닫기 | |
document.querySelector('.session-drawer .close-btn').click(); | |
} | |
} | |
</script> | |
""" | |
return gr.HTML(value=html_content) | |
def load_all_templates(): | |
return create_template_html("🎮 모든 게임 템플릿", load_json_data()) | |
def remove_code_block(text): | |
pattern = r'```html\s*([\s\S]+?)\s*```' | |
match = re.search(pattern, text, re.DOTALL) | |
if match: | |
return match.group(1).strip() | |
pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```' | |
match = re.search(pattern, text, re.DOTALL) | |
if match: | |
return match.group(1).strip() | |
text = re.sub(r'```html\s*', '', text) | |
text = re.sub(r'\s*```', '', text) | |
return text.strip() | |
def optimize_code(code: str) -> str: | |
if not code or len(code.strip()) == 0: | |
return code | |
lines = code.split('\n') | |
if len(lines) <= 200: | |
return code | |
comment_patterns = [ | |
r'/\*[\s\S]*?\*/', | |
r'//.*?$', | |
r'<!--[\s\S]*?-->' | |
] | |
cleaned_code = code | |
for pattern in comment_patterns: | |
cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE) | |
cleaned_lines = [] | |
empty_line_count = 0 | |
for line in cleaned_code.split('\n'): | |
if line.strip() == '': | |
empty_line_count += 1 | |
if empty_line_count <= 1: | |
cleaned_lines.append('') | |
else: | |
empty_line_count = 0 | |
cleaned_lines.append(line) | |
cleaned_code = '\n'.join(cleaned_lines) | |
cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE) | |
cleaned_code = re.sub(r' {2,}', ' ', cleaned_code) | |
return cleaned_code | |
def send_to_sandbox(code): | |
clean_code = remove_code_block(code) | |
clean_code = optimize_code(clean_code) | |
if clean_code.startswith('```html'): | |
clean_code = clean_code[7:].strip() | |
if clean_code.endswith('```'): | |
clean_code = clean_code[:-3].strip() | |
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'): | |
clean_code = f"""<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Game Preview</title> | |
</head> | |
<body> | |
{clean_code} | |
</body> | |
</html>""" | |
encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8') | |
data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}" | |
return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>' | |
def boost_prompt(prompt: str) -> str: | |
if not prompt: | |
return "" | |
boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다. | |
(중략) | |
""" | |
try: | |
# 시도1: Claude | |
try: | |
response = claude_client.messages.create( | |
model="claude-2", | |
max_tokens=10000, | |
temperature=0.3, | |
messages=[{ | |
"role": "user", | |
"content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}" | |
}], | |
system=boost_system_prompt | |
) | |
if hasattr(response, 'content') and len(response.content) > 0: | |
return response.content[0].text | |
raise Exception("Claude 응답 형식 오류") | |
except Exception: | |
# 시도2: OpenAI | |
completion = openai_client.chat.completions.create( | |
model="gpt-4", | |
messages=[ | |
{"role": "system", "content": boost_system_prompt}, | |
{"role": "user", "content": f"다음 게임 프롬프트를 증강: {prompt}"} | |
], | |
max_tokens=10000, | |
temperature=0.3 | |
) | |
if completion.choices and len(completion.choices) > 0: | |
return completion.choices[0].message.content | |
raise Exception("OpenAI 응답 형식 오류") | |
except Exception: | |
return prompt | |
def handle_boost(prompt: str): | |
try: | |
boosted_prompt = boost_prompt(prompt) | |
return boosted_prompt, gr.update(active_key="empty") | |
except: | |
return prompt, gr.update(active_key="empty") | |
def history_render(history: History): | |
return gr.update(open=True), history | |
def execute_code(query: str): | |
if not query or query.strip() == '': | |
return None, gr.update(active_key="empty") | |
try: | |
clean_code = remove_code_block(query) | |
if clean_code.startswith('```html'): | |
clean_code = clean_code[7:].strip() | |
if clean_code.endswith('```'): | |
clean_code = clean_code[:-3].strip() | |
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'): | |
if not ('<body' in clean_code and '</body>' in clean_code): | |
clean_code = f"""<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Game Preview</title> | |
</head> | |
<body> | |
{clean_code} | |
</body> | |
</html>""" | |
return send_to_sandbox(clean_code), gr.update(active_key="render") | |
except Exception as e: | |
print(f"Execute code error: {str(e)}") | |
return None, gr.update(active_key="empty") | |
class Demo: | |
def __init__(self): | |
pass | |
async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]): | |
if not query or query.strip() == '': | |
query = random.choice(DEMO_LIST)['description'] | |
if _history is None: | |
_history = [] | |
query = f""" | |
다음 게임을 제작해주세요. | |
(중략) | |
게임 요청: {query} | |
""" | |
messages = history_to_messages(_history, _setting['system']) | |
system_message = messages[0]['content'] | |
claude_messages = [ | |
{"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]} | |
for msg in messages[1:] + [{'role': Role.USER, 'content': query}] | |
if msg["content"].strip() != '' | |
] | |
openai_messages = [{"role": "system", "content": system_message}] | |
for msg in messages[1:]: | |
openai_messages.append({ | |
"role": msg["role"], | |
"content": msg["content"] | |
}) | |
openai_messages.append({"role": "user", "content": query}) | |
try: | |
yield [ | |
"Generating code...", | |
_history, | |
None, | |
gr.update(active_key="loading"), | |
gr.update(open=True) | |
] | |
await asyncio.sleep(0) | |
collected_content = None | |
try: | |
async for content in try_claude_api(system_message, claude_messages): | |
yield [ | |
content, | |
_history, | |
None, | |
gr.update(active_key="loading"), | |
gr.update(open=True) | |
] | |
await asyncio.sleep(0) | |
collected_content = content | |
except Exception: | |
async for content in try_openai_api(openai_messages): | |
yield [ | |
content, | |
_history, | |
None, | |
gr.update(active_key="loading"), | |
gr.update(open=True) | |
] | |
await asyncio.sleep(0) | |
collected_content = content | |
if collected_content: | |
clean_code = remove_code_block(collected_content) | |
code_lines = clean_code.count('\n') + 1 | |
if code_lines > 700: | |
warning_msg = f""" | |
⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄)** | |
(중략) | |
""" | |
collected_content = warning_msg | |
yield [ | |
collected_content, | |
_history, | |
None, | |
gr.update(active_key="empty"), | |
gr.update(open=True) | |
] | |
else: | |
_history = messages_to_history([ | |
{'role': Role.SYSTEM, 'content': system_message} | |
] + claude_messages + [{ | |
'role': Role.ASSISTANT, | |
'content': collected_content | |
}]) | |
yield [ | |
collected_content, | |
_history, | |
send_to_sandbox(clean_code), | |
gr.update(active_key="render"), | |
gr.update(open=True) | |
] | |
else: | |
raise ValueError("No content generated.") | |
except Exception as e: | |
raise ValueError(f'Error calling APIs: {str(e)}') | |
def clear_history(self): | |
return [] | |
demo_instance = Demo() | |
theme = gr.themes.Soft( | |
primary_hue="blue", | |
secondary_hue="purple", | |
neutral_hue="slate", | |
spacing_size=gr.themes.sizes.spacing_md, | |
radius_size=gr.themes.sizes.radius_md, | |
text_size=gr.themes.sizes.text_md, | |
) | |
with gr.Blocks(css_paths=["app.css"], theme=theme) as demo: | |
header_html = gr.HTML(""" | |
<div class="app-header"> | |
<h1>🎮 Vibe Game Craft</h1> | |
<p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다...</p> | |
</div> | |
<!-- 배포 결과 배너 - 헤더 바로 아래 위치 --> | |
<div id="deploy-banner" style="display:none;" class="deploy-banner"> | |
<div class="deploy-banner-content"> | |
<div class="deploy-banner-icon">🚀</div> | |
<div class="deploy-banner-info"> | |
<div id="deploy-banner-title" class="deploy-banner-title">배포 상태</div> | |
<div id="deploy-banner-message" class="deploy-banner-message"></div> | |
</div> | |
<div id="deploy-banner-url-container" class="deploy-banner-url-container" style="display:none;"> | |
<a id="deploy-banner-url" href="#" target="_blank" class="deploy-banner-url"></a> | |
<button onclick="copyBannerUrl()" class="deploy-banner-copy-btn">복사</button> | |
</div> | |
</div> | |
</div> | |
<script> | |
function copyBannerUrl() { | |
const url = document.getElementById('deploy-banner-url').href; | |
navigator.clipboard.writeText(url).then(()=>{ | |
const copyBtn = document.querySelector('.deploy-banner-copy-btn'); | |
const originalText = copyBtn.textContent; | |
copyBtn.textContent = '복사됨!'; | |
setTimeout(()=>{copyBtn.textContent=originalText;},1000); | |
}); | |
} | |
function showDeployBanner(type, title, message, url) { | |
const banner = document.getElementById('deploy-banner'); | |
banner.className = 'deploy-banner '+type; | |
document.getElementById('deploy-banner-title').textContent = title; | |
document.getElementById('deploy-banner-message').textContent = message; | |
const urlContainer = document.getElementById('deploy-banner-url-container'); | |
const bannerUrl = document.getElementById('deploy-banner-url'); | |
if(url){ | |
bannerUrl.href=url; bannerUrl.textContent=url; | |
urlContainer.style.display='flex'; | |
} else { urlContainer.style.display='none'; } | |
banner.style.display='block'; | |
} | |
</script> | |
""") | |
history = gr.State([]) | |
setting = gr.State({"system": SystemPrompt}) | |
deploy_status = gr.State({"is_deployed":False,"status":"","url":"","message":""}) | |
with ms.Application() as app: | |
with antd.ConfigProvider(): | |
with antd.Drawer(open=False, title="코드 보기", placement="left", width="750px") as code_drawer: | |
code_output = legacy.Markdown() | |
with antd.Drawer(open=False, title="히스토리", placement="left", width="900px") as history_drawer: | |
history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot") | |
with antd.Drawer(open=False, title="게임 템플릿", placement="right", width="900px", elem_classes="session-drawer") as session_drawer: | |
with antd.Flex(vertical=True, gap="middle"): | |
gr.Markdown("### 사용 가능한 게임 템플릿") | |
session_history = gr.HTML(elem_classes="session-history") | |
close_btn = antd.Button("닫기", type="default", elem_classes="close-btn") | |
with antd.Row(gutter=[32,12], align="top", elem_classes="equal-height-container") as layout: | |
with antd.Col(span=24, md=16, elem_classes="equal-height-col"): | |
with ms.Div(elem_classes="right_panel panel"): | |
gr.HTML(""" | |
<div class="render_header"> | |
<span class="header_btn"></span> | |
<span class="header_btn"></span> | |
<span class="header_btn"></span> | |
</div> | |
""") | |
with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab: | |
with antd.Tabs.Item(key="empty"): | |
empty = antd.Empty(description="게임을 만들려면 설명을 입력하세요", elem_classes="right_content") | |
with antd.Tabs.Item(key="loading"): | |
loading = antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content") | |
with antd.Tabs.Item(key="render"): | |
sandbox = gr.HTML(elem_classes="html_content") | |
with antd.Col(span=24, md=8, elem_classes="equal-height-col"): | |
with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"): | |
with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"): | |
codeBtn = antd.Button("🧑💻 코드 보기", type="default", elem_classes="code-btn") | |
historyBtn = antd.Button("📜 히스토리", type="default", elem_classes="history-btn") | |
template_btn = antd.Button("🎮 템플릿", type="default", elem_classes="template-btn") | |
with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"): | |
btn = antd.Button("전송", type="primary", size="large", elem_classes="send-btn") | |
boost_btn = antd.Button("증강", type="default", size="large", elem_classes="boost-btn") | |
execute_btn = antd.Button("코드", type="default", size="large", elem_classes="execute-btn") | |
deploy_btn = antd.Button("배포", type="default", size="large", elem_classes="deploy-btn") | |
clear_btn = antd.Button("클리어", type="default", size="large", elem_classes="clear-btn") | |
with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"): | |
input_text = antd.InputTextarea( | |
size="large", | |
allow_clear=True, | |
placeholder="예) 테트리스 게임 만들기...", | |
max_length=100000 | |
) | |
gr.HTML('<div class="help-text">💡 원하는 게임의 설명을 입력하세요.</div>') | |
# 별도의 배포 결과 섹션(박스) | |
deploy_result_container = gr.HTML(""" | |
<div class="deploy-section"> | |
<div class="deploy-header">📤 배포 결과</div> | |
<div id="deploy-result-box" class="deploy-result-box"> | |
<div class="no-deploy">아직 배포된 게임이 없습니다.</div> | |
</div> | |
</div> | |
""") | |
js_trigger = gr.HTML(elem_id="js-trigger", visible=False) | |
# Drawer 이벤트 | |
codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer]) | |
code_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[code_drawer]) | |
historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output]) | |
history_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[history_drawer]) | |
template_btn.click(fn=lambda: (gr.update(open=True), load_all_templates()), | |
outputs=[session_drawer, session_history], queue=False) | |
session_drawer.close(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history]) | |
close_btn.click(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history]) | |
btn.click( | |
demo_instance.generation_code, | |
inputs=[input_text, setting, history], | |
outputs=[code_output, history, sandbox, state_tab, code_drawer] | |
) | |
clear_btn.click(demo_instance.clear_history, inputs=[], outputs=[history]) | |
boost_btn.click(handle_boost, inputs=[input_text], outputs=[input_text, state_tab]) | |
execute_btn.click(execute_code, inputs=[input_text], outputs=[sandbox, state_tab]) | |
def deploy_to_vercel(code: str): | |
try: | |
token = "YOUR_VERCEL_TOKEN" # 실제 토큰 | |
if not token: | |
return {"status": "error", "message": "Vercel 토큰이 설정되지 않았습니다."} | |
project_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) | |
deploy_url = "https://api.vercel.com/v13/deployments" | |
headers = {"Authorization": f"Bearer {token}","Content-Type": "application/json"} | |
package_json = { | |
"name": project_name, | |
"version": "1.0.0", | |
"private": True, | |
"dependencies": {"vite": "^5.0.0"}, | |
"scripts": { | |
"dev": "vite", | |
"build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/", | |
"preview": "vite preview" | |
} | |
} | |
files = [ | |
{"file": "index.html", "data": code}, | |
{"file": "package.json", "data": json.dumps(package_json, indent=2)} | |
] | |
project_settings = { | |
"buildCommand": "npm run build", | |
"outputDirectory": "dist", | |
"installCommand": "npm install", | |
"framework": None | |
} | |
deploy_data = { | |
"name": project_name, | |
"files": files, | |
"target": "production", | |
"projectSettings": project_settings | |
} | |
r = requests.post(deploy_url, headers=headers, json=deploy_data) | |
if r.status_code != 200: | |
return {"status":"error","message":f"배포 실패: {r.text}"} | |
deployment_url = f"https://{project_name}.vercel.app" | |
time.sleep(5) # 실제 배포가 되기를 잠시 대기 | |
return {"status":"success","url":deployment_url,"project_name":project_name} | |
except Exception as e: | |
return {"status":"error","message":f"배포 중 오류: {e}"} | |
def handle_deploy(code, deploy_status): | |
if not code: | |
js_code = """ | |
<script> | |
showDeployBanner('error','⚠️ 배포 실패','배포할 코드가 없습니다. 먼저 게임 코드를 생성해주세요.',''); | |
document.getElementById('deploy-result-box').innerHTML = ` | |
<div class="deploy-error"> | |
<div class="error-icon">⚠️</div> | |
<div class="error-message">배포할 코드가 없습니다.</div> | |
</div> | |
`; | |
</script> | |
""" | |
return js_code, {"is_deployed":False,"status":"error","message":"코드 없음","url":""} | |
try: | |
loading_js = """ | |
<script> | |
showDeployBanner('loading','🔄 배포 진행 중','Vercel에 게임을 배포하고 있습니다... 잠시 기다려주세요.', ''); | |
document.getElementById('deploy-result-box').innerHTML = ` | |
<div class="deploy-loading"> | |
<div class="loading-spinner"></div> | |
<div class="loading-message">Vercel에 배포 중...</div> | |
</div> | |
`; | |
</script> | |
""" | |
yield loading_js, deploy_status | |
clean_code = remove_code_block(code) | |
result = deploy_to_vercel(clean_code) | |
if result.get("status")=="success": | |
url = result["url"] | |
success_js = f""" | |
<script> | |
showDeployBanner('success','✅ 배포 완료','게임이 성공적으로 배포되었습니다.','{url}'); | |
document.getElementById('deploy-result-box').innerHTML = ` | |
<div class="deploy-success"> | |
<div class="success-icon">✅</div> | |
<div>배포 성공!</div> | |
<div><a href="{url}" target="_blank">{url}</a></div> | |
</div> | |
`; | |
</script> | |
""" | |
return success_js, { | |
"is_deployed": True, | |
"status":"success","url":url,"message":"배포 완료" | |
} | |
else: | |
msg = result.get("message","알 수 없는 오류") | |
error_js = f""" | |
<script> | |
showDeployBanner('error','⚠️ 배포 실패','{msg}',''); | |
document.getElementById('deploy-result-box').innerHTML=` | |
<div class="deploy-error"> | |
<div class="error-icon">⚠️</div> | |
<div class="error-message">배포 실패: {msg}</div> | |
</div> | |
`; | |
</script> | |
""" | |
return error_js, {"is_deployed":False,"status":"error","url":"","message":msg} | |
except Exception as e: | |
err_msg = str(e) | |
exception_js = f""" | |
<script> | |
showDeployBanner('error','⚠️ 시스템 오류','{err_msg}',''); | |
document.getElementById('deploy-result-box').innerHTML=` | |
<div class="deploy-error"> | |
<div class="error-icon">⚠️</div> | |
<div class="error-message">시스템 오류: {err_msg}</div> | |
</div> | |
`; | |
</script> | |
""" | |
return exception_js, {"is_deployed":False,"status":"error","url":"","message":err_msg} | |
deploy_btn.click( | |
fn=handle_deploy, | |
inputs=[code_output, deploy_status], | |
outputs=[deploy_result_container, deploy_status] | |
) | |
if __name__ == "__main__": | |
try: | |
demo_instance = Demo() | |
demo.queue(default_concurrency_limit=20).launch(ssr_mode=False) | |
except Exception as e: | |
print(f"Initialization error: {e}") | |
raise | |