openfree commited on
Commit
a3481fa
·
verified ·
1 Parent(s): 7e41ff4

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +759 -0
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