openfree commited on
Commit
0a93e2d
·
verified ·
1 Parent(s): cf995f4

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +358 -582
app.py CHANGED
@@ -1,63 +1,26 @@
1
  # app.py
2
 
3
- ############################################
4
- # (1) HTML을 그대로 남기고 싶다면, 아래와 같이 멀티라인 문자열로 묶어두고
5
- # 파이썬 실행에 영향을 주지 않도록 처리합니다.
6
- ############################################
7
-
8
- html_header_for_reference = r"""
9
- <!DOCTYPE html>
10
- <html>
11
- <head>
12
- <style>
13
- /*
14
- We'll keep minimal direct CSS here,
15
- the rest will be handled in the Python script or internal Gradio style.
16
- */
17
- </style>
18
- </head>
19
- <body>
20
-
21
- <!--
22
- NOTE:
23
- 1. Removed the "좌측 상단 박스" and replaced it with option selectors at the top.
24
- 2. Added four categories: 난이도, 게임 유형, 그래픽 스타일, 시점 뷰 as radio groups.
25
- 3. The rest of the logic remains nearly identical, just reorganized in the layout.
26
- 4. Code is wrapped in triple backticks with "html" as requested.
27
- -->
28
-
29
- <!-- for reference only, not used directly in code -->
30
-
31
- </body>
32
- </html>
33
- """
34
-
35
- ############################################
36
- # (2) 이제부터는 실제 파이썬 코드 (Gradio 앱)입니다.
37
- ############################################
38
-
39
  import os
40
  import re
41
  import random
42
- from http import HTTPStatus
43
  from typing import Dict, List, Optional, Tuple
44
  import base64
45
  import anthropic
46
  import openai
47
  import asyncio
48
  import time
49
- from functools import partial
50
  import json
 
 
 
51
  import gradio as gr
52
  import modelscope_studio.components.base as ms
53
  import modelscope_studio.components.legacy as legacy
54
  import modelscope_studio.components.antd as antd
55
 
56
- import html
57
- import urllib.parse
58
- from huggingface_hub import HfApi, create_repo
59
- import string
60
- import requests
61
 
