openfree commited on
Commit
fb325e8
·
verified ·
1 Parent(s): 0bfb168

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +424 -421
app.py CHANGED
@@ -1,93 +1,28 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <style>
5
- /* app.css 내용이 이미 포함되어 있다고 가정 */
6
-
7
- body {
8
- margin: 0;
9
- padding: 0;
10
- font-family: sans-serif;
11
- }
12
- .left_header {
13
- text-align: center;
14
- margin-bottom: 10px;
15
- }
16
- .header_btn {
17
- width: 12px;
18
- height: 12px;
19
- border-radius: 50%;
20
- background-color: #ff5f56;
21
- display: inline-block;
22
- margin-right: 8px;
23
- }
24
- .header_btn:nth-child(2) {
25
- background-color: #ffbd2e;
26
- }
27
- .header_btn:nth-child(3) {
28
- background-color: #27c93f;
29
- }
30
- .render_header {
31
- background: #f3f3f3;
32
- padding: 8px;
33
- margin-top: 10px;
34
- border-radius: 4px;
35
- display: inline-block;
36
- }
37
- .right_content {
38
- width: 100%;
39
- height: 900px;
40
- display: flex;
41
- align-items: center;
42
- justify-content: center;
43
- }
44
- .html_content iframe {
45
- border: none;
46
- overflow: hidden;
47
- }
48
- </style>
49
- </head>
50
- <body>
51
- <script>
52
- "use strict";
53
-
54
- /* 원본 코드 그대로 유지, 단 layout 부분 수정 */
55
- </script>
56
 
57
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/base64js.min.js"></script>
58
- <script src="https://cdn.jsdelivr.net/npm/[email protected]/marked.min.js"></script>
59
-
60
- <!-- Gradio/Modelscope/antd 라이브러리가 이미 깔려 있다고 가정 -->
61
- <script>
62
- /* 원본 파이썬 코드에서 JS로 변환되는 부분이 아니라,
63
- Gradio/Modelscope 통합 환경에서 파이썬 코드 그대로 동작한다고 가정. */
64
- </script>
65
-
66
- <!-- Python code (for Gradio/Modelscope) -->
67
- <script type="module">
68
  import os
69
  import re
70
  import random
71
- from http import HTTPStatus
72
- from typing import Dict, List, Optional, Tuple
73
  import base64
 
 
 
 
74
  import anthropic
75
  import openai
76
- import asyncio
77
- import time
 
78
  from functools import partial
79
- import json
80
  import gradio as gr
81
  import modelscope_studio.components.base as ms
82
  import modelscope_studio.components.legacy as legacy
83
  import modelscope_studio.components.antd as antd
84
 
