openfree commited on
Commit
dc94023
·
verified ·
1 Parent(s): 60c86e7

Create app.py

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