62
  DEMO_LIST = [
63
  {"description": "Create a Tetris-like puzzle game with arrow key controls, line-clearing mechanics, and increasing difficulty levels."},
@@ -131,6 +94,10 @@ Messages = List[Dict[str, str]]
131
  IMAGE_CACHE = {}
132
 
133
  def get_image_base64(image_path):
 
 
 
 
134
  if image_path in IMAGE_CACHE:
135
  return IMAGE_CACHE[image_path]
136
  try:
@@ -139,7 +106,8 @@ def get_image_base64(image_path):
139
  IMAGE_CACHE[image_path] = encoded_string
140
  return encoded_string
141
  except:
142
- return IMAGE_CACHE.get('default.png', '')
 
143
 
144
  def history_to_messages(history: History, system: str) -> Messages:
145
  messages = [{'role': Role.SYSTEM, 'content': system}]
@@ -151,6 +119,8 @@ def history_to_messages(history: History, system: str) -> Messages:
151
  def messages_to_history(messages: Messages) -> History:
152
  assert messages[0]['role'] == Role.SYSTEM
153
  history = []
 
 
154
  for q, r in zip(messages[1::2], messages[2::2]):
155
  history.append([q['content'], r['content']])
156
  return history
@@ -158,10 +128,14 @@ def messages_to_history(messages: Messages) -> History:
158
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
159
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
160
 
 
161
  claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
162
  openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
163
 
164
  async def try_claude_api(system_message, claude_messages, timeout=15):
 
 
 
165
  try:
166
  start_time = time.time()
167
  with claude_client.messages.stream(
@@ -185,9 +159,12 @@ async def try_claude_api(system_message, claude_messages, timeout=15):
185
  raise e
186
 
187
  async def try_openai_api(openai_messages):
 
 
 
188
  try:
189
  stream = openai_client.chat.completions.create(
190
- model="gpt-4o",
191
  messages=openai_messages,
192
  stream=True,
193
  max_tokens=4096,
@@ -202,96 +179,11 @@ async def try_openai_api(openai_messages):
202
  print(f"OpenAI API error: {str(e)}")
203
  raise e
204
 
205
- class Demo:
206
- def __init__(self):
207
- pass
208
-
209
- async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
210
- if not query or query.strip() == '':
211
- query = random.choice(DEMO_LIST)['description']
212
-
213
- if _history is None:
214
- _history = []
215
-
216
- messages = history_to_messages(_history, _setting['system'])
217
- system_message = messages[0]['content']
218
-
219
- claude_messages = [
220
- {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
221
- for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
222
- if msg["content"].strip() != ''
223
- ]
224
-
225
- openai_messages = [{"role": "system", "content": system_message}]
226
- for msg in messages[1:]:
227
- openai_messages.append({
228
- "role": msg["role"],
229
- "content": msg["content"]
230
- })
231
- openai_messages.append({"role": "user", "content": query})
232
-
233
- try:
234
- yield [
235
- "Generating code...",
236
- _history,
237
- None,
238
- gr.update(active_key="loading"),
239
- gr.update(open=True)
240
- ]
241
- await asyncio.sleep(0)
242
-
243
- collected_content = None
244
- try:
245
- async for content in try_claude_api(system_message, claude_messages, timeout=15):
246
- yield [
247
- content,
248
- _history,
249
- None,
250
- gr.update(active_key="loading"),
251
- gr.update(open=True)
252
- ]
253
- await asyncio.sleep(0)
254
- collected_content = content
255
-
256
- except Exception as claude_error:
257
- print(f"Falling back to OpenAI API due to Claude error: {str(claude_error)}")
258
- async for content in try_openai_api(openai_messages):
259
- yield [
260
- content,
261
- _history,
262
- None,
263
- gr.update(active_key="loading"),
264
- gr.update(open=True)
265
- ]
266
- await asyncio.sleep(0)
267
- collected_content = content
268
-
269
- if collected_content:
270
- _history = messages_to_history([
271
- {'role': Role.SYSTEM, 'content': system_message}
272
- ] + claude_messages + [{
273
- 'role': Role.ASSISTANT,
274
- 'content': collected_content
275
- }])
276
-
277
- yield [
278
- collected_content,
279
- _history,
280
- send_to_sandbox(remove_code_block(collected_content)),
281
- gr.update(active_key="render"),
282
- gr.update(open=True)
283
- ]
284
- else:
285
- raise ValueError("No content was generated from either API")
286
-
287
- except Exception as e:
288
- print(f"Error details: {str(e)}")
289
- raise ValueError(f'Error calling APIs: {str(e)}')
290
-
291
- def clear_history(self):
292
- return []
293
-
294
  def remove_code_block(text):
 
 
 
 
295
  pattern = r'```html\n(.+?)\n```'
296
  match = re.search(pattern, text, re.DOTALL)
297
  if match:
@@ -299,18 +191,34 @@ def remove_code_block(text):
299
  else:
300
  return text.strip()
301
 
302
- def history_render(history: History):
303
- return gr.update(open=True), history
304
-
305
  def send_to_sandbox(code):
 
 
 
306
  encoded_html = base64.b64encode(code.encode('utf-8')).decode('utf-8')
307
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
308
  return f"<iframe src=\"{data_uri}\" width=\"100%\" height=\"920px\"></iframe>"
309
 
 
 
 
 
 
 
310
  theme = gr.themes.Soft()
311
 
 
 
 
 
312
  def load_json_data():
313
- return [
 
 
 
 
 
 
314
  {
315
  "name": "[게임] 테트리스 클론",
316
  "image_url": "data:image/png;base64," + get_image_base64('tetris.png'),
@@ -321,215 +229,49 @@ def load_json_data():
321
  "image_url": "data:image/png;base64," + get_image_base64('chess.png'),
322
  "prompt": "Build an interactive Chess game with a basic AI opponent and drag-and-drop piece movement. Keep track of moves and detect check/checkmate."
323
  },
324
- {
325
- "name": "[게임] 카드 매칭 게임",
326
- "image_url": "data:image/png;base64," + get_image_base64('memory.png'),
327
- "prompt": "Design a memory matching card game with flip animations, scoring system, and multiple difficulty levels."
328
- },
329
- {
330
- "name": "[게임] 슈팅 게임 (Space Shooter)",
331
- "image_url": "data:image/png;base64," + get_image_base64('spaceshooter.png'),
332
- "prompt": "Create a space shooter game with enemy waves, collision detection, and power-ups. Use keyboard or mouse controls for ship movement."
333
- },
334
- {
335
- "name": "[게임] 슬라이드 퍼즐",
336
- "image_url": "data:image/png;base64," + get_image_base64('slidepuzzle.png'),
337
- "prompt": "Implement a slide puzzle game using images or numbers. Include shuffle functionality, move counter, and difficulty settings."
338
- },
339
- {
340
- "name": "[게임] 뱀 게임 (Snake)",
341
- "image_url": "data:image/png;base64," + get_image_base64('snake.png'),
342
- "prompt": "Implement the classic Snake game with grid-based movement, score tracking, and increasing speed. Use arrow keys for control."
343
- },
344
- {
345
- "name": "[게임] 브레이크아웃 (벽돌깨기)",
346
- "image_url": "data:image/png;base64," + get_image_base64('breakout.png'),
347
- "prompt": "Build a classic breakout game with paddle, ball, and bricks. Increase ball speed and track lives/score."
348
- },
349
- {
350
- "name": "[게임] 타워 디펜스",
351
- "image_url": "data:image/png;base64," + get_image_base64('towerdefense.png'),
352
- "prompt": "Create a tower defense game with multiple tower types and enemy waves. Include an upgrade system and resource management."
353
- },
354
- {
355
- "name": "[게임] 런닝 점프 (Endless Runner)",
356
- "image_url": "data:image/png;base64," + get_image_base64('runner.png'),
357
- "prompt": "Design an endless runner with side-scrolling obstacles. Use keyboard or mouse to jump and avoid collisions."
358
- },
359
- {
360
- "name": "[게임] 플랫포머 (Platformer)",
361
- "image_url": "data:image/png;base64," + get_image_base64('platformer.png'),
362
- "prompt": "Implement a platformer game with character movement, jumping, and collectible items. Use arrow keys for control."
363
- },
364
- {
365
- "name": "[게임] 미로 찾기 (Maze)",
366
- "image_url": "data:image/png;base64," + get_image_base64('maze.png'),
367
- "prompt": "Generate a random maze and allow the player to navigate from start to finish. Include a timer and pathfinding animations."
368
- },
369
- {
370
- "name": "[게임] 미션 RPG",
371
- "image_url": "data:image/png;base64," + get_image_base64('rpg.png'),
372
- "prompt": "Build a simple top-down RPG with tile-based movement, monsters, and loot. Use arrow keys for movement and track player stats."
373
- },
374
- {
375
- "name": "[게임] Match-3 퍼즐",
376
- "image_url": "data:image/png;base64," + get_image_base64('match3.png'),
377
- "prompt": "Create a match-3 puzzle game with swipe-based mechanics, special tiles, and combo scoring."
378
- },
379
- {
380
- "name": "[게임] 하늘 나는 새 (Flappy Bird)",
381
- "image_url": "data:image/png;base64," + get_image_base64('flappy.png'),
382
- "prompt": "Implement a Flappy Bird clone with space bar or mouse click to flap, randomized pipe positions, and score tracking."
383
- },
384
- {
385
- "name": "[게임] 그림 찾기 (Spot the Difference)",
386
- "image_url": "data:image/png;base64," + get_image_base64('spotdiff.png'),
387
- "prompt": "Build a spot-the-difference game using pairs of similar images. Track remaining differences and time limit."
388
- },
389
- {
390
- "name": "[게임] 타이핑 게임",
391
- "image_url": "data:image/png;base64," + get_image_base64('typing.png'),
392
- "prompt": "Create a typing speed test game where words fall from the top. Type them before they reach the bottom to score points."
393
- },
394
- {
395
- "name": "[게임] 미니 골프",
396
- "image_url": "data:image/png;base64," + get_image_base64('minigolf.png'),
397
- "prompt": "Implement a mini golf game with physics-based ball movement. Include multiple holes and scoring based on strokes."
398
- },
399
- {
400
- "name": "[게임] 낚시 게임",
401
- "image_url": "data:image/png;base64," + get_image_base64('fishing.png'),
402
- "prompt": "Design a fishing game where the player casts a line, reels fish, and can upgrade gear. Manage fish spawn rates and scoring."
403
- },
404
- {
405
- "name": "[게임] 빙고",
406
- "image_url": "data:image/png;base64," + get_image_base64('bingo.png'),
407
- "prompt": "Build a bingo game with randomly generated boards and a calling system. Automatically check winning lines."
408
- },
409
- {
410
- "name": "[게임] 리듬 게임",
411
- "image_url": "data:image/png;base64," + get_image_base64('rhythm.png'),
412
- "prompt": "Create a web-based rhythm game using keyboard inputs. Time hits accurately for score, and add background music."
413
- },
414
- {
415
- "name": "[게임] 2D 레이싱",
416
- "image_url": "data:image/png;base64," + get_image_base64('racing2d.png'),
417
- "prompt": "Implement a top-down 2D racing game with track boundaries, lap times, and multiple AI opponents."
418
- },
419
- {
420
- "name": "[게임] 퀴즈 게임",
421
- "image_url": "data:image/png;base64," + get_image_base64('quiz.png'),
422
- "prompt": "Build a quiz game with multiple-choice questions, scoring, and a timer. Randomize question order each round."
423
- },
424
- {
425
- "name": "[게임] 돌 맞추기 (Shooting Gallery)",
426
- "image_url": "data:image/png;base64," + get_image_base64('gallery.png'),
427
- "prompt": "Create a shooting gallery game with moving targets, limited ammo, and a time limit. Track hits and misses."
428
- },
429
- {
430
- "name": "[게임] 주사위 보드",
431
- "image_url": "data:image/png;base64," + get_image_base64('diceboard.png'),
432
- "prompt": "Implement a dice-based board game with multiple squares, events, and item usage. Players take turns rolling."
433
- },
434
- {
435
- "name": "[게임] 좀비 서바이벌",
436
- "image_url": "data:image/png;base64," + get_image_base64('zombie.png'),
437
- "prompt": "Design a top-down zombie survival game with wave-based enemies, pickups, and limited ammo. Track score and health."
438
- },
439
- {
440
- "name": "[게임] 축구 게임 (Penalty Kick)",
441
- "image_url": "data:image/png;base64," + get_image_base64('soccer.png'),
442
- "prompt": "Build a simple penalty shootout game with aiming, power bars, and a goalie AI that guesses shots randomly."
443
- },
444
- {
445
- "name": "[게임] Minesweeper",
446
- "image_url": "data:image/png;base64," + get_image_base64('minesweeper.png'),
447
- "prompt": "Implement the classic Minesweeper game with left-click reveal, right-click flags, and adjacency logic for numbers."
448
- },
449
- {
450
- "name": "[게임] Connect Four",
451
- "image_url": "data:image/png;base64," + get_image_base64('connect4.png'),
452
- "prompt": "Create a Connect Four game with drag-and-drop or click-based input, alternating turns, and a win check algorithm."
453
- },
454
- {
455
- "name": "[게임] 스크래블 (단어 퍼즐)",
456
- "image_url": "data:image/png;base64," + get_image_base64('scrabble.png'),
457
- "prompt": "Build a Scrabble-like word puzzle game with letter tiles, scoring, and a local dictionary for validation."
458
- },
459
- {
460
- "name": "[게임] 2D 슈팅 (Tank Battle)",
461
- "image_url": "data:image/png;base64," + get_image_base64('tank.png'),
462
- "prompt": "Implement a 2D tank battle game with destructible terrain, power-ups, and AI or multiplayer functionality."
463
- },
464
- {
465
- "name": "[게임] 젬 크러쉬",
466
- "image_url": "data:image/png;base64," + get_image_base64('gemcrush.png'),
467
- "prompt": "Create a gem-crushing puzzle game where matching gems cause chain reactions. Track combos and score bonuses."
468
- },
469
- {
470
- "name": "[게임] Shooting Tower",
471
- "image_url": "data:image/png;base64," + get_image_base64('tower.png'),
472
- "prompt": "Design a 2D defense game where a single tower shoots incoming enemies in waves. Upgrade the tower’s stats over time."
473
- },
474
- {
475
- "name": "[게임] 좀비 러너",
476
- "image_url": "data:image/png;base64," + get_image_base64('zombierunner.png'),
477
- "prompt": "Make a side-scrolling runner where a character avoids zombies and obstacles, collecting power-ups along the way."
478
- },
479
- {
480
- "name": "[게임] 스킬 액션 RPG",
481
- "image_url": "data:image/png;base64," + get_image_base64('actionrpg.png'),
482
- "prompt": "Create a small action RPG with WASD movement, an attack button, special moves, leveling, and item drops."
483
- }
484
  ]
485
-
486
- def load_best_templates():
487
- json_data = load_json_data()[:12]
488
- return create_template_html("🏆 베스트 게임 템플릿", json_data)
489
-
490
- def load_trending_templates():
491
- json_data = load_json_data()[12:24]
492
- return create_template_html("🔥 트렌딩 게임 템플릿", json_data)
493
-
494
- def load_new_templates():
495
- json_data = load_json_data()[24:44]
496
- return create_template_html("✨ NEW 게임 템플릿", json_data)
497
 
498
  def create_template_html(title, items):
499
- html_content = """
 
 
 
500
  <style>
501
- .prompt-grid {
502
  display: grid;
503
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
504
  gap: 20px;
505
  padding: 20px;
506
- }
507
- .prompt-card {
508
  background: white;
509
  border: 1px solid #eee;
510
  border-radius: 8px;
511
  padding: 15px;
512
  cursor: pointer;
513
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
514
- }
515
- .prompt-card:hover {
516
  transform: translateY(-2px);
517
  transition: transform 0.2s;
518
- }
519
- .card-image {
520
  width: 100%;
521
  height: 180px;
522
  object-fit: cover;
523
  border-radius: 4px;
524
  margin-bottom: 10px;
525
- }
526
- .card-name {
527
  font-weight: bold;
528
  margin-bottom: 8px;
529
  font-size: 16px;
530
  color: #333;
531
- }
532
- .card-prompt {
533
  font-size: 11px;
534
  line-height: 1.4;
535
  color: #666;
@@ -541,19 +283,23 @@ def create_template_html(title, items):
541
  background-color: #f8f9fa;
542
  padding: 8px;
543
  border-radius: 4px;
544
- }
545
  </style>
546
  <div class="prompt-grid">
547
  """
548
  for item in items:
 
 
 
549
  html_content += f"""
550
- <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
551
- <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
552
- <div class="card-name">{html.escape(item.get('name', ''))}</div>
553
- <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
554
  </div>
555
  """
556
  html_content += """
 
557
  <script>
558
  function copyToInput(card) {
559
  const prompt = card.dataset.prompt;
@@ -561,185 +307,51 @@ def create_template_html(title, items):
561
  if (textarea) {
562
  textarea.value = prompt;
563
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
564
- document.querySelector('.session-drawer .close-btn').click();
 
 
 
 
565
  }
566
  }
567
  </script>
568
- </div>
569
  """
570
  return gr.HTML(value=html_content)
571
 
572
- TEMPLATE_CACHE = None
 
 
573
 
574
- def load_session_history(template_type="best"):
575
- global TEMPLATE_CACHE
576
-
577
- try:
578
- json_data = load_json_data()
579
-
580
- templates = {
581
- "best": json_data[:12],
582
- "trending": json_data[12:24],
583
- "new": json_data[24:44]
584
- }
585
-
586
- titles = {
587
- "best": "🏆 베스트 게임 템플릿",
588
- "trending": "🔥 트렌딩 게임 템플릿",
589
- "new": "✨ NEW 게임 템플릿"
590
- }
591
-
592
- html_content = """
593
- <style>
594
- .template-nav {
595
- display: flex;
596
- gap: 10px;
597
- margin: 20px;
598
- position: sticky;
599
- top: 0;
600
- background: white;
601
- z-index: 100;
602
- padding: 10px 0;
603
- border-bottom: 1px solid #eee;
604
- }
605
- .template-btn {
606
- padding: 8px 16px;
607
- border: 1px solid #1890ff;
608
- border-radius: 4px;
609
- cursor: pointer;
610
- background: white;
611
- color: #1890ff;
612
- font-weight: bold;
613
- transition: all 0.3s;
614
- }
615
- .template-btn:hover {
616
- background: #1890ff;
617
- color: white;
618
- }
619
- .template-btn.active {
620
- background: #1890ff;
621
- color: white;
622
- }
623
- .prompt-grid {
624
- display: grid;
625
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
626
- gap: 20px;
627
- padding: 20px;
628
- }
629
- .prompt-card {
630
- background: white;
631
- border: 1px solid #eee;
632
- border-radius: 8px;
633
- padding: 15px;
634
- cursor: pointer;
635
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
636
- }
637
- .prompt-card:hover {
638
- transform: translateY(-2px);
639
- transition: transform 0.2s;
640
- }
641
- .card-image {
642
- width: 100%;
643
- height: 180px;
644
- object-fit: cover;
645
- border-radius: 4px;
646
- margin-bottom: 10px;
647
- }
648
- .card-name {
649
- font-weight: bold;
650
- margin-bottom: 8px;
651
- font-size: 16px;
652
- color: #333;
653
- }
654
- .card-prompt {
655
- font-size: 11px;
656
- line-height: 1.4;
657
- color: #666;
658
- display: -webkit-box;
659
- -webkit-line-clamp: 6;
660
- -webkit-box-orient: vertical;
661
- overflow: hidden;
662
- height: 90px;
663
- background-color: #f8f9fa;
664
- padding: 8px;
665
- border-radius: 4px;
666
- }
667
- .template-section {
668
- display: none;
669
- }
670
- .template-section.active {
671
- display: block;
672
- }
673
- </style>
674
- <div class="template-nav">
675
- <button class="template-btn" onclick="showTemplate('best')">🏆 베스트</button>
676
- <button class="template-btn" onclick="showTemplate('trending')">🔥 트렌딩</button>
677
- <button class="template-btn" onclick="showTemplate('new')">✨ NEW</button>
678
- </div>
679
- """
680
- for section, items in templates.items():
681
- html_content += f"""
682
- <div class="template-section" id="{section}-templates">
683
- <div class="prompt-grid">
684
- """
685
- for item in items:
686
- html_content += f"""
687
- <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
688
- <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
689
- <div class="card-name">{html.escape(item.get('name', ''))}</div>
690
- <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
691
- </div>
692
- """
693
- html_content += "</div></div>"
694
-
695
- html_content += """
696
- <script>
697
- function copyToInput(card) {
698
- const prompt = card.dataset.prompt;
699
- const textarea = document.querySelector('.ant-input-textarea-large textarea');
700
- if (textarea) {
701
- textarea.value = prompt;
702
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
703
- document.querySelector('.session-drawer .close-btn').click();
704
- }
705
- }
706
-
707
- function showTemplate(type) {
708
- document.querySelectorAll('.template-section').forEach(section => {
709
- section.style.display = 'none';
710
- });
711
- document.querySelectorAll('.template-btn').forEach(btn => {
712
- btn.classList.remove('active');
713
- });
714
- document.getElementById(type + '-templates').style.display = 'block';
715
- event.target.classList.add('active');
716
- }
717
- document.addEventListener('DOMContentLoaded', function() {
718
- showTemplate('best');
719
- document.querySelector('.template-btn').classList.add('active');
720
- });
721
- </script>
722
- """
723
- return gr.HTML(value=html_content)
724
- except Exception as e:
725
- print(f"Error in load_session_history: {str(e)}")
726
- return gr.HTML("Error loading templates")
727
 
728
- def generate_space_name():
729
- letters = string.ascii_lowercase
730
- return ''.join(random.choice(letters) for i in range(6))
731
 
732
  def deploy_to_vercel(code: str):
 
 
 
 
733
  try:
734
- token = "A8IFZmgW2cqA4yUNlLPnci0N"
735
  if not token:
736
  return "Vercel 토큰이 설정되지 않았습니다."
 
737
  project_name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
738
  deploy_url = "https://api.vercel.com/v13/deployments"
 
739
  headers = {
740
  "Authorization": f"Bearer {token}",
741
  "Content-Type": "application/json"
742
  }
 
743
  package_json = {
744
  "name": project_name,
745
  "version": "1.0.0",
@@ -753,6 +365,7 @@ def deploy_to_vercel(code: str):
753
  "preview": "vite preview"
754
  }
755
  }
 
756
  files = [
757
  {
758
  "file": "index.html",
@@ -763,27 +376,36 @@ def deploy_to_vercel(code: str):
763
  "data": json.dumps(package_json, indent=2)
764
  }
765
  ]
 
766
  project_settings = {
767
  "buildCommand": "npm run build",
768
  "outputDirectory": "dist",
769
  "installCommand": "npm install",
770
  "framework": None
771
  }
 
772
  deploy_data = {
773
  "name": project_name,
774
  "files": files,
775
  "target": "production",
776
  "projectSettings": project_settings
777
  }
 
778
  deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data)
779
  if deploy_response.status_code != 200:
780
  return f"배포 실패: {deploy_response.text}"
 
781
  deployment_url = f"{project_name}.vercel.app"
782
  time.sleep(5)
 
783
  return f"""배포 완료! <a href="https://{deployment_url}" target="_blank" style="color: #1890ff; text-decoration: underline; cursor: pointer;">https://{deployment_url}</a>"""
784
  except Exception as e:
785
  return f"배포 중 오류 발생: {str(e)}"
786
 
 
 
 
 
787
  def boost_prompt(prompt: str) -> str:
788
  if not prompt:
789
  return ""
@@ -798,7 +420,9 @@ def boost_prompt(prompt: str) -> str:
798
  5. 접근성과 호환성
799
  기존 SystemPrompt의 모든 규칙을 준수하면서 증강된 프롬프트를 생성하십시오.
800
  """
 
801
  try:
 
802
  try:
803
  response = claude_client.messages.create(
804
  model="claude-3-7-sonnet-20250219",
@@ -811,7 +435,10 @@ def boost_prompt(prompt: str) -> str:
811
  if hasattr(response, 'content') and len(response.content) > 0:
812
  return response.content[0].text
813
  raise Exception("Claude API 응답 형식 오류")
 
814
  except Exception as claude_error:
 
 
815
  completion = openai_client.chat.completions.create(
816
  model="gpt-4",
817
  messages=[
@@ -824,247 +451,396 @@ def boost_prompt(prompt: str) -> str:
824
  if completion.choices and len(completion.choices) > 0:
825
  return completion.choices[0].message.content
826
  raise Exception("OpenAI API 응답 형식 오류")
 
827
  except Exception as e:
 
828
  return prompt
829
 
830
  def handle_boost(prompt: str):
 
 
 
 
831
  try:
832
  boosted_prompt = boost_prompt(prompt)
833
  return boosted_prompt, gr.update(active_key="empty")
834
  except Exception as e:
 
835
  return prompt, gr.update(active_key="empty")
836
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
837
  demo_instance = Demo()
838
 
839
  with gr.Blocks(css_paths="app.css", theme=theme) as demo:
840
  history = gr.State([])
841
- setting = gr.State({
842
- "system": SystemPrompt,
843
- })
844
 
845
  with ms.Application() as app:
846
  with antd.ConfigProvider():
 
847
  with antd.Drawer(open=False, title="code", placement="left", width="750px") as code_drawer:
848
  code_output = legacy.Markdown()
849
 
 
850
  with antd.Drawer(open=False, title="history", placement="left", width="900px") as history_drawer:
851
- history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot")
852
-
 
 
 
 
 
 
853
  with antd.Drawer(
854
- open=False,
855
- title="Templates",
856
  placement="right",
857
  width="900px",
858
  elem_classes="session-drawer"
859
  ) as session_drawer:
860
  with antd.Flex(vertical=True, gap="middle"):
861
  gr.Markdown("### Available Game Templates")
862
- session_history = gr.HTML(
863
- elem_classes="session-history"
864
- )
865
- close_btn = antd.Button(
866
- "Close",
867
- type="default",
868
- elem_classes="close-btn"
869
- )
870
-
871
- # Removed the left box entirely. We place the generation options at the top.
872
- with antd.Row(gutter=[32, 12]) as layout:
873
  with antd.Col(span=24):
874
  with antd.Flex(vertical=True, gap="small", wrap=True):
875
  # ---- GAME CREATION OPTION MENU ----
876
  with antd.Card(title="게임 생성 옵션"):
 
877
  difficulty = antd.RadioGroup(
878
- choices=["초보자", "중급", "고급"],
879
- value="초보자",
880
- direction="horizontal",
881
- label="난이도"
 
 
882
  )
 
883
  game_type = antd.RadioGroup(
884
- choices=["액션", "퍼즐", "RPG", "전략", "아케이드"],
 
 
 
 
 
 
885
  value="액션",
886
- direction="horizontal",
887
- label="게임 유형"
888
  )
 
889
  graphic_style = antd.RadioGroup(
890
- choices=["픽셀 아트", "미니멀", "카툰", "레트로"],
 
 
 
 
 
891
  value="픽셀 아트",
892
- direction="horizontal",
893
- label="그래픽 스타일"
894
  )
 
895
  viewpoint = antd.RadioGroup(
896
- choices=["탑다운", "사이드 스크롤", "아이소메트릭", "1인칭", "3인칭"],
 
 
 
 
 
 
897
  value="탑다운",
898
- direction="horizontal",
899
- label="시점 뷰"
900
  )
901
 
902
- # Combined prompt generation function
 
 
 
 
 
 
 
 
 
903
  def update_prompt(dif, gtype, gstyle, vp, custom):
904
  base_prompt = (
905
  f"{dif} 난이도의 {gtype} 장르 게임, "
906
  f"{gstyle} 그래픽 스타일, {vp} 시점으로 제작. "
907
  )
 
908
  if custom and custom.strip():
909
  base_prompt += custom.strip()
910
  return base_prompt
911
 
912
- with antd.Flex(gap="middle", wrap=True):
913
- input = antd.InputTextarea(
914
- size="large",
915
- allow_clear=True,
916
- placeholder=random.choice(DEMO_LIST)['description'],
917
- label="최종 게임 프롬프트"
918
- )
919
-
920
- # Bind changes from radio groups to update the text area
921
  difficulty.change(
922
  fn=update_prompt,
923
- inputs=[difficulty, game_type, graphic_style, viewpoint, input],
924
- outputs=[input]
925
  )
926
  game_type.change(
927
  fn=update_prompt,
928
- inputs=[difficulty, game_type, graphic_style, viewpoint, input],
929
- outputs=[input]
930
  )
931
  graphic_style.change(
932
  fn=update_prompt,
933
- inputs=[difficulty, game_type, graphic_style, viewpoint, input],
934
- outputs=[input]
935
  )
936
  viewpoint.change(
937
  fn=update_prompt,
938
- inputs=[difficulty, game_type, graphic_style, viewpoint, input],
939
- outputs=[input]
940
  )
941
 
 
942
  with antd.Flex(gap="small", justify="start"):
943
- btn = antd.Button("Send", type="primary", size="large")
944
- boost_btn = antd.Button("Boost", type="default", size="large")
945
- execute_btn = antd.Button("Code실행", type="default", size="large")
946
- deploy_btn = antd.Button("배포", type="default", size="large")
947
- clear_btn = antd.Button("클리어", type="default", size="large")
948
 
949
  deploy_result = gr.HTML(label="배포 결과")
950
 
 
951
  with ms.Div(elem_classes="right_panel"):
952
  with antd.Flex(gap="small", elem_classes="setting-buttons"):
953
- codeBtn = antd.Button("🧑‍💻 코드 보기", type="default")
954
- historyBtn = antd.Button("📜 히스토리", type="default")
955
- best_btn = antd.Button("🏆 베스트 템플릿", type="default")
956
- trending_btn = antd.Button("🔥 트렌딩 템플릿", type="default")
957
- new_btn = antd.Button("✨ NEW 템플릿", type="default")
958
 
959
  gr.HTML('<div class="render_header"><span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span></div>')
960
 
961
  with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
962
  with antd.Tabs.Item(key="empty"):
963
- empty = antd.Empty(description="empty input", elem_classes="right_content")
964
  with antd.Tabs.Item(key="loading"):
965
- loading = antd.Spin(True, tip="coding...", size="large", elem_classes="right_content")
966
  with antd.Tabs.Item(key="render"):
967
  sandbox = gr.HTML(elem_classes="html_content")
968
 
969
- def execute_code(query: str):
970
- if not query or query.strip() == '':
971
- return None, gr.update(active_key="empty")
972
- try:
973
- if '```html' in query and '```' in query:
974
- code = remove_code_block(query)
975
- else:
976
- code = query.strip()
977
- return send_to_sandbox(code), gr.update(active_key="render")
978
- except Exception as e:
979
- return None, gr.update(active_key="empty")
980
-
981
- execute_btn.click(
982
  fn=execute_code,
983
- inputs=[input],
984
  outputs=[sandbox, state_tab]
985
  )
986
 
 
987
  codeBtn.click(
988
- lambda: gr.update(open=True),
989
- inputs=[],
990
  outputs=[code_drawer]
991
  )
992
-
993
  code_drawer.close(
994
- lambda: gr.update(open=False),
995
- inputs=[],
996
  outputs=[code_drawer]
997
  )
998
 
 
999
  historyBtn.click(
1000
  history_render,
1001
  inputs=[history],
1002
  outputs=[history_drawer, history_output]
1003
  )
1004
-
1005
  history_drawer.close(
1006
- lambda: gr.update(open=False),
1007
  inputs=[],
1008
  outputs=[history_drawer]
1009
  )
1010
 
 
1011
  best_btn.click(
1012
  fn=lambda: (gr.update(open=True), load_best_templates()),
1013
  outputs=[session_drawer, session_history],
1014
  queue=False
1015
  )
1016
-
1017
  trending_btn.click(
1018
  fn=lambda: (gr.update(open=True), load_trending_templates()),
1019
  outputs=[session_drawer, session_history],
1020
  queue=False
1021
  )
1022
-
1023
  new_btn.click(
1024
  fn=lambda: (gr.update(open=True), load_new_templates()),
1025
  outputs=[session_drawer, session_history],
1026
  queue=False
1027
  )
1028
-
1029
  session_drawer.close(
1030
- lambda: (gr.update(open=False), gr.HTML("")),
1031
  outputs=[session_drawer, session_history]
1032
  )
1033
-
1034
  close_btn.click(
1035
- lambda: (gr.update(open=False), gr.HTML("")),
1036
  outputs=[session_drawer, session_history]
1037
  )
1038
 
 
1039
  btn.click(
1040
- demo_instance.generation_code,
1041
- inputs=[input, setting, history],
1042
  outputs=[code_output, history, sandbox, state_tab, code_drawer]
1043
  )
1044
 
 
1045
  clear_btn.click(
1046
- demo_instance.clear_history,
1047
  inputs=[],
1048
  outputs=[history]
1049
  )
1050
 
 
1051
  boost_btn.click(
1052
  fn=handle_boost,
1053
- inputs=[input],
1054
- outputs=[input, state_tab]
1055
  )
1056
 
 
1057
  deploy_btn.click(
1058
  fn=lambda code: deploy_to_vercel(remove_code_block(code)) if code else "코드가 없습니다.",
1059
  inputs=[code_output],
1060
  outputs=[deploy_result]
1061
  )
1062
 
1063
- # 실제 실행 부분
 
 
1064
  if __name__ == "__main__":
1065
  try:
1066
  demo_instance = Demo()
1067
- # queue()를 통해 비동기 처리; ssr_mode=False는 서버-사이드 렌더링 비활성화
1068
  demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
1069
  except Exception as e:
1070
  print(f"Initialization error: {e}")
 
1
  # app.py
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import os
4
  import re
5
  import random
 
6
  from typing import Dict, List, Optional, Tuple
7
  import base64
8
  import anthropic
9
  import openai
10
  import asyncio
11
  import time
 
12
  import json
13
+ import string
14
+ import requests
15
+
16
  import gradio as gr
17
  import modelscope_studio.components.base as ms
18
  import modelscope_studio.components.legacy as legacy
19
  import modelscope_studio.components.antd as antd
20
 
21
+ ##################################################
22
+ # 아래는 사용하는 DEMO_LIST, SystemPrompt, 클래스 정의 등
23
+ ##################################################
 
 
24
 
25
  DEMO_LIST = [
26
  {"description": "Create a Tetris-like puzzle game with arrow key controls, line-clearing mechanics, and increasing difficulty levels."},
 
94
  IMAGE_CACHE = {}
95
 
96
  def get_image_base64(image_path):
97
+ """
98
+ 필요한 경우, 이미지 파일 읽어 base64로 변환하여 캐싱하는 함수.
99
+ 여기서는 예시로만 사용.
100
+ """
101
  if image_path in IMAGE_CACHE:
102
  return IMAGE_CACHE[image_path]
103
  try:
 
106
  IMAGE_CACHE[image_path] = encoded_string
107
  return encoded_string
108
  except:
109
+ # fallback
110
+ return ""
111
 
112
  def history_to_messages(history: History, system: str) -> Messages:
113
  messages = [{'role': Role.SYSTEM, 'content': system}]
 
119
  def messages_to_history(messages: Messages) -> History:
120
  assert messages[0]['role'] == Role.SYSTEM
121
  history = []
122
+ # messages[1::2] => user role
123
+ # messages[2::2] => assistant role
124
  for q, r in zip(messages[1::2], messages[2::2]):
125
  history.append([q['content'], r['content']])
126
  return history
 
128
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
129
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
130
 
131
+ # Anthropic / OpenAI 초기화
132
  claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
133
  openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
134
 
135
  async def try_claude_api(system_message, claude_messages, timeout=15):
136
+ """
137
+ Anthropic Claude API를 호출해 스트리밍 방식으로 결과를 yield.
138
+ """
139
  try:
140
  start_time = time.time()
141
  with claude_client.messages.stream(
 
159
  raise e
160
 
161
  async def try_openai_api(openai_messages):
162
+ """
163
+ OpenAI ChatCompletion (GPT-4 / gpt-4o 등) 을 스트리밍 방식으로 호출.
164
+ """
165
  try:
166
  stream = openai_client.chat.completions.create(
167
+ model="gpt-4o", # 예: "gpt-4"
168
  messages=openai_messages,
169
  stream=True,
170
  max_tokens=4096,
 
179
  print(f"OpenAI API error: {str(e)}")
180
  raise e
181
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  def remove_code_block(text):
183
+ """
184
+ AI가 생성한 응답에서 ```html\n ... \n``` 구문을 추출해
185
+ 실제로 실행할 HTML 코드만 떼어내는 함수.
186
+ """
187
  pattern = r'```html\n(.+?)\n```'
188
  match = re.search(pattern, text, re.DOTALL)
189
  if match:
 
191
  else:
192
  return text.strip()
193
 
 
 
 
194
  def send_to_sandbox(code):
195
+ """
196
+ HTML 코드를 data URI로 변환해 iframe으로 보여주는 용도.
197
+ """
198
  encoded_html = base64.b64encode(code.encode('utf-8')).decode('utf-8')
199
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
200
  return f"<iframe src=\"{data_uri}\" width=\"100%\" height=\"920px\"></iframe>"
201
 
202
+ def history_render(history: History):
203
+ """
204
+ 히스토리창을 열고 기존 history를 전달
205
+ """
206
+ return gr.update(open=True), history
207
+
208
  theme = gr.themes.Soft()
209
 
210
+ ##################################################
211
+ # 템플릿과 베이스/트렌딩/뉴 항목 로딩 함수
212
+ ##################################################
213
+
214
  def load_json_data():
215
+ """
216
+ 예시로 게임 템플릿 목록을 반환.
217
+ 실제로는 DB나 외부 파일에서 로딩할 수도 있음.
218
+ """
219
+ data = []
220
+ # 예시 데이터
221
+ data = [
222
  {
223
  "name": "[게임] 테트리스 클론",
224
  "image_url": "data:image/png;base64," + get_image_base64('tetris.png'),
 
229
  "image_url": "data:image/png;base64," + get_image_base64('chess.png'),
230
  "prompt": "Build an interactive Chess game with a basic AI opponent and drag-and-drop piece movement. Keep track of moves and detect check/checkmate."
231
  },
232
+ # ... 더 많은 항목 ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  ]
234
+ # 필요하면 DEMO_LIST를 참조하거나 더 큰 목록으로 확장 가능
235
+ return data
 
 
 
 
 
 
 
 
 
 
236
 
237
  def create_template_html(title, items):
238
+ """
239
+ 카드 레이아웃 형태의 HTML 템플릿을 만들어 Gradio HTML 컴포넌트로 전달
240
+ """
241
+ html_content = f"""
242
  <style>
243
+ .prompt-grid {{
244
  display: grid;
245
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
246
  gap: 20px;
247
  padding: 20px;
248
+ }}
249
+ .prompt-card {{
250
  background: white;
251
  border: 1px solid #eee;
252
  border-radius: 8px;
253
  padding: 15px;
254
  cursor: pointer;
255
  box-shadow: 0 2px 5px rgba(0,0,0,0.1);
256
+ }}
257
+ .prompt-card:hover {{
258
  transform: translateY(-2px);
259
  transition: transform 0.2s;
260
+ }}
261
+ .card-image {{
262
  width: 100%;
263
  height: 180px;
264
  object-fit: cover;
265
  border-radius: 4px;
266
  margin-bottom: 10px;
267
+ }}
268
+ .card-name {{
269
  font-weight: bold;
270
  margin-bottom: 8px;
271
  font-size: 16px;
272
  color: #333;
273
+ }}
274
+ .card-prompt {{
275
  font-size: 11px;
276
  line-height: 1.4;
277
  color: #666;
 
283
  background-color: #f8f9fa;
284
  padding: 8px;
285
  border-radius: 4px;
286
+ }}
287
  </style>
288
  <div class="prompt-grid">
289
  """
290
  for item in items:
291
+ safe_prompt = html.escape(item.get('prompt', ''))
292
+ name_txt = html.escape(item.get('name', ''))
293
+ image_url = item.get('image_url', '')
294
  html_content += f"""
295
+ <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{safe_prompt}">
296
+ <img src="{image_url}" class="card-image" loading="lazy" alt="{name_txt}">
297
+ <div class="card-name">{name_txt}</div>
298
+ <div class="card-prompt">{safe_prompt}</div>
299
  </div>
300
  """
301
  html_content += """
302
+ </div>
303
  <script>
304
  function copyToInput(card) {
305
  const prompt = card.dataset.prompt;
 
307
  if (textarea) {
308
  textarea.value = prompt;
309
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
310
+ // drawer 닫기
311
+ const drawerCloseBtn = document.querySelector('.session-drawer .close-btn');
312
+ if (drawerCloseBtn) {
313
+ drawerCloseBtn.click();
314
+ }
315
  }
316
  }
317
  </script>
 
318
  """
319
  return gr.HTML(value=html_content)
320
 
321
+ def load_best_templates():
322
+ json_data = load_json_data()[:5] # 예시로 5개만
323
+ return create_template_html("🏆 베스트 게임 템플릿", json_data)
324
 
325
+ def load_trending_templates():
326
+ json_data = load_json_data()[5:10] # 예시
327
+ return create_template_html("🔥 트렌딩 게임 템플릿", json_data)
328
+
329
+ def load_new_templates():
330
+ json_data = load_json_data()[10:15] # 예시
331
+ return create_template_html("✨ NEW 게임 템플릿", json_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
332
 
333
+ ##################################################
334
+ # Deploy 함수 예시
335
+ ##################################################
336
 
337
  def deploy_to_vercel(code: str):
338
+ """
339
+ 예시로, Vercel API 를 통해 HTML을 배포하는 함수.
340
+ token을 지정해야 정상 동작.
341
+ """
342
  try:
343
+ token = "A8IFZmgW2cqA4yUNlLPnci0N" # 예시 토큰
344
  if not token:
345
  return "Vercel 토큰이 설정되지 않았습니다."
346
+
347
  project_name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
348
  deploy_url = "https://api.vercel.com/v13/deployments"
349
+
350
  headers = {
351
  "Authorization": f"Bearer {token}",
352
  "Content-Type": "application/json"
353
  }
354
+
355
  package_json = {
356
  "name": project_name,
357
  "version": "1.0.0",
 
365
  "preview": "vite preview"
366
  }
367
  }
368
+
369
  files = [
370
  {
371
  "file": "index.html",
 
376
  "data": json.dumps(package_json, indent=2)
377
  }
378
  ]
379
+
380
  project_settings = {
381
  "buildCommand": "npm run build",
382
  "outputDirectory": "dist",
383
  "installCommand": "npm install",
384
  "framework": None
385
  }
386
+
387
  deploy_data = {
388
  "name": project_name,
389
  "files": files,
390
  "target": "production",
391
  "projectSettings": project_settings
392
  }
393
+
394
  deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data)
395
  if deploy_response.status_code != 200:
396
  return f"배포 실패: {deploy_response.text}"
397
+
398
  deployment_url = f"{project_name}.vercel.app"
399
  time.sleep(5)
400
+
401
  return f"""배포 완료! <a href="https://{deployment_url}" target="_blank" style="color: #1890ff; text-decoration: underline; cursor: pointer;">https://{deployment_url}</a>"""
402
  except Exception as e:
403
  return f"배포 중 오류 발생: {str(e)}"
404
 
405
+ ##################################################
406
+ # Prompt Boost 예시
407
+ ##################################################
408
+
409
  def boost_prompt(prompt: str) -> str:
410
  if not prompt:
411
  return ""
 
420
  5. 접근성과 호환성
421
  기존 SystemPrompt의 모든 규칙을 준수하면서 증강된 프롬프트를 생성하십시오.
422
  """
423
+
424
  try:
425
+ # 우선 Anthropic 시도
426
  try:
427
  response = claude_client.messages.create(
428
  model="claude-3-7-sonnet-20250219",
 
435
  if hasattr(response, 'content') and len(response.content) > 0:
436
  return response.content[0].text
437
  raise Exception("Claude API 응답 형식 오류")
438
+
439
  except Exception as claude_error:
440
+ # Claude 실패 -> OpenAI로 fallback
441
+ print(f"Claude API 에러, OpenAI로 전환: {str(claude_error)}")
442
  completion = openai_client.chat.completions.create(
443
  model="gpt-4",
444
  messages=[
 
451
  if completion.choices and len(completion.choices) > 0:
452
  return completion.choices[0].message.content
453
  raise Exception("OpenAI API 응답 형식 오류")
454
+
455
  except Exception as e:
456
+ print(f"프롬프트 증강 오류: {str(e)}")
457
  return prompt
458
 
459
  def handle_boost(prompt: str):
460
+ """
461
+ Gradio 이벤트에서 사용.
462
+ Boost 수행 후, 탭 상태를 empty로 변경.
463
+ """
464
  try:
465
  boosted_prompt = boost_prompt(prompt)
466
  return boosted_prompt, gr.update(active_key="empty")
467
  except Exception as e:
468
+ print(f"handle_boost error: {str(e)}")
469
  return prompt, gr.update(active_key="empty")
470
 
471
+ ##################################################
472
+ # Demo Class
473
+ ##################################################
474
+
475
+ class Demo:
476
+ def __init__(self):
477
+ pass
478
+
479
+ async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
480
+ """
481
+ 사용자가 입력한 query (게임 아이디어)로 AI에게 코드 생성 요청.
482
+ """
483
+ if not query or query.strip() == '':
484
+ query = random.choice(DEMO_LIST)['description']
485
+
486
+ if _history is None:
487
+ _history = []
488
+
489
+ messages = history_to_messages(_history, _setting['system'])
490
+ system_message = messages[0]['content']
491
+
492
+ claude_messages = [
493
+ {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
494
+ for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
495
+ if msg["content"].strip() != ''
496
+ ]
497
+
498
+ openai_messages = [{"role": "system", "content": system_message}]
499
+ for msg in messages[1:]:
500
+ openai_messages.append({
501
+ "role": msg["role"],
502
+ "content": msg["content"]
503
+ })
504
+ openai_messages.append({"role": "user", "content": query})
505
+
506
+ try:
507
+ # 1) 우선 "Generating code..." 표시
508
+ yield [
509
+ "Generating code...",
510
+ _history,
511
+ None,
512
+ gr.update(active_key="loading"),
513
+ gr.update(open=True)
514
+ ]
515
+ await asyncio.sleep(0)
516
+
517
+ collected_content = None
518
+
519
+ # 2) Claude 시도
520
+ try:
521
+ async for content in try_claude_api(system_message, claude_messages, timeout=15):
522
+ yield [
523
+ content, # code_output
524
+ _history, # history
525
+ None, # sandbox
526
+ gr.update(active_key="loading"),
527
+ gr.update(open=True)
528
+ ]
529
+ await asyncio.sleep(0)
530
+ collected_content = content
531
+
532
+ except Exception as claude_error:
533
+ # 3) Claude 실패시 OpenAI 사용
534
+ print(f"Falling back to OpenAI API due to Claude error: {str(claude_error)}")
535
+ async for content in try_openai_api(openai_messages):
536
+ yield [
537
+ content,
538
+ _history,
539
+ None,
540
+ gr.update(active_key="loading"),
541
+ gr.update(open=True)
542
+ ]
543
+ await asyncio.sleep(0)
544
+ collected_content = content
545
+
546
+ # 4) 최종 코드가 있다면 history 갱신 + iframe 표시
547
+ if collected_content:
548
+ _history = messages_to_history([
549
+ {'role': Role.SYSTEM, 'content': system_message}
550
+ ] + claude_messages + [{
551
+ 'role': Role.ASSISTANT,
552
+ 'content': collected_content
553
+ }])
554
+
555
+ # 실제 실행가능한 HTML 코드 추출
556
+ final_html_code = remove_code_block(collected_content)
557
+
558
+ yield [
559
+ collected_content, # code_output
560
+ _history, # history
561
+ send_to_sandbox(final_html_code), # sandbox iframe
562
+ gr.update(active_key="render"),
563
+ gr.update(open=True)
564
+ ]
565
+ else:
566
+ raise ValueError("No content was generated from either API")
567
+
568
+ except Exception as e:
569
+ print(f"Error details: {str(e)}")
570
+ raise ValueError(f'Error calling APIs: {str(e)}')
571
+
572
+ def clear_history(self):
573
+ """
574
+ 사용자가 '클리어' 버튼을 눌렀을 때, 히스토리 초기화
575
+ """
576
+ return []
577
+
578
+ ##################################################
579
+ # Gradio UI 시작
580
+ ##################################################
581
+
582
+ def execute_code(query: str):
583
+ """
584
+ 사용자 입력에 코드가 있으면 실행하여 iframe으로 표시
585
+ """
586
+ if not query or query.strip() == '':
587
+ return None, gr.update(active_key="empty")
588
+ try:
589
+ if '```html' in query and '```' in query:
590
+ code = remove_code_block(query)
591
+ else:
592
+ code = query.strip()
593
+ return send_to_sandbox(code), gr.update(active_key="render")
594
+ except Exception as e:
595
+ print(f"execute_code error: {str(e)}")
596
+ return None, gr.update(active_key="empty")
597
+
598
+
599
  demo_instance = Demo()
600
 
601
  with gr.Blocks(css_paths="app.css", theme=theme) as demo:
602
  history = gr.State([])
603
+ setting = gr.State({"system": SystemPrompt})
 
 
604
 
605
  with ms.Application() as app:
606
  with antd.ConfigProvider():
607
+ # ----- code drawer (좌측) -----
608
  with antd.Drawer(open=False, title="code", placement="left", width="750px") as code_drawer:
609
  code_output = legacy.Markdown()
610
 
611
+ # ----- history drawer (좌측) -----
612
  with antd.Drawer(open=False, title="history", placement="left", width="900px") as history_drawer:
613
+ history_output = legacy.Chatbot(
614
+ show_label=False,
615
+ flushing=False,
616
+ height=960,
617
+ elem_classes="history_chatbot"
618
+ )
619
+
620
+ # ----- session drawer (우측) for templates -----
621
  with antd.Drawer(
622
+ open=False,
623
+ title="Templates",
624
  placement="right",
625
  width="900px",
626
  elem_classes="session-drawer"
627
  ) as session_drawer:
628
  with antd.Flex(vertical=True, gap="middle"):
629
  gr.Markdown("### Available Game Templates")
630
+ session_history = gr.HTML(elem_classes="session-history")
631
+ close_btn = antd.Button("Close", type="default", elem_classes="close-btn")
632
+
633
+ # 메인 레이아웃
634
+ with antd.Row(gutter=[32, 12]):
 
 
 
 
 
 
635
  with antd.Col(span=24):
636
  with antd.Flex(vertical=True, gap="small", wrap=True):
637
  # ---- GAME CREATION OPTION MENU ----
638
  with antd.Card(title="게임 생성 옵션"):
639
+ # antd.RadioGroup는 'choices' 대신 'options' 인자를 사용
640
  difficulty = antd.RadioGroup(
641
+ options=[
642
+ {"label": "초보자", "value": "초보자"},
643
+ {"label": "중급", "value": "중급"},
644
+ {"label": "고급", "value": "고급"},
645
+ ],
646
+ value="초보자", # 초기 선택값
647
  )
648
+
649
  game_type = antd.RadioGroup(
650
+ options=[
651
+ {"label": "액션", "value": "액션"},
652
+ {"label": "퍼즐", "value": "퍼즐"},
653
+ {"label": "RPG", "value": "RPG"},
654
+ {"label": "전략", "value": "전략"},
655
+ {"label": "아케이드", "value": "아케이드"},
656
+ ],
657
  value="액션",
 
 
658
  )
659
+
660
  graphic_style = antd.RadioGroup(
661
+ options=[
662
+ {"label": "픽셀 아트", "value": "픽셀 아트"},
663
+ {"label": "미니멀", "value": "미니멀"},
664
+ {"label": "카툰", "value": "카툰"},
665
+ {"label": "레트로", "value": "레트로"},
666
+ ],
667
  value="픽셀 아트",
 
 
668
  )
669
+
670
  viewpoint = antd.RadioGroup(
671
+ options=[
672
+ {"label": "탑다운", "value": "탑다운"},
673
+ {"label": "사이드 스크롤", "value": "사이드 스크롤"},
674
+ {"label": "아이소메트릭", "value": "아이소메트릭"},
675
+ {"label": "1인칭", "value": "1인칭"},
676
+ {"label": "3인칭", "value": "3인칭"},
677
+ ],
678
  value="탑다운",
 
 
679
  )
680
 
681
+ # 프롬프트 입력 (최종)
682
+ with antd.Flex(gap="middle", wrap=True):
683
+ input_text = antd.InputTextarea(
684
+ size="large",
685
+ allow_clear=True,
686
+ placeholder=random.choice(DEMO_LIST)['description'],
687
+ label="최종 게임 프롬프트"
688
+ )
689
+
690
+ # 라디오 변경 -> InputTextarea 업데이트 함수
691
  def update_prompt(dif, gtype, gstyle, vp, custom):
692
  base_prompt = (
693
  f"{dif} 난이도의 {gtype} 장르 게임, "
694
  f"{gstyle} 그래픽 스타일, {vp} 시점으로 제작. "
695
  )
696
+ # custom : textarea 내용을 우선 유지
697
  if custom and custom.strip():
698
  base_prompt += custom.strip()
699
  return base_prompt
700
 
701
+ # 라디오 체인지 이벤트들
 
 
 
 
 
 
 
 
702
  difficulty.change(
703
  fn=update_prompt,
704
+ inputs=[difficulty, game_type, graphic_style, viewpoint, input_text],
705
+ outputs=[input_text]
706
  )
707
  game_type.change(
708
  fn=update_prompt,
709
+ inputs=[difficulty, game_type, graphic_style, viewpoint, input_text],
710
+ outputs=[input_text]
711
  )
712
  graphic_style.change(
713
  fn=update_prompt,
714
+ inputs=[difficulty, game_type, graphic_style, viewpoint, input_text],
715
+ outputs=[input_text]
716
  )
717
  viewpoint.change(
718
  fn=update_prompt,
719
+ inputs=[difficulty, game_type, graphic_style, viewpoint, input_text],
720
+ outputs=[input_text]
721
  )
722
 
723
+ # 버튼들
724
  with antd.Flex(gap="small", justify="start"):
725
+ btn = antd.Button("Send", type="primary", size="large")
726
+ boost_btn = antd.Button("Boost", type="default", size="large")
727
+ exec_btn = antd.Button("Code실행", type="default", size="large")
728
+ deploy_btn= antd.Button("배포", type="default", size="large")
729
+ clear_btn = antd.Button("클리어", type="default", size="large")
730
 
731
  deploy_result = gr.HTML(label="배포 결과")
732
 
733
+ # 오른쪽 패널
734
  with ms.Div(elem_classes="right_panel"):
735
  with antd.Flex(gap="small", elem_classes="setting-buttons"):
736
+ codeBtn = antd.Button("🧑‍💻 코드 보기", type="default")
737
+ historyBtn = antd.Button("📜 히스토리", type="default")
738
+ best_btn = antd.Button("🏆 베스트 템플릿", type="default")
739
+ trending_btn= antd.Button("🔥 트렌딩 템플릿", type="default")
740
+ new_btn = antd.Button("✨ NEW 템플릿", type="default")
741
 
742
  gr.HTML('<div class="render_header"><span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span></div>')
743
 
744
  with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
745
  with antd.Tabs.Item(key="empty"):
746
+ empty_tab = antd.Empty(description="empty input", elem_classes="right_content")
747
  with antd.Tabs.Item(key="loading"):
748
+ loading_tab = antd.Spin(True, tip="coding...", size="large", elem_classes="right_content")
749
  with antd.Tabs.Item(key="render"):
750
  sandbox = gr.HTML(elem_classes="html_content")
751
 
752
+ # ---- callback 연결 ----
753
+
754
+ # 1) Code실행 버튼
755
+ exec_btn.click(
 
 
 
 
 
 
 
 
 
756
  fn=execute_code,
757
+ inputs=[input_text],
758
  outputs=[sandbox, state_tab]
759
  )
760
 
761
+ # 2) 코드 보기 drawer
762
  codeBtn.click(
763
+ fn=lambda: gr.update(open=True),
764
+ inputs=[],
765
  outputs=[code_drawer]
766
  )
 
767
  code_drawer.close(
768
+ fn=lambda: gr.update(open=False),
769
+ inputs=[],
770
  outputs=[code_drawer]
771
  )
772
 
773
+ # 3) 히스토리 drawer
774
  historyBtn.click(
775
  history_render,
776
  inputs=[history],
777
  outputs=[history_drawer, history_output]
778
  )
 
779
  history_drawer.close(
780
+ fn=lambda: gr.update(open=False),
781
  inputs=[],
782
  outputs=[history_drawer]
783
  )
784
 
785
+ # 4) 템플릿 session drawer (우측)
786
  best_btn.click(
787
  fn=lambda: (gr.update(open=True), load_best_templates()),
788
  outputs=[session_drawer, session_history],
789
  queue=False
790
  )
 
791
  trending_btn.click(
792
  fn=lambda: (gr.update(open=True), load_trending_templates()),
793
  outputs=[session_drawer, session_history],
794
  queue=False
795
  )
 
796
  new_btn.click(
797
  fn=lambda: (gr.update(open=True), load_new_templates()),
798
  outputs=[session_drawer, session_history],
799
  queue=False
800
  )
 
801
  session_drawer.close(
802
+ fn=lambda: (gr.update(open=False), gr.HTML("")),
803
  outputs=[session_drawer, session_history]
804
  )
 
805
  close_btn.click(
806
+ fn=lambda: (gr.update(open=False), gr.HTML("")),
807
  outputs=[session_drawer, session_history]
808
  )
809
 
810
+ # 5) Send 버튼 -> 코드 생성
811
  btn.click(
812
+ fn=demo_instance.generation_code,
813
+ inputs=[input_text, setting, history],
814
  outputs=[code_output, history, sandbox, state_tab, code_drawer]
815
  )
816
 
817
+ # 6) Clear 버튼 -> 히스토리 비움
818
  clear_btn.click(
819
+ fn=demo_instance.clear_history,
820
  inputs=[],
821
  outputs=[history]
822
  )
823
 
824
+ # 7) Boost 버튼
825
  boost_btn.click(
826
  fn=handle_boost,
827
+ inputs=[input_text],
828
+ outputs=[input_text, state_tab]
829
  )
830
 
831
+ # 8) 배포 버튼
832
  deploy_btn.click(
833
  fn=lambda code: deploy_to_vercel(remove_code_block(code)) if code else "코드가 없습니다.",
834
  inputs=[code_output],
835
  outputs=[deploy_result]
836
  )
837
 
838
+ ##################################################
839
+ # 앱 실행
840
+ ##################################################
841
  if __name__ == "__main__":
842
  try:
843
  demo_instance = Demo()
 
844
  demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
845
  except Exception as e:
846
  print(f"Initialization error: {e}")