85
- import html
86
- import urllib.parse
87
- from huggingface_hub import HfApi, create_repo
88
- import string
89
- import requests
90
-
91
  DEMO_LIST = [
92
  {"description": "Create a Tetris-like puzzle game with arrow key controls, line-clearing mechanics, and increasing difficulty levels."},
93
  {"description": "Build an interactive Chess game with a basic AI opponent and drag-and-drop piece movement. Keep track of moves and detect check/checkmate."},
@@ -125,6 +60,7 @@ DEMO_LIST = [
125
  {"description": "Create a small action RPG with WASD movement, an attack button, special moves, leveling, and item drops."},
126
  ]
127
 
 
128
  SystemPrompt = """너의 이름은 'MOUSE'이다. You are an expert web game developer with a strong focus on gameplay mechanics, interactive design, and performance optimization.
129
  Your mission is to create compelling, modern, and fully interactive web-based games using HTML, JavaScript, and CSS.
130
  This code will be rendered directly in the browser.
@@ -157,9 +93,13 @@ class Role:
157
  History = List[Tuple[str, str]]
158
  Messages = List[Dict[str, str]]
159
 
 
160
  IMAGE_CACHE = {}
161
 
162
  def get_image_base64(image_path):
 
 
 
163
  if image_path in IMAGE_CACHE:
164
  return IMAGE_CACHE[image_path]
165
  try:
@@ -184,6 +124,7 @@ def messages_to_history(messages: Messages) -> History:
184
  history.append([q['content'], r['content']])
185
  return history
186
 
 
187
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
188
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
189
 
@@ -191,6 +132,9 @@ claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
191
  openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
192
 
193
  async def try_claude_api(system_message, claude_messages, timeout=15):
 
 
 
194
  try:
195
  start_time = time.time()
196
  with claude_client.messages.stream(
@@ -213,6 +157,9 @@ async def try_claude_api(system_message, claude_messages, timeout=15):
213
  raise e
214
 
215
  async def try_openai_api(openai_messages):
 
 
 
216
  try:
217
  stream = openai_client.chat.completions.create(
218
  model="gpt-4o",
@@ -229,93 +176,10 @@ async def try_openai_api(openai_messages):
229
  except Exception as e:
230
  raise e
231
 
232
- class Demo:
233
- def __init__(self):
234
- pass
235
-
236
- async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
237
- if not query or query.strip() == '':
238
- query = random.choice(DEMO_LIST)['description']
239
-
240
- if _history is None:
241
- _history = []
242
-
243
- messages = history_to_messages(_history, _setting['system'])
244
- system_message = messages[0]['content']
245
-
246
- claude_messages = [
247
- {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
248
- for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
249
- if msg["content"].strip() != ''
250
- ]
251
-
252
- openai_messages = [{"role": "system", "content": system_message}]
253
- for msg in messages[1:]:
254
- openai_messages.append({
255
- "role": msg["role"],
256
- "content": msg["content"]
257
- })
258
- openai_messages.append({"role": "user", "content": query})
259
-
260
- try:
261
- yield [
262
- "Generating code...",
263
- _history,
264
- None,
265
- gr.update(active_key="loading"),
266
- gr.update(open=True)
267
- ]
268
- await asyncio.sleep(0)
269
-
270
- collected_content = None
271
- try:
272
- async for content in try_claude_api(system_message, claude_messages):
273
- yield [
274
- content,
275
- _history,
276
- None,
277
- gr.update(active_key="loading"),
278
- gr.update(open=True)
279
- ]
280
- await asyncio.sleep(0)
281
- collected_content = content
282
- except Exception as claude_error:
283
- async for content in try_openai_api(openai_messages):
284
- yield [
285
- content,
286
- _history,
287
- None,
288
- gr.update(active_key="loading"),
289
- gr.update(open=True)
290
- ]
291
- await asyncio.sleep(0)
292
- collected_content = content
293
-
294
- if collected_content:
295
- _history = messages_to_history([
296
- {'role': Role.SYSTEM, 'content': system_message}
297
- ] + claude_messages + [{
298
- 'role': Role.ASSISTANT,
299
- 'content': collected_content
300
- }])
301
-
302
- yield [
303
- collected_content,
304
- _history,
305
- send_to_sandbox(remove_code_block(collected_content)),
306
- gr.update(active_key="render"),
307
- gr.update(open=True)
308
- ]
309
- else:
310
- raise ValueError("No content was generated from either API")
311
-
312
- except Exception as e:
313
- raise ValueError(f'Error calling APIs: {str(e)}')
314
-
315
- def clear_history(self):
316
- return []
317
-
318
  def remove_code_block(text):
 
 
 
319
  pattern = r'```html\n(.+?)\n```'
320
  match = re.search(pattern, text, re.DOTALL)
321
  if match:
@@ -323,17 +187,26 @@ def remove_code_block(text):
323
  else:
324
  return text.strip()
325
 
326
- def history_render(history: History):
327
- return gr.update(open=True), history
328
-
329
  def send_to_sandbox(code):
 
 
 
330
  encoded_html = base64.b64encode(code.encode('utf-8')).decode('utf-8')
331
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
332
- return f"<iframe src=\"{data_uri}\" width=\"100%\" height=\"920px\"></iframe>"
333
 
334
- theme = gr.themes.Soft()
 
 
 
 
 
 
 
 
335
 
336
  def load_json_data():
 
337
  return [
338
  {
339
  "name": "[게임] 테트리스 클론",
@@ -520,84 +393,90 @@ def load_new_templates():
520
  return create_template_html("✨ NEW 게임 템플릿", json_data)
521
 
522
  def create_template_html(title, items):
523
- html_content = """
524
- <style>
525
- .prompt-grid {
526
- display: grid;
527
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
528
- gap: 20px;
529
- padding: 20px;
530
- }
531
- .prompt-card {
532
- background: white;
533
- border: 1px solid #eee;
534
- border-radius: 8px;
535
- padding: 15px;
536
- cursor: pointer;
537
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
538
- }
539
- .prompt-card:hover {
540
- transform: translateY(-2px);
541
- transition: transform 0.2s;
542
- }
543
- .card-image {
544
- width: 100%;
545
- height: 180px;
546
- object-fit: cover;
547
- border-radius: 4px;
548
- margin-bottom: 10px;
549
- }
550
- .card-name {
551
- font-weight: bold;
552
- margin-bottom: 8px;
553
- font-size: 16px;
554
- color: #333;
555
- }
556
- .card-prompt {
557
- font-size: 11px;
558
- line-height: 1.4;
559
- color: #666;
560
- display: -webkit-box;
561
- -webkit-line-clamp: 6;
562
- -webkit-box-orient: vertical;
563
- overflow: hidden;
564
- height: 90px;
565
- background-color: #f8f9fa;
566
- padding: 8px;
567
- border-radius: 4px;
568
- }
569
- </style>
570
- <div class="prompt-grid">
571
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572
  for item in items:
573
- html_content += f"""
574
- <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
575
- <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
576
- <div class="card-name">{html.escape(item.get('name', ''))}</div>
577
- <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
578
- </div>
579
- """
580
- html_content += """
581
- <script>
582
- function copyToInput(card) {
583
- const prompt = card.dataset.prompt;
584
- const textarea = document.querySelector('.ant-input-textarea-large textarea');
585
- if (textarea) {
586
- textarea.value = prompt;
587
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
588
- document.querySelector('.session-drawer .close-btn').click();
589
- }
590
  }
591
- </script>
592
- </div>
593
- """
 
594
  return gr.HTML(value=html_content)
595
 
596
  TEMPLATE_CACHE = None
597
 
598
  def load_session_history(template_type="best"):
599
- global TEMPLATE_CACHE
600
-
 
601
  try:
602
  json_data = load_json_data()
603
  templates = {
@@ -611,148 +490,162 @@ def load_session_history(template_type="best"):
611
  "new": "✨ NEW 게임 템플릿"
612
  }
613
 
614
- html_content = """
615
- <style>
616
- .template-nav {
617
- display: flex;
618
- gap: 10px;
619
- margin: 20px;
620
- position: sticky;
621
- top: 0;
622
- background: white;
623
- z-index: 100;
624
- padding: 10px 0;
625
- border-bottom: 1px solid #eee;
626
- }
627
- .template-btn {
628
- padding: 8px 16px;
629
- border: 1px solid #1890ff;
630
- border-radius: 4px;
631
- cursor: pointer;
632
- background: white;
633
- color: #1890ff;
634
- font-weight: bold;
635
- transition: all 0.3s;
636
- }
637
- .template-btn:hover {
638
- background: #1890ff;
639
- color: white;
640
- }
641
- .template-btn.active {
642
- background: #1890ff;
643
- color: white;
644
- }
645
- .prompt-grid {
646
- display: grid;
647
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
648
- gap: 20px;
649
- padding: 20px;
650
- }
651
- .prompt-card {
652
- background: white;
653
- border: 1px solid #eee;
654
- border-radius: 8px;
655
- padding: 15px;
656
- cursor: pointer;
657
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
658
- }
659
- .prompt-card:hover {
660
- transform: translateY(-2px);
661
- transition: transform 0.2s;
662
- }
663
- .card-image {
664
- width: 100%;
665
- height: 180px;
666
- object-fit: cover;
667
- border-radius: 4px;
668
- margin-bottom: 10px;
669
- }
670
- .card-name {
671
- font-weight: bold;
672
- margin-bottom: 8px;
673
- font-size: 16px;
674
- color: #333;
675
- }
676
- .card-prompt {
677
- font-size: 11px;
678
- line-height: 1.4;
679
- color: #666;
680
- display: -webkit-box;
681
- -webkit-line-clamp: 6;
682
- -webkit-box-orient: vertical;
683
- overflow: hidden;
684
- height: 90px;
685
- background-color: #f8f9fa;
686
- padding: 8px;
687
- border-radius: 4px;
688
- }
689
- .template-section {
690
- display: none;
691
- }
692
- .template-section.active {
693
- display: block;
694
- }
695
- </style>
696
- <div class="template-nav">
697
- <button class="template-btn" onclick="showTemplate('best')">🏆 베스트</button>
698
- <button class="template-btn" onclick="showTemplate('trending')">🔥 트렌딩</button>
699
- <button class="template-btn" onclick="showTemplate('new')">✨ NEW</button>
700
- </div>
701
- """
 
702
  for section, items in templates.items():
703
  html_content += f"""
704
- <div class="template-section" id="{section}-templates">
705
- <div class="prompt-grid">
706
- """
707
  for item in items:
708
- html_content += f"""
709
- <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
710
- <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
711
- <div class="card-name">{html.escape(item.get('name', ''))}</div>
712
- <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
713
- </div>
714
- """
715
- html_content += "</div></div>"
716
- html_content += """
717
- <script>
718
- function copyToInput(card) {
719
- const prompt = card.dataset.prompt;
720
- const textarea = document.querySelector('.ant-input-textarea-large textarea');
721
- if (textarea) {
722
- textarea.value = prompt;
723
- textarea.dispatchEvent(new Event('input', { bubbles: true }));
724
- document.querySelector('.session-drawer .close-btn').click();
725
- }
726
- }
727
- function showTemplate(type) {
728
- document.querySelectorAll('.template-section').forEach(section => {
729
- section.style.display = 'none';
730
- });
731
- document.querySelectorAll('.template-btn').forEach(btn => {
732
- btn.classList.remove('active');
733
- });
734
- document.getElementById(type + '-templates').style.display = 'block';
735
- event.target.classList.add('active');
736
- }
737
- document.addEventListener('DOMContentLoaded', function() {
738
- showTemplate('best');
739
- document.querySelector('.template-btn').classList.add('active');
740
- });
741
- </script>
742
- """
 
 
 
 
 
 
 
 
743
  return gr.HTML(value=html_content)
744
- except Exception as e:
745
  return gr.HTML("Error loading templates")
746
 
747
  def generate_space_name():
 
748
  letters = string.ascii_lowercase
749
  return ''.join(random.choice(letters) for i in range(6))
750
 
751
  def deploy_to_vercel(code: str):
 
 
 
752
  try:
753
- token = "A8IFZmgW2cqA4yUNlLPnci0N"
754
  if not token:
755
  return "Vercel 토큰이 설정되지 않았습니다."
 
756
  project_name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
757
  deploy_url = "https://api.vercel.com/v13/deployments"
758
  headers = {
@@ -804,22 +697,25 @@ def deploy_to_vercel(code: str):
804
  return f"배포 중 오류 발생: {str(e)}"
805
 
806
  def boost_prompt(prompt: str) -> str:
 
 
 
807
  if not prompt:
808
  return ""
809
- boost_system_prompt = """
810
- 당신은 게임 개발 프롬프트 전문가입니다.
811
- 주어진 프롬프트를 분석하여 상세하고 전문적인 요구사항으로 확장하되,
812
- 원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
813
-
814
- 1. 게임 플레이 재미와 난이도 밸런스
815
- 2. 인터랙티브 그래픽 애니메이션
816
- 3. 사용자 경험 최적화 (UI/UX)
817
- 4. 성능 최적화
818
- 5. 접근성과 호환성
819
-
820
- 기존 SystemPrompt의 모든 규칙을 준수하면서 증강된 프롬프트를 생성하십시오.
821
- """
822
  try:
 
823
  try:
824
  response = claude_client.messages.create(
825
  model="claude-3-7-sonnet-20250219",
@@ -832,7 +728,8 @@ def boost_prompt(prompt: str) -> str:
832
  if hasattr(response, 'content') and len(response.content) > 0:
833
  return response.content[0].text
834
  raise Exception("Claude API 응답 형식 오류")
835
- except Exception as claude_error:
 
836
  completion = openai_client.chat.completions.create(
837
  model="gpt-4",
838
  messages=[
@@ -845,32 +742,132 @@ def boost_prompt(prompt: str) -> str:
845
  if completion.choices and len(completion.choices) > 0:
846
  return completion.choices[0].message.content
847
  raise Exception("OpenAI API 응답 형식 오류")
848
- except Exception as e:
 
849
  return prompt
850
 
851
  def handle_boost(prompt: str):
852
  try:
853
  boosted_prompt = boost_prompt(prompt)
854
  return boosted_prompt, gr.update(active_key="empty")
855
- except Exception as e:
856
  return prompt, gr.update(active_key="empty")
857
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  demo_instance = Demo()
 
859
 
860
  with gr.Blocks(css_paths="app.css", theme=theme) as demo:
861
  history = gr.State([])
862
- setting = gr.State({
863
- "system": SystemPrompt,
864
- })
865
 
866
  with ms.Application() as app:
867
  with antd.ConfigProvider():
 
 
868
  with antd.Drawer(open=False, title="code", placement="left", width="750px") as code_drawer:
869
  code_output = legacy.Markdown()
870
-
 
871
  with antd.Drawer(open=False, title="history", placement="left", width="900px") as history_drawer:
872
- history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot")
 
 
873
 
 
874
  with antd.Drawer(
875
  open=False,
876
  title="Templates",
@@ -880,19 +877,13 @@ with gr.Blocks(css_paths="app.css", theme=theme) as demo:
880
  ) as session_drawer:
881
  with antd.Flex(vertical=True, gap="middle"):
882
  gr.Markdown("### Available Game Templates")
883
- session_history = gr.HTML(
884
- elem_classes="session-history"
885
- )
886
- close_btn = antd.Button(
887
- "Close",
888
- type="default",
889
- elem_classes="close-btn"
890
- )
891
-
892
- # 여기서부터 레이아웃 순서를 변경 (좌측: 미리보기 / 우측: 입력 및 버튼)
893
  with antd.Row(gutter=[32, 12]) as layout:
894
 
895
- # 왼쪽 Col (미리보기)
896
  with antd.Col(span=24, md=16):
897
  with ms.Div(elem_classes="right_panel"):
898
  with antd.Flex(gap="small", elem_classes="setting-buttons"):
@@ -902,24 +893,30 @@ with gr.Blocks(css_paths="app.css", theme=theme) as demo:
902
  trending_btn = antd.Button("🔥 트렌딩 템플릿", type="default")
903
  new_btn = antd.Button("✨ NEW 템플릿", type="default")
904
 
905
- gr.HTML('<div class="render_header"><span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span></div>')
 
 
 
 
 
 
906
 
907
  with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
908
  with antd.Tabs.Item(key="empty"):
909
  empty = antd.Empty(description="empty input", elem_classes="right_content")
910
  with antd.Tabs.Item(key="loading"):
911
- loading = antd.Spin(True, tip="coding...", size="large", elem_classes="right_content")
 
 
912
  with antd.Tabs.Item(key="render"):
913
  sandbox = gr.HTML(elem_classes="html_content")
914
 
915
- # 오른쪽 Col (입력 액션)
916
  with antd.Col(span=24, md=8):
917
  with antd.Flex(vertical=True, gap="middle", wrap=True):
918
- # 제거 요청된 상단 박스(로고 등)를 완전히 삭제
919
-
920
- input = antd.InputTextarea(
921
- size="large",
922
- allow_clear=True,
923
  placeholder=random.choice(DEMO_LIST)['description']
924
  )
925
 
@@ -932,7 +929,9 @@ with gr.Blocks(css_paths="app.css", theme=theme) as demo:
932
 
933
  deploy_result = gr.HTML(label="배포 결과")
934
 
935
- # 이벤트 연결
 
 
936
  def execute_code(query: str):
937
  if not query or query.strip() == '':
938
  return None, gr.update(active_key="empty")
@@ -942,97 +941,101 @@ with gr.Blocks(css_paths="app.css", theme=theme) as demo:
942
  else:
943
  code = query.strip()
944
  return send_to_sandbox(code), gr.update(active_key="render")
945
- except Exception as e:
946
  return None, gr.update(active_key="empty")
947
 
948
  execute_btn.click(
949
  fn=execute_code,
950
- inputs=[input],
951
  outputs=[sandbox, state_tab]
952
  )
953
 
 
954
  codeBtn.click(
955
  lambda: gr.update(open=True),
956
  inputs=[],
957
  outputs=[code_drawer]
958
  )
959
-
960
  code_drawer.close(
961
  lambda: gr.update(open=False),
962
- inputs=[],
963
  outputs=[code_drawer]
964
  )
965
 
 
966
  historyBtn.click(
967
  history_render,
968
  inputs=[history],
969
  outputs=[history_drawer, history_output]
970
  )
971
-
972
  history_drawer.close(
973
  lambda: gr.update(open=False),
974
  inputs=[],
975
  outputs=[history_drawer]
976
  )
977
 
 
978
  best_btn.click(
979
  fn=lambda: (gr.update(open=True), load_best_templates()),
980
  outputs=[session_drawer, session_history],
981
  queue=False
982
  )
983
-
984
  trending_btn.click(
985
  fn=lambda: (gr.update(open=True), load_trending_templates()),
986
  outputs=[session_drawer, session_history],
987
  queue=False
988
  )
989
-
990
  new_btn.click(
991
  fn=lambda: (gr.update(open=True), load_new_templates()),
992
  outputs=[session_drawer, session_history],
993
  queue=False
994
  )
995
 
 
996
  session_drawer.close(
997
  lambda: (gr.update(open=False), gr.HTML("")),
998
  outputs=[session_drawer, session_history]
999
  )
1000
-
1001
  close_btn.click(
1002
  lambda: (gr.update(open=False), gr.HTML("")),
1003
  outputs=[session_drawer, session_history]
1004
  )
1005
 
 
1006
  btn.click(
1007
  demo_instance.generation_code,
1008
- inputs=[input, setting, history],
1009
  outputs=[code_output, history, sandbox, state_tab, code_drawer]
1010
  )
1011
 
 
1012
  clear_btn.click(
1013
  demo_instance.clear_history,
1014
  inputs=[],
1015
  outputs=[history]
1016
  )
1017
 
 
1018
  boost_btn.click(
1019
  fn=handle_boost,
1020
- inputs=[input],
1021
- outputs=[input, state_tab]
1022
  )
1023
 
 
1024
  deploy_btn.click(
1025
  fn=lambda code: deploy_to_vercel(remove_code_block(code)) if code else "코드가 없습니다.",
1026
  inputs=[code_output],
1027
  outputs=[deploy_result]
1028
  )
1029
 
 
1030
  if __name__ == "__main__":
1031
  try:
1032
  demo_instance = Demo()
 
 
1033
  demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
1034
  except Exception as e:
 
1035
  raise
1036
- </script>
1037
- </body>
1038
- </html>
 
1
+ # app.py
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
3
  import os
4
  import re
5
  import random
6
+ import time
7
+ import html
8
  import base64
9
+ import string
10
+ import json
11
+ import asyncio
12
+ import requests
13
  import anthropic
14
  import openai
15
+
16
+ from http import HTTPStatus
17
+ from typing import Dict, List, Optional, Tuple
18
  from functools import partial
19
+
20
  import gradio as gr
21
  import modelscope_studio.components.base as ms
22
  import modelscope_studio.components.legacy as legacy
23
  import modelscope_studio.components.antd as antd
24
 
25
+ # DEMO_LIST 직접 정의
 
 
 
 
 
26
  DEMO_LIST = [
27
  {"description": "Create a Tetris-like puzzle game with arrow key controls, line-clearing mechanics, and increasing difficulty levels."},
28
  {"description": "Build an interactive Chess game with a basic AI opponent and drag-and-drop piece movement. Keep track of moves and detect check/checkmate."},
 
60
  {"description": "Create a small action RPG with WASD movement, an attack button, special moves, leveling, and item drops."},
61
  ]
62
 
63
+ # SystemPrompt 정의
64
  SystemPrompt = """너의 이름은 'MOUSE'이다. You are an expert web game developer with a strong focus on gameplay mechanics, interactive design, and performance optimization.
65
  Your mission is to create compelling, modern, and fully interactive web-based games using HTML, JavaScript, and CSS.
66
  This code will be rendered directly in the browser.
 
93
  History = List[Tuple[str, str]]
94
  Messages = List[Dict[str, str]]
95
 
96
+ # 이미지 캐싱
97
  IMAGE_CACHE = {}
98
 
99
  def get_image_base64(image_path):
100
+ """
101
+ 이미지 파일을 base64로 읽어서 캐싱
102
+ """
103
  if image_path in IMAGE_CACHE:
104
  return IMAGE_CACHE[image_path]
105
  try:
 
124
  history.append([q['content'], r['content']])
125
  return history
126
 
127
+ # API 토큰
128
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
129
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
130
 
 
132
  openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
133
 
134
  async def try_claude_api(system_message, claude_messages, timeout=15):
135
+ """
136
+ Claude API 호출 (스트리밍)
137
+ """
138
  try:
139
  start_time = time.time()
140
  with claude_client.messages.stream(
 
157
  raise e
158
 
159
  async def try_openai_api(openai_messages):
160
+ """
161
+ OpenAI API 호출 (스트리밍)
162
+ """
163
  try:
164
  stream = openai_client.chat.completions.create(
165
  model="gpt-4o",
 
176
  except Exception as e:
177
  raise e
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  def remove_code_block(text):
180
+ """
181
+ 메시지 내의 ```html ... ``` 부분만 추출하여 반환
182
+ """
183
  pattern = r'```html\n(.+?)\n```'
184
  match = re.search(pattern, text, re.DOTALL)
185
  if match:
 
187
  else:
188
  return text.strip()
189
 
 
 
 
190
  def send_to_sandbox(code):
191
+ """
192
+ HTML 코드를 iframe으로 렌더링하기 위한 data URI 생성
193
+ """
194
  encoded_html = base64.b64encode(code.encode('utf-8')).decode('utf-8')
195
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
196
+ return f"<iframe src=\"{data_uri}\" width=\"100%\" height=\"920px\" style=\"border:none;\"></iframe>"
197
 
198
+ def history_render(history: History):
199
+ """
200
+ 히스토리 Drawer 열고, Chatbot UI에 히스토리 반영
201
+ """
202
+ return gr.update(open=True), history
203
+
204
+ # ------------------
205
+ # Template Data 관련
206
+ # ------------------
207
 
208
  def load_json_data():
209
+ # 하드코딩된 템플릿 데이터
210
  return [
211
  {
212
  "name": "[게임] 테트리스 클론",
 
393
  return create_template_html("✨ NEW 게임 템플릿", json_data)
394
 
395
  def create_template_html(title, items):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  """
397
+ 카드형 UI로 템플릿 목록을 표시하는 HTML
398
+ """
399
+ # 모든 CSS/HTML을 문자열로 안전하게 감싸기
400
+ html_content = r"""
401
+ <style>
402
+ .prompt-grid {
403
+ display: grid;
404
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
405
+ gap: 20px;
406
+ padding: 20px;
407
+ }
408
+ .prompt-card {
409
+ background: white;
410
+ border: 1px solid #eee;
411
+ border-radius: 8px;
412
+ padding: 15px;
413
+ cursor: pointer;
414
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
415
+ }
416
+ .prompt-card:hover {
417
+ transform: translateY(-2px);
418
+ transition: transform 0.2s;
419
+ }
420
+ .card-image {
421
+ width: 100%;
422
+ height: 180px;
423
+ object-fit: cover;
424
+ border-radius: 4px;
425
+ margin-bottom: 10px;
426
+ }
427
+ .card-name {
428
+ font-weight: bold;
429
+ margin-bottom: 8px;
430
+ font-size: 16px;
431
+ color: #333;
432
+ }
433
+ .card-prompt {
434
+ font-size: 11px;
435
+ line-height: 1.4;
436
+ color: #666;
437
+ display: -webkit-box;
438
+ -webkit-line-clamp: 6;
439
+ -webkit-box-orient: vertical;
440
+ overflow: hidden;
441
+ height: 90px;
442
+ background-color: #f8f9fa;
443
+ padding: 8px;
444
+ border-radius: 4px;
445
+ }
446
+ </style>
447
+ <div class="prompt-grid">
448
+ """
449
  for item in items:
450
+ card_html = f"""
451
+ <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
452
+ <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
453
+ <div class="card-name">{html.escape(item.get('name', ''))}</div>
454
+ <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
455
+ </div>
456
+ """
457
+ html_content += card_html
458
+ html_content += r"""
459
+ <script>
460
+ function copyToInput(card) {
461
+ const prompt = card.dataset.prompt;
462
+ const textarea = document.querySelector('.ant-input-textarea-large textarea');
463
+ if (textarea) {
464
+ textarea.value = prompt;
465
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
466
+ document.querySelector('.session-drawer .close-btn').click();
467
  }
468
+ }
469
+ </script>
470
+ </div>
471
+ """
472
  return gr.HTML(value=html_content)
473
 
474
  TEMPLATE_CACHE = None
475
 
476
  def load_session_history(template_type="best"):
477
+ """
478
+ 오른쪽 Drawer에 표시할 템플릿들(베스트/트렌딩/NEW)을 보여주는 HTML
479
+ """
480
  try:
481
  json_data = load_json_data()
482
  templates = {
 
490
  "new": "✨ NEW 게임 템플릿"
491
  }
492
 
493
+ html_content = r"""
494
+ <style>
495
+ .template-nav {
496
+ display: flex;
497
+ gap: 10px;
498
+ margin: 20px;
499
+ position: sticky;
500
+ top: 0;
501
+ background: white;
502
+ z-index: 100;
503
+ padding: 10px 0;
504
+ border-bottom: 1px solid #eee;
505
+ }
506
+ .template-btn {
507
+ padding: 8px 16px;
508
+ border: 1px solid #1890ff;
509
+ border-radius: 4px;
510
+ cursor: pointer;
511
+ background: white;
512
+ color: #1890ff;
513
+ font-weight: bold;
514
+ transition: all 0.3s;
515
+ }
516
+ .template-btn:hover {
517
+ background: #1890ff;
518
+ color: white;
519
+ }
520
+ .template-btn.active {
521
+ background: #1890ff;
522
+ color: white;
523
+ }
524
+ .prompt-grid {
525
+ display: grid;
526
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
527
+ gap: 20px;
528
+ padding: 20px;
529
+ }
530
+ .prompt-card {
531
+ background: white;
532
+ border: 1px solid #eee;
533
+ border-radius: 8px;
534
+ padding: 15px;
535
+ cursor: pointer;
536
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
537
+ }
538
+ .prompt-card:hover {
539
+ transform: translateY(-2px);
540
+ transition: transform 0.2s;
541
+ }
542
+ .card-image {
543
+ width: 100%;
544
+ height: 180px;
545
+ object-fit: cover;
546
+ border-radius: 4px;
547
+ margin-bottom: 10px;
548
+ }
549
+ .card-name {
550
+ font-weight: bold;
551
+ margin-bottom: 8px;
552
+ font-size: 16px;
553
+ color: #333;
554
+ }
555
+ .card-prompt {
556
+ font-size: 11px;
557
+ line-height: 1.4;
558
+ color: #666;
559
+ display: -webkit-box;
560
+ -webkit-line-clamp: 6;
561
+ -webkit-box-orient: vertical;
562
+ overflow: hidden;
563
+ height: 90px;
564
+ background-color: #f8f9fa;
565
+ padding: 8px;
566
+ border-radius: 4px;
567
+ }
568
+ .template-section {
569
+ display: none;
570
+ }
571
+ .template-section.active {
572
+ display: block;
573
+ }
574
+ </style>
575
+ <div class="template-nav">
576
+ <button class="template-btn" onclick="showTemplate('best')">🏆 베스트</button>
577
+ <button class="template-btn" onclick="showTemplate('trending')">🔥 트렌딩</button>
578
+ <button class="template-btn" onclick="showTemplate('new')">✨ NEW</button>
579
+ </div>
580
+ """
581
+ # 섹션별 템플릿 생성
582
  for section, items in templates.items():
583
  html_content += f"""
584
+ <div class="template-section" id="{section}-templates">
585
+ <div class="prompt-grid">
586
+ """
587
  for item in items:
588
+ card_html = f"""
589
+ <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html.escape(item.get('prompt', ''))}">
590
+ <img src="{item.get('image_url', '')}" class="card-image" loading="lazy" alt="{html.escape(item.get('name', ''))}">
591
+ <div class="card-name">{html.escape(item.get('name', ''))}</div>
592
+ <div class="card-prompt">{html.escape(item.get('prompt', ''))}</div>
593
+ </div>
594
+ """
595
+ html_content += card_html
596
+ html_content += """
597
+ </div>
598
+ </div>
599
+ """
600
+ html_content += r"""
601
+ <script>
602
+ function copyToInput(card) {
603
+ const prompt = card.dataset.prompt;
604
+ const textarea = document.querySelector('.ant-input-textarea-large textarea');
605
+ if (textarea) {
606
+ textarea.value = prompt;
607
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
608
+ document.querySelector('.session-drawer .close-btn').click();
609
+ }
610
+ }
611
+ function showTemplate(type) {
612
+ // 모든 섹션 숨기기
613
+ document.querySelectorAll('.template-section').forEach(section => {
614
+ section.style.display = 'none';
615
+ });
616
+ // 모든 버튼 비활성화
617
+ document.querySelectorAll('.template-btn').forEach(btn => {
618
+ btn.classList.remove('active');
619
+ });
620
+ // 선택된 섹션 보이기
621
+ document.getElementById(type + '-templates').style.display = 'block';
622
+ // 선택된 버튼 활성화
623
+ event.target.classList.add('active');
624
+ }
625
+ document.addEventListener('DOMContentLoaded', function() {
626
+ showTemplate('best');
627
+ document.querySelector('.template-btn').classList.add('active');
628
+ });
629
+ </script>
630
+ """
631
  return gr.HTML(value=html_content)
632
+ except Exception:
633
  return gr.HTML("Error loading templates")
634
 
635
  def generate_space_name():
636
+ """6자리 랜덤 영문 이름 생성"""
637
  letters = string.ascii_lowercase
638
  return ''.join(random.choice(letters) for i in range(6))
639
 
640
  def deploy_to_vercel(code: str):
641
+ """
642
+ Vercel에 배포하는 함수 (예시)
643
+ """
644
  try:
645
+ token = "A8IFZmgW2cqA4yUNlLPnci0N" # 실제 토큰 필요
646
  if not token:
647
  return "Vercel 토큰이 설정되지 않았습니다."
648
+
649
  project_name = ''.join(random.choice(string.ascii_lowercase) for i in range(6))
650
  deploy_url = "https://api.vercel.com/v13/deployments"
651
  headers = {
 
697
  return f"배포 중 오류 발생: {str(e)}"
698
 
699
  def boost_prompt(prompt: str) -> str:
700
+ """
701
+ 'Boost' 버튼 눌렀을 때 프롬프트를 좀 더 풍부하게 생성하는 함수 (예시)
702
+ """
703
  if not prompt:
704
  return ""
705
+ boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다.
706
+ 주어진 프롬프트를 분석하여 상세하고 전문적인 요구사항으로 확장하되,
707
+ 원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
708
+
709
+ 1. 게임 플레이 재미와 난이도 밸런스
710
+ 2. 인터랙티브 그래픽 애니메이션
711
+ 3. 사용자 경험 최적화 (UI/UX)
712
+ 4. 성능 최적화
713
+ 5. 접근성과 호환성
714
+
715
+ 기존 SystemPrompt의 모든 규칙을 준수하면서 증강된 프롬프트를 생성하십시오.
716
+ """
 
717
  try:
718
+ # Claude API 시도
719
  try:
720
  response = claude_client.messages.create(
721
  model="claude-3-7-sonnet-20250219",
 
728
  if hasattr(response, 'content') and len(response.content) > 0:
729
  return response.content[0].text
730
  raise Exception("Claude API 응답 형식 오류")
731
+ except Exception:
732
+ # OpenAI API로 fallback
733
  completion = openai_client.chat.completions.create(
734
  model="gpt-4",
735
  messages=[
 
742
  if completion.choices and len(completion.choices) > 0:
743
  return completion.choices[0].message.content
744
  raise Exception("OpenAI API 응답 형식 오류")
745
+ except Exception:
746
+ # 실패 시 원본 그대로 반환
747
  return prompt
748
 
749
  def handle_boost(prompt: str):
750
  try:
751
  boosted_prompt = boost_prompt(prompt)
752
  return boosted_prompt, gr.update(active_key="empty")
753
+ except Exception:
754
  return prompt, gr.update(active_key="empty")
755
 
756
+ class Demo:
757
+ """
758
+ Main Demo 클래스
759
+ """
760
+ def __init__(self):
761
+ pass
762
+
763
+ async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
764
+ if not query or query.strip() == '':
765
+ query = random.choice(DEMO_LIST)['description']
766
+
767
+ if _history is None:
768
+ _history = []
769
+
770
+ messages = history_to_messages(_history, _setting['system'])
771
+ system_message = messages[0]['content']
772
+
773
+ claude_messages = [
774
+ {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
775
+ for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
776
+ if msg["content"].strip() != ''
777
+ ]
778
+
779
+ openai_messages = [{"role": "system", "content": system_message}]
780
+ for msg in messages[1:]:
781
+ openai_messages.append({
782
+ "role": msg["role"],
783
+ "content": msg["content"]
784
+ })
785
+ openai_messages.append({"role": "user", "content": query})
786
+
787
+ try:
788
+ # 우선 "Generating code..." 출력
789
+ yield [
790
+ "Generating code...",
791
+ _history,
792
+ None,
793
+ gr.update(active_key="loading"),
794
+ gr.update(open=True)
795
+ ]
796
+ await asyncio.sleep(0)
797
+
798
+ collected_content = None
799
+ try:
800
+ # Claude API 시도
801
+ async for content in try_claude_api(system_message, claude_messages):
802
+ yield [
803
+ content,
804
+ _history,
805
+ None,
806
+ gr.update(active_key="loading"),
807
+ gr.update(open=True)
808
+ ]
809
+ await asyncio.sleep(0)
810
+ collected_content = content
811
+ except Exception:
812
+ # OpenAI fallback
813
+ async for content in try_openai_api(openai_messages):
814
+ yield [
815
+ content,
816
+ _history,
817
+ None,
818
+ gr.update(active_key="loading"),
819
+ gr.update(open=True)
820
+ ]
821
+ await asyncio.sleep(0)
822
+ collected_content = content
823
+
824
+ if collected_content:
825
+ _history = messages_to_history([
826
+ {'role': Role.SYSTEM, 'content': system_message}
827
+ ] + claude_messages + [{
828
+ 'role': Role.ASSISTANT,
829
+ 'content': collected_content
830
+ }])
831
+
832
+ # 최종 결과 출력 (sandbox 렌더링)
833
+ yield [
834
+ collected_content,
835
+ _history,
836
+ send_to_sandbox(remove_code_block(collected_content)),
837
+ gr.update(active_key="render"),
838
+ gr.update(open=True)
839
+ ]
840
+ else:
841
+ raise ValueError("No content was generated from either API")
842
+ except Exception as e:
843
+ raise ValueError(f'Error calling APIs: {str(e)}')
844
+
845
+ def clear_history(self):
846
+ return []
847
+
848
+ # ----------- Gradio / Modelscope UI 빌드 -----------
849
+
850
  demo_instance = Demo()
851
+ theme = gr.themes.Soft()
852
 
853
  with gr.Blocks(css_paths="app.css", theme=theme) as demo:
854
  history = gr.State([])
855
+ setting = gr.State({"system": SystemPrompt})
 
 
856
 
857
  with ms.Application() as app:
858
  with antd.ConfigProvider():
859
+
860
+ # code Drawer
861
  with antd.Drawer(open=False, title="code", placement="left", width="750px") as code_drawer:
862
  code_output = legacy.Markdown()
863
+
864
+ # history Drawer
865
  with antd.Drawer(open=False, title="history", placement="left", width="900px") as history_drawer:
866
+ history_output = legacy.Chatbot(
867
+ show_label=False, flushing=False, height=960, elem_classes="history_chatbot"
868
+ )
869
 
870
+ # templates Drawer
871
  with antd.Drawer(
872
  open=False,
873
  title="Templates",
 
877
  ) as session_drawer:
878
  with antd.Flex(vertical=True, gap="middle"):
879
  gr.Markdown("### Available Game Templates")
880
+ session_history = gr.HTML(elem_classes="session-history")
881
+ close_btn = antd.Button("Close", type="default", elem_classes="close-btn")
882
+
883
+ # 레이아웃(좌측: 미리보기 / 우측: 입력 + 버튼들)
 
 
 
 
 
 
884
  with antd.Row(gutter=[32, 12]) as layout:
885
 
886
+ # 왼쪽 Col: 미리보기
887
  with antd.Col(span=24, md=16):
888
  with ms.Div(elem_classes="right_panel"):
889
  with antd.Flex(gap="small", elem_classes="setting-buttons"):
 
893
  trending_btn = antd.Button("🔥 트렌딩 템플릿", type="default")
894
  new_btn = antd.Button("✨ NEW 템플릿", type="default")
895
 
896
+ gr.HTML(r"""
897
+ <div class="render_header">
898
+ <span class="header_btn"></span>
899
+ <span class="header_btn"></span>
900
+ <span class="header_btn"></span>
901
+ </div>
902
+ """)
903
 
904
  with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
905
  with antd.Tabs.Item(key="empty"):
906
  empty = antd.Empty(description="empty input", elem_classes="right_content")
907
  with antd.Tabs.Item(key="loading"):
908
+ loading = antd.Spin(
909
+ True, tip="coding...", size="large", elem_classes="right_content"
910
+ )
911
  with antd.Tabs.Item(key="render"):
912
  sandbox = gr.HTML(elem_classes="html_content")
913
 
914
+ # 오른쪽 Col: 입력부 + 버튼들
915
  with antd.Col(span=24, md=8):
916
  with antd.Flex(vertical=True, gap="middle", wrap=True):
917
+ input_text = antd.InputTextarea(
918
+ size="large",
919
+ allow_clear=True,
 
 
920
  placeholder=random.choice(DEMO_LIST)['description']
921
  )
922
 
 
929
 
930
  deploy_result = gr.HTML(label="배포 결과")
931
 
932
+ # ---- 이벤트 / 콜백 등록 ----
933
+
934
+ # 'Code실행' 버튼: 입력창에 있는 내용을 iframe 실행
935
  def execute_code(query: str):
936
  if not query or query.strip() == '':
937
  return None, gr.update(active_key="empty")
 
941
  else:
942
  code = query.strip()
943
  return send_to_sandbox(code), gr.update(active_key="render")
944
+ except Exception:
945
  return None, gr.update(active_key="empty")
946
 
947
  execute_btn.click(
948
  fn=execute_code,
949
+ inputs=[input_text],
950
  outputs=[sandbox, state_tab]
951
  )
952
 
953
+ # 코드 Drawer 열기 / 닫기
954
  codeBtn.click(
955
  lambda: gr.update(open=True),
956
  inputs=[],
957
  outputs=[code_drawer]
958
  )
 
959
  code_drawer.close(
960
  lambda: gr.update(open=False),
961
+ inputs=[],
962
  outputs=[code_drawer]
963
  )
964
 
965
+ # 히스토리 Drawer 열기 / 닫기
966
  historyBtn.click(
967
  history_render,
968
  inputs=[history],
969
  outputs=[history_drawer, history_output]
970
  )
 
971
  history_drawer.close(
972
  lambda: gr.update(open=False),
973
  inputs=[],
974
  outputs=[history_drawer]
975
  )
976
 
977
+ # 템플릿 Drawer 열기 (베스트/트렌딩/NEW)
978
  best_btn.click(
979
  fn=lambda: (gr.update(open=True), load_best_templates()),
980
  outputs=[session_drawer, session_history],
981
  queue=False
982
  )
 
983
  trending_btn.click(
984
  fn=lambda: (gr.update(open=True), load_trending_templates()),
985
  outputs=[session_drawer, session_history],
986
  queue=False
987
  )
 
988
  new_btn.click(
989
  fn=lambda: (gr.update(open=True), load_new_templates()),
990
  outputs=[session_drawer, session_history],
991
  queue=False
992
  )
993
 
994
+ # 템플릿 Drawer 닫기
995
  session_drawer.close(
996
  lambda: (gr.update(open=False), gr.HTML("")),
997
  outputs=[session_drawer, session_history]
998
  )
 
999
  close_btn.click(
1000
  lambda: (gr.update(open=False), gr.HTML("")),
1001
  outputs=[session_drawer, session_history]
1002
  )
1003
 
1004
+ # 'Send' 버튼: 코드 생성 (Claude/OpenAI)
1005
  btn.click(
1006
  demo_instance.generation_code,
1007
+ inputs=[input_text, setting, history],
1008
  outputs=[code_output, history, sandbox, state_tab, code_drawer]
1009
  )
1010
 
1011
+ # '클리어' 버튼: 히스토리 초기화
1012
  clear_btn.click(
1013
  demo_instance.clear_history,
1014
  inputs=[],
1015
  outputs=[history]
1016
  )
1017
 
1018
+ # 'Boost' 버튼: 프롬프트 보강
1019
  boost_btn.click(
1020
  fn=handle_boost,
1021
+ inputs=[input_text],
1022
+ outputs=[input_text, state_tab]
1023
  )
1024
 
1025
+ # '배포' 버튼: Vercel 배포
1026
  deploy_btn.click(
1027
  fn=lambda code: deploy_to_vercel(remove_code_block(code)) if code else "코드가 없습니다.",
1028
  inputs=[code_output],
1029
  outputs=[deploy_result]
1030
  )
1031
 
1032
+ # 실제 실행부
1033
  if __name__ == "__main__":
1034
  try:
1035
  demo_instance = Demo()
1036
+ # Gradio/Modelscope 앱 실행
1037
+ # 포트나 설정은 환경에 맞게 수정 가능
1038
  demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
1039
  except Exception as e:
1040
+ print(f"Initialization error: {e}")
1041
  raise