Spaces:
Paused
Paused
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,759 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import random
|
4 |
+
import time
|
5 |
+
import html
|
6 |
+
import base64
|
7 |
+
import string
|
8 |
+
import json
|
9 |
+
import asyncio
|
10 |
+
import requests
|
11 |
+
import anthropic
|
12 |
+
import openai
|
13 |
+
|
14 |
+
from http import HTTPStatus
|
15 |
+
from typing import Dict, List, Optional, Tuple
|
16 |
+
from functools import partial
|
17 |
+
|
18 |
+
import gradio as gr
|
19 |
+
import modelscope_studio.components.base as ms
|
20 |
+
import modelscope_studio.components.legacy as legacy
|
21 |
+
import modelscope_studio.components.antd as antd
|
22 |
+
|
23 |
+
|
24 |
+
# ------------------------
|
25 |
+
# 1) DEMO_LIST 및 SystemPrompt
|
26 |
+
# ------------------------
|
27 |
+
|
28 |
+
DEMO_LIST = [
|
29 |
+
{"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요. 화살표 키로 조작하며, 가로줄이 채워지면 해당 줄이 제거되고 점수가 올라가는 메커니즘이 필요합니다. 난이도는 시간이 지날수록 블록이 빨라지도록 구현하고, 게임오버 조건과 점수 표시 기능을 포함해주세요."},
|
30 |
+
{"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요. 기본적인 체스 규칙(킹, 퀸, 룩, 비숍, 나이트, 폰의 이동 규칙)을 구현하고, 체크와 체크메이트 감지 기능이 필요합니다. 드래그 앤 드롭으로 말을 움직일 수 있게 하며, 이동 기록도 표시해주세요."},
|
31 |
+
# ...
|
32 |
+
]
|
33 |
+
|
34 |
+
SystemPrompt = """
|
35 |
+
# GameCraft 시스템 프롬프트
|
36 |
+
(중략 - 동일)
|
37 |
+
"""
|
38 |
+
|
39 |
+
# ------------------------
|
40 |
+
# 2) 공통 상수, 함수, 클래스
|
41 |
+
# ------------------------
|
42 |
+
|
43 |
+
class Role:
|
44 |
+
SYSTEM = "system"
|
45 |
+
USER = "user"
|
46 |
+
ASSISTANT = "assistant"
|
47 |
+
|
48 |
+
History = List[Tuple[str, str]]
|
49 |
+
Messages = List[Dict[str, str]]
|
50 |
+
|
51 |
+
IMAGE_CACHE = {}
|
52 |
+
|
53 |
+
def get_image_base64(image_path):
|
54 |
+
if image_path in IMAGE_CACHE:
|
55 |
+
return IMAGE_CACHE[image_path]
|
56 |
+
try:
|
57 |
+
with open(image_path, "rb") as image_file:
|
58 |
+
encoded_string = base64.b64encode(image_file.read()).decode()
|
59 |
+
IMAGE_CACHE[image_path] = encoded_string
|
60 |
+
return encoded_string
|
61 |
+
except:
|
62 |
+
return IMAGE_CACHE.get('default.png', '')
|
63 |
+
|
64 |
+
def history_to_messages(history: History, system: str) -> Messages:
|
65 |
+
messages = [{'role': Role.SYSTEM, 'content': system}]
|
66 |
+
for h in history:
|
67 |
+
messages.append({'role': Role.USER, 'content': h[0]})
|
68 |
+
messages.append({'role': Role.ASSISTANT, 'content': h[1]})
|
69 |
+
return messages
|
70 |
+
|
71 |
+
def messages_to_history(messages: Messages) -> History:
|
72 |
+
assert messages[0]['role'] == Role.SYSTEM
|
73 |
+
history = []
|
74 |
+
for q, r in zip(messages[1::2], messages[2::2]):
|
75 |
+
history.append([q['content'], r['content']])
|
76 |
+
return history
|
77 |
+
|
78 |
+
|
79 |
+
# ------------------------
|
80 |
+
# 3) API 연동 설정
|
81 |
+
# ------------------------
|
82 |
+
|
83 |
+
YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
|
84 |
+
YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
|
85 |
+
|
86 |
+
claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
|
87 |
+
openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
|
88 |
+
|
89 |
+
async def try_claude_api(system_message, claude_messages, timeout=15):
|
90 |
+
try:
|
91 |
+
system_message_with_limit = system_message + "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요..."
|
92 |
+
start_time = time.time()
|
93 |
+
with claude_client.messages.stream(
|
94 |
+
model="claude-3-7-sonnet-20250219",
|
95 |
+
max_tokens=19800,
|
96 |
+
system=system_message_with_limit,
|
97 |
+
messages=claude_messages,
|
98 |
+
temperature=0.3
|
99 |
+
) as stream:
|
100 |
+
collected_content = ""
|
101 |
+
for chunk in stream:
|
102 |
+
current_time = time.time()
|
103 |
+
if current_time - start_time > timeout:
|
104 |
+
raise TimeoutError("Claude API timeout")
|
105 |
+
if chunk.type == "content_block_delta":
|
106 |
+
collected_content += chunk.delta.text
|
107 |
+
yield collected_content
|
108 |
+
await asyncio.sleep(0)
|
109 |
+
start_time = current_time
|
110 |
+
except Exception as e:
|
111 |
+
raise e
|
112 |
+
|
113 |
+
async def try_openai_api(openai_messages):
|
114 |
+
try:
|
115 |
+
if openai_messages and openai_messages[0]["role"] == "system":
|
116 |
+
openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요..."
|
117 |
+
|
118 |
+
stream = openai_client.chat.completions.create(
|
119 |
+
model="o3",
|
120 |
+
messages=openai_messages,
|
121 |
+
stream=True,
|
122 |
+
max_tokens=19800,
|
123 |
+
temperature=0.2
|
124 |
+
)
|
125 |
+
collected_content = ""
|
126 |
+
for chunk in stream:
|
127 |
+
if chunk.choices[0].delta.content is not None:
|
128 |
+
collected_content += chunk.choices[0].delta.content
|
129 |
+
yield collected_content
|
130 |
+
except Exception as e:
|
131 |
+
raise e
|
132 |
+
|
133 |
+
|
134 |
+
# ------------------------
|
135 |
+
# 4) 템플릿(하나로 통합)
|
136 |
+
# ------------------------
|
137 |
+
|
138 |
+
def load_json_data():
|
139 |
+
data_list = [
|
140 |
+
{
|
141 |
+
"name": "[게임] 테트리스 클론",
|
142 |
+
"prompt": "블록이 위에서 떨어지는 클래식 테���리스 게임을 개발해주세요..."
|
143 |
+
},
|
144 |
+
# ...
|
145 |
+
]
|
146 |
+
return data_list
|
147 |
+
|
148 |
+
def create_template_html(title, items):
|
149 |
+
import html as html_lib
|
150 |
+
html_content = r"""
|
151 |
+
<style>
|
152 |
+
.prompt-grid {
|
153 |
+
display: grid;
|
154 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
155 |
+
gap: 16px;
|
156 |
+
padding: 12px;
|
157 |
+
}
|
158 |
+
.prompt-card {
|
159 |
+
background: white;
|
160 |
+
border: 1px solid #eee;
|
161 |
+
border-radius: 12px;
|
162 |
+
padding: 12px;
|
163 |
+
cursor: pointer;
|
164 |
+
box-shadow: 0 4px 8px rgba(0,0,0,0.05);
|
165 |
+
transition: all 0.3s ease;
|
166 |
+
}
|
167 |
+
.prompt-card:hover {
|
168 |
+
transform: translateY(-4px);
|
169 |
+
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
|
170 |
+
}
|
171 |
+
.card-name {
|
172 |
+
font-weight: bold;
|
173 |
+
margin-bottom: 8px;
|
174 |
+
font-size: 13px;
|
175 |
+
color: #444;
|
176 |
+
}
|
177 |
+
.card-prompt {
|
178 |
+
font-size: 11px;
|
179 |
+
line-height: 1.4;
|
180 |
+
color: #666;
|
181 |
+
display: -webkit-box;
|
182 |
+
-webkit-line-clamp: 7;
|
183 |
+
-webkit-box-orient: vertical;
|
184 |
+
overflow: hidden;
|
185 |
+
height: 84px;
|
186 |
+
background-color: #f8f9fa;
|
187 |
+
padding: 8px;
|
188 |
+
border-radius: 6px;
|
189 |
+
}
|
190 |
+
</style>
|
191 |
+
<div class="prompt-grid">
|
192 |
+
"""
|
193 |
+
for item in items:
|
194 |
+
card_html = f"""
|
195 |
+
<div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html_lib.escape(item.get('prompt', ''))}">
|
196 |
+
<div class="card-name">{html_lib.escape(item.get('name', ''))}</div>
|
197 |
+
<div class="card-prompt">{html_lib.escape(item.get('prompt', ''))}</div>
|
198 |
+
</div>
|
199 |
+
"""
|
200 |
+
html_content += card_html
|
201 |
+
html_content += r"""
|
202 |
+
<script>
|
203 |
+
function copyToInput(card) {
|
204 |
+
const prompt = card.dataset.prompt;
|
205 |
+
const textarea = document.querySelector('.ant-input-textarea-large textarea');
|
206 |
+
if (textarea) {
|
207 |
+
textarea.value = prompt;
|
208 |
+
textarea.dispatchEvent(new Event('input', { bubbles: true }));
|
209 |
+
document.querySelector('.session-drawer .close-btn').click();
|
210 |
+
}
|
211 |
+
}
|
212 |
+
</script>
|
213 |
+
</div>
|
214 |
+
"""
|
215 |
+
return gr.HTML(value=html_content)
|
216 |
+
|
217 |
+
def load_all_templates():
|
218 |
+
return create_template_html("🎮 모든 게임 템플릿", load_json_data())
|
219 |
+
|
220 |
+
# ------------------------
|
221 |
+
# 5) 배포/부스트/기타 유틸
|
222 |
+
# ------------------------
|
223 |
+
|
224 |
+
def remove_code_block(text):
|
225 |
+
pattern = r'```html\s*([\s\S]+?)\s*```'
|
226 |
+
match = re.search(pattern, text, re.DOTALL)
|
227 |
+
if match:
|
228 |
+
return match.group(1).strip()
|
229 |
+
|
230 |
+
pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
|
231 |
+
match = re.search(pattern, text, re.DOTALL)
|
232 |
+
if match:
|
233 |
+
return match.group(1).strip()
|
234 |
+
|
235 |
+
text = re.sub(r'```html\s*', '', text)
|
236 |
+
text = re.sub(r'\s*```', '', text)
|
237 |
+
return text.strip()
|
238 |
+
|
239 |
+
def optimize_code(code: str) -> str:
|
240 |
+
if not code or len(code.strip()) == 0:
|
241 |
+
return code
|
242 |
+
lines = code.split('\n')
|
243 |
+
if len(lines) <= 200:
|
244 |
+
return code
|
245 |
+
|
246 |
+
comment_patterns = [
|
247 |
+
r'/\*[\s\S]*?\*/',
|
248 |
+
r'//.*?$',
|
249 |
+
r'<!--[\s\S]*?-->'
|
250 |
+
]
|
251 |
+
cleaned_code = code
|
252 |
+
for pattern in comment_patterns:
|
253 |
+
cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE)
|
254 |
+
|
255 |
+
cleaned_lines = []
|
256 |
+
empty_line_count = 0
|
257 |
+
for line in cleaned_code.split('\n'):
|
258 |
+
if line.strip() == '':
|
259 |
+
empty_line_count += 1
|
260 |
+
if empty_line_count <= 1:
|
261 |
+
cleaned_lines.append('')
|
262 |
+
else:
|
263 |
+
empty_line_count = 0
|
264 |
+
cleaned_lines.append(line)
|
265 |
+
cleaned_code = '\n'.join(cleaned_lines)
|
266 |
+
cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE)
|
267 |
+
cleaned_code = re.sub(r' {2,}', ' ', cleaned_code)
|
268 |
+
return cleaned_code
|
269 |
+
|
270 |
+
def send_to_sandbox(code):
|
271 |
+
clean_code = remove_code_block(code)
|
272 |
+
clean_code = optimize_code(clean_code)
|
273 |
+
if clean_code.startswith('```html'):
|
274 |
+
clean_code = clean_code[7:].strip()
|
275 |
+
if clean_code.endswith('```'):
|
276 |
+
clean_code = clean_code[:-3].strip()
|
277 |
+
|
278 |
+
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
|
279 |
+
clean_code = f"""<!DOCTYPE html>
|
280 |
+
<html>
|
281 |
+
<head>
|
282 |
+
<meta charset="UTF-8">
|
283 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
284 |
+
<title>Game Preview</title>
|
285 |
+
</head>
|
286 |
+
<body>
|
287 |
+
{clean_code}
|
288 |
+
</body>
|
289 |
+
</html>"""
|
290 |
+
encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
|
291 |
+
data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
|
292 |
+
return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>'
|
293 |
+
|
294 |
+
def boost_prompt(prompt: str) -> str:
|
295 |
+
if not prompt:
|
296 |
+
return ""
|
297 |
+
# (생략)
|
298 |
+
# 이 부분은 동일
|
299 |
+
|
300 |
+
def handle_boost(prompt: str):
|
301 |
+
try:
|
302 |
+
boosted_prompt = boost_prompt(prompt)
|
303 |
+
return boosted_prompt, gr.update(active_key="empty")
|
304 |
+
except Exception:
|
305 |
+
return prompt, gr.update(active_key="empty")
|
306 |
+
|
307 |
+
def history_render(history: History):
|
308 |
+
return gr.update(open=True), history
|
309 |
+
|
310 |
+
def execute_code(query: str):
|
311 |
+
if not query or query.strip() == '':
|
312 |
+
return None, gr.update(active_key="empty")
|
313 |
+
try:
|
314 |
+
clean_code = remove_code_block(query)
|
315 |
+
if clean_code.startswith('```html'):
|
316 |
+
clean_code = clean_code[7:].strip()
|
317 |
+
if clean_code.endswith('```'):
|
318 |
+
clean_code = clean_code[:-3].strip()
|
319 |
+
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
|
320 |
+
if not ('<body' in clean_code and '</body>' in clean_code):
|
321 |
+
clean_code = f"""<!DOCTYPE html>
|
322 |
+
<html>
|
323 |
+
<head>
|
324 |
+
<meta charset="UTF-8">
|
325 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
326 |
+
<title>Game Preview</title>
|
327 |
+
</head>
|
328 |
+
<body>
|
329 |
+
{clean_code}
|
330 |
+
</body>
|
331 |
+
</html>"""
|
332 |
+
return send_to_sandbox(clean_code), gr.update(active_key="render")
|
333 |
+
except Exception as e:
|
334 |
+
print(f"Execute code error: {str(e)}")
|
335 |
+
return None, gr.update(active_key="empty")
|
336 |
+
|
337 |
+
|
338 |
+
# ------------------------
|
339 |
+
# 6) 데모 클래스
|
340 |
+
# ------------------------
|
341 |
+
|
342 |
+
class Demo:
|
343 |
+
def __init__(self):
|
344 |
+
pass
|
345 |
+
|
346 |
+
async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
|
347 |
+
if not query or query.strip() == '':
|
348 |
+
query = random.choice(DEMO_LIST)['description']
|
349 |
+
|
350 |
+
if _history is None:
|
351 |
+
_history = []
|
352 |
+
|
353 |
+
query = f"""
|
354 |
+
다음 게임을 제작해주세요.
|
355 |
+
중요 요구사항:
|
356 |
+
1. 코드는 가능한 한 간결하게 작성할 것
|
357 |
+
2. 불필요한 주석이나 설명은 제외할 것
|
358 |
+
3. 코드는 600줄을 넘지 않을 것
|
359 |
+
4. 모든 코드는 하나의 HTML 파일에 통합할 것
|
360 |
+
5. 핵심 기능만 구현하고 부가 기능은 생략할 것
|
361 |
+
게임 요청: {query}
|
362 |
+
"""
|
363 |
+
messages = history_to_messages(_history, _setting['system'])
|
364 |
+
system_message = messages[0]['content']
|
365 |
+
|
366 |
+
claude_messages = [
|
367 |
+
{"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
|
368 |
+
for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
|
369 |
+
if msg["content"].strip() != ''
|
370 |
+
]
|
371 |
+
|
372 |
+
openai_messages = [{"role": "system", "content": system_message}]
|
373 |
+
for msg in messages[1:]:
|
374 |
+
openai_messages.append({"role": msg["role"], "content": msg["content"]})
|
375 |
+
openai_messages.append({"role": "user", "content": query})
|
376 |
+
|
377 |
+
try:
|
378 |
+
yield [
|
379 |
+
"Generating code...",
|
380 |
+
_history,
|
381 |
+
None,
|
382 |
+
gr.update(active_key="loading"),
|
383 |
+
gr.update(open=True)
|
384 |
+
]
|
385 |
+
await asyncio.sleep(0)
|
386 |
+
collected_content = None
|
387 |
+
try:
|
388 |
+
async for content in try_claude_api(system_message, claude_messages):
|
389 |
+
yield [
|
390 |
+
content,
|
391 |
+
_history,
|
392 |
+
None,
|
393 |
+
gr.update(active_key="loading"),
|
394 |
+
gr.update(open=True)
|
395 |
+
]
|
396 |
+
await asyncio.sleep(0)
|
397 |
+
collected_content = content
|
398 |
+
except Exception:
|
399 |
+
async for content in try_openai_api(openai_messages):
|
400 |
+
yield [
|
401 |
+
content,
|
402 |
+
_history,
|
403 |
+
None,
|
404 |
+
gr.update(active_key="loading"),
|
405 |
+
gr.update(open=True)
|
406 |
+
]
|
407 |
+
await asyncio.sleep(0)
|
408 |
+
collected_content = content
|
409 |
+
|
410 |
+
if collected_content:
|
411 |
+
clean_code = remove_code_block(collected_content)
|
412 |
+
code_lines = clean_code.count('\n') + 1
|
413 |
+
if code_lines > 700:
|
414 |
+
warning_msg = f"""
|
415 |
+
⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄)** ...
|
416 |
+
"""
|
417 |
+
yield [warning_msg, _history, None, gr.update(active_key="empty"), gr.update(open=True)]
|
418 |
+
else:
|
419 |
+
_history = messages_to_history([
|
420 |
+
{'role': Role.SYSTEM, 'content': system_message}
|
421 |
+
] + claude_messages + [{
|
422 |
+
'role': Role.ASSISTANT, 'content': collected_content
|
423 |
+
}])
|
424 |
+
yield [
|
425 |
+
collected_content,
|
426 |
+
_history,
|
427 |
+
send_to_sandbox(clean_code),
|
428 |
+
gr.update(active_key="render"),
|
429 |
+
gr.update(open=True)
|
430 |
+
]
|
431 |
+
else:
|
432 |
+
raise ValueError("No content was generated from either API")
|
433 |
+
except Exception as e:
|
434 |
+
raise ValueError(f'Error calling APIs: {str(e)}')
|
435 |
+
|
436 |
+
def clear_history(self):
|
437 |
+
return []
|
438 |
+
|
439 |
+
|
440 |
+
# ------------------------
|
441 |
+
# 7) Gradio / Modelscope UI 빌드
|
442 |
+
# ------------------------
|
443 |
+
|
444 |
+
demo_instance = Demo()
|
445 |
+
theme = gr.themes.Soft(
|
446 |
+
primary_hue="blue",
|
447 |
+
secondary_hue="purple",
|
448 |
+
neutral_hue="slate",
|
449 |
+
spacing_size=gr.themes.sizes.spacing_md,
|
450 |
+
radius_size=gr.themes.sizes.radius_md,
|
451 |
+
text_size=gr.themes.sizes.text_md,
|
452 |
+
)
|
453 |
+
|
454 |
+
with gr.Blocks(css_paths=["app.css"], theme=theme) as demo:
|
455 |
+
# 헤더 영역 (스크립트 제거 - 순수 HTML만 남김)
|
456 |
+
header_html = gr.HTML("""
|
457 |
+
<div class="app-header">
|
458 |
+
<h1>🎮 Vibe Game Craft</h1>
|
459 |
+
<p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 직관적인 인터페이스로 쉽게 게임을 만들고, 실시간으로 미리보기를 확인하세요.</p>
|
460 |
+
</div>
|
461 |
+
""")
|
462 |
+
|
463 |
+
history = gr.State([])
|
464 |
+
setting = gr.State({"system": SystemPrompt})
|
465 |
+
|
466 |
+
deploy_status = gr.State({
|
467 |
+
"is_deployed": False,
|
468 |
+
"status": "",
|
469 |
+
"url": "",
|
470 |
+
"message": ""
|
471 |
+
})
|
472 |
+
|
473 |
+
with ms.Application() as app:
|
474 |
+
with antd.ConfigProvider():
|
475 |
+
|
476 |
+
# code Drawer
|
477 |
+
with antd.Drawer(open=False, title="코드 보기", placement="left", width="750px") as code_drawer:
|
478 |
+
code_output = legacy.Markdown()
|
479 |
+
|
480 |
+
# history Drawer
|
481 |
+
with antd.Drawer(open=False, title="히스토리", placement="left", width="900px") as history_drawer:
|
482 |
+
history_output = legacy.Chatbot(
|
483 |
+
show_label=False, flushing=False, height=960, elem_classes="history_chatbot"
|
484 |
+
)
|
485 |
+
|
486 |
+
# templates Drawer
|
487 |
+
with antd.Drawer(
|
488 |
+
open=False,
|
489 |
+
title="게임 템플릿",
|
490 |
+
placement="right",
|
491 |
+
width="900px",
|
492 |
+
elem_classes="session-drawer"
|
493 |
+
) as session_drawer:
|
494 |
+
with antd.Flex(vertical=True, gap="middle"):
|
495 |
+
gr.Markdown("### 사용 가능한 게임 템플릿")
|
496 |
+
session_history = gr.HTML(elem_classes="session-history")
|
497 |
+
close_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
|
498 |
+
|
499 |
+
with antd.Row(gutter=[32, 12], align="top", elem_classes="equal-height-container") as layout:
|
500 |
+
|
501 |
+
# 좌측 미리보기
|
502 |
+
with antd.Col(span=24, md=16, elem_classes="equal-height-col"):
|
503 |
+
with ms.Div(elem_classes="right_panel panel"):
|
504 |
+
gr.HTML(r"""
|
505 |
+
<div class="render_header">
|
506 |
+
<span class="header_btn"></span>
|
507 |
+
<span class="header_btn"></span>
|
508 |
+
<span class="header_btn"></span>
|
509 |
+
</div>
|
510 |
+
""")
|
511 |
+
with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
|
512 |
+
with antd.Tabs.Item(key="empty"):
|
513 |
+
empty = antd.Empty(description="게임을 만들려면 설명을 입력하세요", elem_classes="right_content")
|
514 |
+
with antd.Tabs.Item(key="loading"):
|
515 |
+
loading = antd.Spin(
|
516 |
+
True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content"
|
517 |
+
)
|
518 |
+
with antd.Tabs.Item(key="render"):
|
519 |
+
sandbox = gr.HTML(elem_classes="html_content")
|
520 |
+
|
521 |
+
# 우측 입력/버튼/배포
|
522 |
+
with antd.Col(span=24, md=8, elem_classes="equal-height-col"):
|
523 |
+
with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"):
|
524 |
+
with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"):
|
525 |
+
codeBtn = antd.Button("🧑💻 코드 보기", type="default", elem_classes="code-btn")
|
526 |
+
historyBtn = antd.Button("📜 히스토리", type="default", elem_classes="history-btn")
|
527 |
+
template_btn = antd.Button("🎮 템플릿", type="default", elem_classes="template-btn")
|
528 |
+
|
529 |
+
with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"):
|
530 |
+
btn = antd.Button("전송", type="primary", size="large", elem_classes="send-btn")
|
531 |
+
boost_btn = antd.Button("증강", type="default", size="large", elem_classes="boost-btn")
|
532 |
+
execute_btn = antd.Button("코드", type="default", size="large", elem_classes="execute-btn")
|
533 |
+
deploy_btn = antd.Button("배포", type="default", size="large", elem_classes="deploy-btn")
|
534 |
+
clear_btn = antd.Button("클리어", type="default", size="large", elem_classes="clear-btn")
|
535 |
+
|
536 |
+
with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"):
|
537 |
+
input_text = antd.InputTextarea(
|
538 |
+
size="large",
|
539 |
+
allow_clear=True,
|
540 |
+
placeholder=random.choice(DEMO_LIST)['description'],
|
541 |
+
max_length=100000
|
542 |
+
)
|
543 |
+
gr.HTML('<div class="help-text">💡 원하는 게임의 설명을 입력하세요. 예: "테트리스 게임 제작해줘."</div>')
|
544 |
+
|
545 |
+
# 순수 HTML만 넣을 컨테이너
|
546 |
+
deploy_result_container = gr.HTML("""
|
547 |
+
<div class="deploy-section">
|
548 |
+
<div class="deploy-header">📤 배포 결과</div>
|
549 |
+
<div id="deploy-result-box" class="deploy-result-box">
|
550 |
+
<div class="no-deploy">아직 배포된 게임이 없습니다.</div>
|
551 |
+
</div>
|
552 |
+
</div>
|
553 |
+
""")
|
554 |
+
|
555 |
+
# 더 이상 자바스크립트 삽입 없음
|
556 |
+
js_trigger = gr.HTML(elem_id="js-trigger", visible=False)
|
557 |
+
|
558 |
+
# ─── 버튼 / Drawer 이벤트 ───
|
559 |
+
codeBtn.click(
|
560 |
+
lambda: gr.update(open=True),
|
561 |
+
inputs=[],
|
562 |
+
outputs=[code_drawer]
|
563 |
+
)
|
564 |
+
code_drawer.close(
|
565 |
+
lambda: gr.update(open=False),
|
566 |
+
inputs=[],
|
567 |
+
outputs=[code_drawer]
|
568 |
+
)
|
569 |
+
|
570 |
+
historyBtn.click(
|
571 |
+
history_render,
|
572 |
+
inputs=[history],
|
573 |
+
outputs=[history_drawer, history_output]
|
574 |
+
)
|
575 |
+
history_drawer.close(
|
576 |
+
lambda: gr.update(open=False),
|
577 |
+
inputs=[],
|
578 |
+
outputs=[history_drawer]
|
579 |
+
)
|
580 |
+
|
581 |
+
template_btn.click(
|
582 |
+
fn=lambda: (gr.update(open=True), load_all_templates()),
|
583 |
+
outputs=[session_drawer, session_history],
|
584 |
+
queue=False
|
585 |
+
)
|
586 |
+
session_drawer.close(
|
587 |
+
lambda: (gr.update(open=False), gr.HTML("")),
|
588 |
+
outputs=[session_drawer, session_history]
|
589 |
+
)
|
590 |
+
close_btn.click(
|
591 |
+
lambda: (gr.update(open=False), gr.HTML("")),
|
592 |
+
outputs=[session_drawer, session_history]
|
593 |
+
)
|
594 |
+
|
595 |
+
btn.click(
|
596 |
+
demo_instance.generation_code,
|
597 |
+
inputs=[input_text, setting, history],
|
598 |
+
outputs=[code_output, history, sandbox, state_tab, code_drawer]
|
599 |
+
)
|
600 |
+
|
601 |
+
clear_btn.click(
|
602 |
+
demo_instance.clear_history,
|
603 |
+
inputs=[],
|
604 |
+
outputs=[history]
|
605 |
+
)
|
606 |
+
|
607 |
+
boost_btn.click(
|
608 |
+
fn=handle_boost,
|
609 |
+
inputs=[input_text],
|
610 |
+
outputs=[input_text, state_tab]
|
611 |
+
)
|
612 |
+
|
613 |
+
execute_btn.click(
|
614 |
+
fn=execute_code,
|
615 |
+
inputs=[input_text],
|
616 |
+
outputs=[sandbox, state_tab]
|
617 |
+
)
|
618 |
+
|
619 |
+
def deploy_to_vercel(code: str):
|
620 |
+
"""
|
621 |
+
배포 로직 (API 호출) - 스크립트 없이
|
622 |
+
"""
|
623 |
+
try:
|
624 |
+
token = "A8IFZmgW2cqA4yUNlLPnci0N" # 실제 토큰
|
625 |
+
if not token:
|
626 |
+
return {"status": "error", "message": "Vercel 토큰이 설정되지 않았습니다."}
|
627 |
+
|
628 |
+
project_name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
|
629 |
+
deploy_url = "https://api.vercel.com/v13/deployments"
|
630 |
+
headers = {
|
631 |
+
"Authorization": f"Bearer {token}",
|
632 |
+
"Content-Type": "application/json"
|
633 |
+
}
|
634 |
+
package_json = {
|
635 |
+
"name": project_name,
|
636 |
+
"version": "1.0.0",
|
637 |
+
"private": True,
|
638 |
+
"dependencies": {"vite": "^5.0.0"},
|
639 |
+
"scripts": {
|
640 |
+
"dev": "vite",
|
641 |
+
"build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/",
|
642 |
+
"preview": "vite preview"
|
643 |
+
}
|
644 |
+
}
|
645 |
+
files = [
|
646 |
+
{"file": "index.html", "data": code},
|
647 |
+
{"file": "package.json", "data": json.dumps(package_json, indent=2)}
|
648 |
+
]
|
649 |
+
project_settings = {
|
650 |
+
"buildCommand": "npm run build",
|
651 |
+
"outputDirectory": "dist",
|
652 |
+
"installCommand": "npm install",
|
653 |
+
"framework": None
|
654 |
+
}
|
655 |
+
deploy_data = {
|
656 |
+
"name": project_name,
|
657 |
+
"files": files,
|
658 |
+
"target": "production",
|
659 |
+
"projectSettings": project_settings
|
660 |
+
}
|
661 |
+
deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data)
|
662 |
+
if deploy_response.status_code != 200:
|
663 |
+
return {"status": "error", "message": f"배포 실패: {deploy_response.text}"}
|
664 |
+
|
665 |
+
deployment_url = f"https://{project_name}.vercel.app"
|
666 |
+
time.sleep(5)
|
667 |
+
|
668 |
+
return {
|
669 |
+
"status": "success",
|
670 |
+
"url": deployment_url,
|
671 |
+
"project_name": project_name
|
672 |
+
}
|
673 |
+
except Exception as e:
|
674 |
+
return {"status": "error", "message": f"배포 중 오류 발생: {str(e)}"}
|
675 |
+
|
676 |
+
# ─── 스크립트 없이 순수 HTML만 반환하는 handle_deploy ───
|
677 |
+
def handle_deploy(code, deploy_status):
|
678 |
+
if not code:
|
679 |
+
# 코드가 없으면 바로 실패
|
680 |
+
error_html = """
|
681 |
+
<div style="border-left:5px solid #ff3b30; padding:10px; background:#ffecec; margin-top:10px;">
|
682 |
+
<strong>⚠️ 배포 실패</strong><br>
|
683 |
+
배포할 코드가 없습니다. 먼저 게임 코드를 생성해주세요.
|
684 |
+
</div>
|
685 |
+
"""
|
686 |
+
return error_html, {
|
687 |
+
"is_deployed": False,
|
688 |
+
"status": "error",
|
689 |
+
"message": "배포할 코드가 없습니다.",
|
690 |
+
"url": ""
|
691 |
+
}
|
692 |
+
|
693 |
+
try:
|
694 |
+
# 배포 실행
|
695 |
+
clean_code = remove_code_block(code)
|
696 |
+
result = deploy_to_vercel(clean_code)
|
697 |
+
|
698 |
+
if result.get("status") == "success":
|
699 |
+
# 성공 시 URL을 순수 HTML로 표시
|
700 |
+
url = result["url"]
|
701 |
+
success_html = f"""
|
702 |
+
<div style="border-left:5px solid #34c759; padding:10px; background:#e8f5e9; margin-top:10px;">
|
703 |
+
<strong>✅ 배포 완료!</strong><br>
|
704 |
+
<p>아래 주소에서 게임을 확인할 수 있습니다:</p>
|
705 |
+
<a href="{url}" target="_blank" style="color:#1976d2;">{url}</a>
|
706 |
+
</div>
|
707 |
+
"""
|
708 |
+
return success_html, {
|
709 |
+
"is_deployed": True,
|
710 |
+
"status": "success",
|
711 |
+
"url": url,
|
712 |
+
"message": "배포 완료!"
|
713 |
+
}
|
714 |
+
else:
|
715 |
+
error_msg = result.get("message", "알 수 없는 오류")
|
716 |
+
error_html = f"""
|
717 |
+
<div style="border-left:5px solid #ff3b30; padding:10px; background:#ffecec; margin-top:10px;">
|
718 |
+
<strong>⚠️ 배포 실패</strong><br>{error_msg}
|
719 |
+
</div>
|
720 |
+
"""
|
721 |
+
return error_html, {
|
722 |
+
"is_deployed": False,
|
723 |
+
"status": "error",
|
724 |
+
"message": error_msg,
|
725 |
+
"url": ""
|
726 |
+
}
|
727 |
+
except Exception as e:
|
728 |
+
# 시스템 오류
|
729 |
+
err = str(e)
|
730 |
+
error_html = f"""
|
731 |
+
<div style="border-left:5px solid #ff3b30; padding:10px; background:#ffecec; margin-top:10px;">
|
732 |
+
<strong>⚠️ 시스템 오류</strong><br>{err}
|
733 |
+
</div>
|
734 |
+
"""
|
735 |
+
return error_html, {
|
736 |
+
"is_deployed": False,
|
737 |
+
"status": "error",
|
738 |
+
"message": err,
|
739 |
+
"url": ""
|
740 |
+
}
|
741 |
+
|
742 |
+
deploy_btn.click(
|
743 |
+
fn=handle_deploy,
|
744 |
+
inputs=[code_output, deploy_status],
|
745 |
+
outputs=[deploy_result_container, deploy_status]
|
746 |
+
)
|
747 |
+
|
748 |
+
|
749 |
+
# ------------------------
|
750 |
+
# 8) 실제 실행
|
751 |
+
# ------------------------
|
752 |
+
|
753 |
+
if __name__ == "__main__":
|
754 |
+
try:
|
755 |
+
demo_instance = Demo()
|
756 |
+
demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
|
757 |
+
except Exception as e:
|
758 |
+
print(f"Initialization error: {e}")
|
759 |
+
raise
|