openfree commited on
Commit
d3166d2
·
verified ·
1 Parent(s): e4ea15b

Create app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +419 -570
app-backup.py CHANGED
@@ -34,73 +34,29 @@ def get_logs():
34
 
35
  import re
36
 
37
- def get_deployment_update(code):
38
- logger.debug(f"[get_deployment_update] 받은 code 길이: {len(code) if code else 0}")
39
- if not code or len(code.strip()) < 10:
40
- logger.info("[get_deployment_update] 코드가 너무 짧아 배포를 진행하지 않습니다.")
41
- return gr.update(
42
- value='<div style="color: orange;">⚠️ 배포할 코드가 없습니다. 먼저 게임을 생성하거나 코드를 입력해주세요.</div>',
43
- visible=True
44
- )
45
 
46
- clean_code = remove_code_block(code)
47
- logger.debug(f"[get_deployment_update] 코드 블록 제거 후 code 길이: {len(clean_code)}")
48
-
49
- # 배포 중임을 알리는 메시지 표시
50
- yield gr.update(
51
- value='<div class="deploy-loading"><div class="loading-spinner"></div> <span class="loading-message">🚀 Vercel에 배포 중입니다... 잠시만 기다려주세요...</span></div>',
52
- visible=True
53
- )
54
-
55
- # 실제 배포 함수 호출 (시간이 걸릴 수 있으므로 비동기적으로 처리되거나, Gradio의 yield를 통해 중간 업데이트)
56
- # deploy_to_vercel이 동기 함수이므로, 이 부분에서 UI가 멈출 수 있습니다.
57
- # 더 나은 사용자 경험을 위해서는 deploy_to_vercel을 비동기화하거나 별도 스레드에서 실행해야 하지만,
58
- # 현재 구조에서는 일단 동기 호출로 진행합니다.
59
- result = deploy_to_vercel(clean_code)
60
- logger.info(f"[get_deployment_update] deploy_to_vercel 결과: {result}")
61
-
62
- # 배포 URL 추출
63
- match = re.search(r'https?://[\\w\\-]+\\.vercel\\.app', result)
64
- if match:
65
- url = match.group(0)
66
- logger.info(f"[get_deployment_update] 배포 URL 추출 성공: {url}")
67
- # HTML 앵커로 링크 표시
68
- final_html = f"""
69
- <div class="deploy-success">
70
- <span class="success-icon">✅</span>
71
- <span class="success-message">배포 성공! 앱이 준비되었습니다.</span>
72
- </div>
73
- <div class="url-box">
74
- <a href="{url}" target="_blank">{url}</a>
75
- <button class="copy-btn" onclick="navigator.clipboard.writeText('{url}')">복사</button>
76
- </div>
77
- """
78
- yield gr.update(
79
- value=final_html,
80
- visible=True
81
  )
82
- else:
83
- logger.warning("[get_deployment_update] 배포 URL을 찾을 수 없습니다.")
84
- error_message = html.escape(result)
85
- # 오류 메시지에 Vercel 로그가 포함될 수 있으므로, 너무 길지 않게 자릅니다.
86
- if len(error_message) > 500:
87
- error_message = error_message[:500] + "..."
88
-
89
- final_html = f"""
90
- <div class="deploy-error">
91
- <span class="error-icon">❌</span>
92
- <span class="error-message">배포 URL을 찾을 수 없습니다.</span>
93
- </div>
94
- <div style="font-size: 0.9em; color: var(--text-secondary); margin-top: 5px;">
95
- <strong>Vercel 응답:</strong><br>
96
- <pre style="white-space: pre-wrap; word-break: break-all; max-height: 100px; overflow-y: auto; background-color: #f0f0f0; padding: 5px; border-radius: 4px;">{error_message}</pre>
97
- </div>
98
- """
99
- yield gr.update(
100
- value=final_html,
101
- visible=True
102
  )
103
 
 
 
 
104
 
105
  # ------------------------
106
  # 1) DEMO_LIST 및 SystemPrompt
@@ -293,8 +249,8 @@ async def try_claude_api(system_message, claude_messages, timeout=15):
293
 
294
  start_time = time.time()
295
  with claude_client.messages.stream(
296
- model="claude-3-opus-20240229", # Sonnet 대신 Opus 사용 또는 최신 Sonnet 모델명 확인
297
- max_tokens=4096, # Claude-3 모델의 최대 토큰 수에 맞게 조정
298
  system=system_message_with_limit,
299
  messages=claude_messages,
300
  temperature=0.3,
@@ -307,16 +263,9 @@ async def try_claude_api(system_message, claude_messages, timeout=15):
307
  if chunk.type == "content_block_delta":
308
  collected_content += chunk.delta.text
309
  yield collected_content
310
- await asyncio.sleep(0) # CPU 양보
311
- # stream.text_stream 사용 시 아래와 같이 처리 가능
312
- # for text in stream.text_stream:
313
- # collected_content += text
314
- # yield collected_content
315
- # await asyncio.sleep(0)
316
- # 마지막으로 수집된 전체 콘텐츠 반환 (필요시)
317
- # yield collected_content
318
  except Exception as e:
319
- logger.error(f"Claude API Error: {e}")
320
  raise e
321
 
322
  async def try_openai_api(openai_messages):
@@ -328,10 +277,10 @@ async def try_openai_api(openai_messages):
328
  openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석은 최소화하고, 핵심 기능만 구현하세요."
329
 
330
  stream = openai_client.chat.completions.create(
331
- model="gpt-4o", # 또는 "gpt-4-turbo" 등 최신/적절한 모델
332
  messages=openai_messages,
333
  stream=True,
334
- max_tokens=4096, # 모델 최대 토큰 수에 맞게 조정
335
  temperature=0.2
336
  )
337
  collected_content = ""
@@ -339,9 +288,7 @@ async def try_openai_api(openai_messages):
339
  if chunk.choices[0].delta.content is not None:
340
  collected_content += chunk.choices[0].delta.content
341
  yield collected_content
342
- await asyncio.sleep(0) # CPU 양보
343
  except Exception as e:
344
- logger.error(f"OpenAI API Error: {e}")
345
  raise e
346
 
347
 
@@ -415,45 +362,19 @@ def create_template_html(title, items):
415
  """
416
  html_content += card_html
417
  html_content += r"""
418
- </div>
419
  <script>
420
  function copyToInput(card) {
421
  const prompt = card.dataset.prompt;
422
- // Gradio 3.x / 4.x 에서 textarea 선택자 변경 가능성 고려
423
- // 우선 .ant-input-textarea-large textarea 시도, 없으면 일반 textarea 시도
424
- let textarea = document.querySelector('.ant-input-textarea-large textarea');
425
- if (!textarea) {
426
- // 일반적인 Gradio textarea 선택자 (버전에 따라 다를 수 있음)
427
- const textareas = document.querySelectorAll('textarea');
428
- // 여러 textarea 중 올바른 것을 특정하기 어려우므로, 좀 더 구체적인 ID나 클래스 필요
429
- // 여기서는 첫 번째 textarea를 사용하거나, ID 기반으로 찾아야 함.
430
- // 예: textarea = document.querySelector('#input_text_area_id textarea');
431
- // 지금은 antd 컴포넌트 기반으로 유지
432
- if (textareas.length > 0) {
433
- // input_text 컴포넌트에 ID를 부여하고 해당 ID로 찾는 것이 더 안정적
434
- // 예: input_text = antd.InputTextarea(..., elem_id="main_prompt_input")
435
- // textarea = document.querySelector('#main_prompt_input textarea');
436
- // 임시로 첫번째 textarea 사용 (주의: 다른 textarea가 있을 경우 문제 발생 가능)
437
- // textarea = textareas[0];
438
- }
439
- }
440
-
441
  if (textarea) {
442
  textarea.value = prompt;
443
- // Gradio 입력값 업데이트를 위해 'input' 이벤트 트리거
444
- const event = new Event('input', { bubbles: true });
445
- textarea.dispatchEvent(event);
446
-
447
- // 템플릿 Drawer 닫기 (Drawer 닫기 버튼의 클래스나 ID로 정확히 선택)
448
- const closeButton = document.querySelector('.session-drawer .ant-drawer-close, .session-drawer .close-btn'); // antd Drawer의 기본 닫기 버튼 또는 커스텀 버튼
449
- if (closeButton) {
450
- closeButton.click();
451
- }
452
- } else {
453
- console.warn('Prompt textarea not found.');
454
  }
455
  }
456
  </script>
 
457
  """
458
  return gr.HTML(value=html_content)
459
 
@@ -466,117 +387,77 @@ def load_all_templates():
466
  # ------------------------
467
 
468
  def remove_code_block(text):
469
- if not text: return ""
470
- # HTML 주석 제거 (코드 블록 제거 전에 하는 것이 좋을 수 있음)
471
- text = re.sub(r'<!--.*?-->', '', text, flags=re.DOTALL)
472
-
473
- # ```html ... ``` 패턴
474
- pattern_html = r'```html\s*([\s\S]+?)\s*```'
475
- match_html = re.search(pattern_html, text, re.DOTALL)
476
- if match_html:
477
- return match_html.group(1).strip()
478
-
479
- # ``` ... ``` 일반 코드 블록 패턴
480
- pattern_general = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
481
- match_general = re.search(pattern_general, text, re.DOTALL)
482
- if match_general:
483
- return match_general.group(1).strip()
484
 
485
- # 코드 블록 마커가 없는 경우, 원본 텍스트가 순수 HTML 코드일 수 있다고 가정
486
- # 또는 마커가 깨진 경우를 대비해 간단히 마커만 제거 시도
487
- text_no_markers = re.sub(r'^```html\s*', '', text, flags=re.MULTILINE).strip()
488
- text_no_markers = re.sub(r'\s*```$', '', text_no_markers, flags=re.MULTILINE).strip()
489
 
490
- # 만약 마커 제거 후에도 변화가 없다면 원본 텍스트 반환 (이미 순수 코드일 가능성)
491
- if text_no_markers == text.strip():
492
- return text.strip()
493
- return text_no_markers
494
-
495
 
496
  def optimize_code(code: str) -> str:
497
  if not code or len(code.strip()) == 0:
498
  return code
499
 
500
  lines = code.split('\n')
501
- # 코드 길이 제한은 LLM에게 맡기는 것이 더 효과적일 수 있음
502
- # if len(lines) <= 200:
503
- # return code
504
-
505
- # 주석 제거 (HTML, CSS, JS)
506
- # HTML 주석
507
- cleaned_code = re.sub(r'<!--[\s\S]*?-->', '', code, flags=re.MULTILINE)
508
- # CSS 주석
509
- cleaned_code = re.sub(r'/\*[\s\S]*?\*/', '', cleaned_code, flags=re.MULTILINE)
510
- # JS 한 줄 주석 (주의: URL 등에서 //가 사용될 수 있으므로, 정교한 패턴 필요)
511
- # cleaned_code = re.sub(r'(?<!:)//.*?$', '', cleaned_code, flags=re.MULTILINE) # Positive lookbehind for : to avoid http://
512
 
513
- # JS 주석은 LLM이 생성 시 최소화하도록 유도하는 것이 더 안전
 
 
 
 
 
 
 
514
 
515
- # 빈 줄 제거 (연속된 빈 줄은 하나로)
516
  cleaned_lines = []
517
  empty_line_count = 0
518
  for line in cleaned_code.split('\n'):
519
- stripped_line = line.strip()
520
- if stripped_line == '':
521
  empty_line_count += 1
522
- if empty_line_count <= 1: # 연속된 빈 줄 중 첫 번째는 유지 (가독성 위해)
523
  cleaned_lines.append('')
524
  else:
525
  empty_line_count = 0
526
- cleaned_lines.append(line) # 원본 줄 유지 (들여쓰기 보존)
527
 
528
  cleaned_code = '\n'.join(cleaned_lines)
529
-
530
- # console.log 제거 (주의: 프로덕션 코드에서는 필요할 수 있음)
531
- # cleaned_code = re.sub(r'console\.log\(.*?\);?', '', cleaned_code, flags=re.MULTILINE)
532
-
533
- # 연속된 공백을 하나로 (주의: 문자열 내부 공백에 영향 줄 수 있음, HTML 태그 속성 등)
534
- # 이 부분은 매우 신중해야 하며, 보통은 적용하지 않는 것이 안전
535
- # cleaned_code = re.sub(r' {2,}', ' ', cleaned_code)
536
-
537
- return cleaned_code.strip()
538
-
539
 
540
  def send_to_sandbox(code):
541
- if not code:
542
- return '<div style="color:red;">미리보기를 위한 코드가 없습니다.</div>'
543
-
544
- clean_code = remove_code_block(code) # 코드 블록 제거
545
- # clean_code = optimize_code(clean_code) # 최적화는 선택 사항, LLM이 이미 최적화된 코드를 주도록 유도
 
 
546
 
547
- # 이미 완전한 HTML 문서인지 확인
548
- is_full_html = clean_code.strip().lower().startswith('<!doctype html>') or \
549
- clean_code.strip().lower().startswith('<html')
550
-
551
- if not is_full_html:
552
- # 기본적인 HTML 구조로 감싸기
553
- # CSS나 JS만 있는 경우를 고려하여 <style>이나 <script> 태그가 있다면 head나 body에 적절히 배치
554
- # 여기서는 단순화를 위해 body에 모두 넣음
555
  clean_code = f"""<!DOCTYPE html>
556
- <html lang="ko">
557
  <head>
558
- <meta charset="UTF-AF">
559
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
560
  <title>Game Preview</title>
561
- <style>
562
- body {{ margin: 0; overflow: hidden; background-color: #f0f0f0; }}
563
- /* Add any default iframe styling here */
564
- </style>
565
  </head>
566
  <body>
567
  {clean_code}
568
  </body>
569
  </html>"""
570
-
571
- # Data URI 생성
572
- # b64encode는 bytes를 받으므로, 문자열을 utf-8로 인코딩
573
  encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
574
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
575
-
576
- # iframe으로 반환
577
- # sandbox 속성으로 보안 강화 (필요에 따라 조정)
578
- return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;" sandbox="allow-scripts allow-same-origin allow-popups allow-forms"></iframe>'
579
-
580
 
581
  def boost_prompt(prompt: str) -> str:
582
  if not prompt:
@@ -585,98 +466,128 @@ def boost_prompt(prompt: str) -> str:
585
  주어진 프롬프트를 분석하여 더 명확하고 간결한 요구사항으로 변환하되,
586
  원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
587
 
588
- 1. 게임플레이 핵심 메커니즘 명확히 정의 (예: 테트리스 - 블록 낙하, 회전, 줄 제거)
589
- 2. 필수적인 상호작용 요소만 포함 (예: 키보드 화살표 키 조작)
590
- 3. 핵심 UI 요소 간략히 기술 (예: 점수판, 다음 블록 표시)
591
- 4. 코드 간결성 유지를 위한 우선순위 설정 (예: 그래픽 최소화, 사운드 제외)
592
- 5. 기본적인 게임 규칙과 승리/패배 조건 명시 (예: 화면 상단까지 블록 쌓이면 게임 오버)
593
 
594
  다음 중요 지침을 반드시 준수하세요:
595
- - 불필요한 세부 사항이나 부가 기능은 제외 (예: 멀티플레이어, 사용자 정의 테마)
596
- - 생성될 코드가 HTML 단일 파일 기준 200줄을 넘지 않도록 기능을 제한 (매우 중요)
597
  - 명확하고 간결한 언어로 요구사항 작성
598
- - 최소한의 필수 게임 요소만 포함 (MVP - Minimum Viable Product)
599
- - "HTML, CSS, JavaScript를 사용하여 단일 파일로 만들어주세요." 라는 문구를 명시적으로 포함.
600
- - "코드는 200줄 이내로 매우 간결해야 합니다." 라는 문구를 명시적으로 포함.
601
  """
602
  try:
603
- # Claude 우선 시도
604
  try:
605
  response = claude_client.messages.create(
606
- model="claude-3-opus-20240229", # 또는 claude-3-sonnet-20240229
607
- max_tokens=1024, # 증강된 프롬프트는 길지 않으므로 토큰 수 줄임
608
- temperature=0.2, # 일관된 결과 선호
609
  messages=[{
610
  "role": "user",
611
- "content": f"다음 게임 프롬프트를 분석하고 증강하여, 200줄 이내의 단일 HTML 파일 게임 코드를 생성할 수 있도록 매우 간결하게 만들어주세요: {prompt}"
612
  }],
613
  system=boost_system_prompt
614
  )
615
- if hasattr(response, 'content') and len(response.content) > 0 and hasattr(response.content[0], 'text'):
616
- return response.content[0].text.strip()
617
- logger.warning("Claude API 응답 형식 오류 또는 내용 없음 (boost_prompt)")
618
- except Exception as e_claude:
619
- logger.warning(f"Claude API 호출 실패 (boost_prompt): {e_claude}. OpenAI로 재시도합니다.")
620
-
621
- # OpenAI 재시도
622
- completion = openai_client.chat.completions.create(
623
- model="gpt-4o", # 또는 gpt-4-turbo
624
- messages=[
625
- {"role": "system", "content": boost_system_prompt},
626
- {"role": "user", "content": f"다음 게임 프롬프트를 분석하고 증강하여, 200줄 이내의 단일 HTML 파일 게임 코드를 생성할 수 있도록 매우 간결하게 만들어주세요: {prompt}"}
627
- ],
628
- max_tokens=1024,
629
- temperature=0.2
630
- )
631
- if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
632
- return completion.choices[0].message.content.strip()
633
- logger.warning("OpenAI API 응답 형식 오류 또는 내용 없음 (boost_prompt)")
634
-
635
- # 두 API 모두 실패 시 원본 프롬프트 반환
636
- return prompt
637
- except Exception as e_general:
638
- logger.error(f"Boost prompt 중 일반 오류 발생: {e_general}")
639
- return prompt # 예외 발생 시 원본 프롬프트 반환
640
 
641
  def handle_boost(prompt: str):
642
- if not prompt or prompt.strip() == "":
643
- return gr.update(value="증강할 내용이 없습니다. 먼저 게임 설명을 입력해주세요."), gr.update(active_key="empty")
644
  try:
645
  boosted_prompt = boost_prompt(prompt)
646
- return boosted_prompt, gr.update(active_key="empty") # 증강 후에는 미리보기 탭을 비움
647
- except Exception as e:
648
- logger.error(f"Handle_boost error: {str(e)}")
649
- return gr.update(value=f"증강 중 오류 발생: {str(e)}"), gr.update(active_key="empty")
650
-
651
 
652
  def history_render(history: History):
653
- # history가 비어있으면 Drawer를 열지 않거나, 비어있다는 메시지를 Chatbot에 표시
654
- if not history:
655
- # Chatbot에 "히스토리가 없습니다" 메시지 표시하는 방법은 Gradio 버전에 따라 다를 수 있음
656
- # 여기서는 Drawer는 열되, Chatbot은 비어있게 됨
657
- return gr.update(open=True), [] # 빈 히스토리 전달
658
  return gr.update(open=True), history
659
 
660
-
661
- def execute_code(query: str): # query는 코드 문자열을 받음
662
  if not query or query.strip() == '':
663
- return None, gr.update(active_key="empty") # 코드가 없으면 아무것도 안 함
664
  try:
665
- # query는 이미 코드 문자열이므로 remove_code_block은 필요 없을 수 있으나,
666
- # 사용자가 입력창에 ```html ... ``` 형식으로 넣을 수도 있으므로 안전하게 호출
667
  clean_code = remove_code_block(query)
668
-
669
- # send_to_sandbox 함수는 이미 내부적으로 HTML 구조를 완성시켜줌
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
670
  return send_to_sandbox(clean_code), gr.update(active_key="render")
671
  except Exception as e:
672
- logger.error(f"Execute code error: {str(e)}")
673
- error_html = f"<div style='color:red; padding:10px;'>코드 실행 중 오류 발생: {html.escape(str(e))}</div>"
674
- return error_html, gr.update(active_key="render") # 오류도 HTML로 렌더링
675
 
676
 
677
  # ------------------------
678
  # 6) 데모 클래스
679
  # ------------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
680
 
681
  class Demo:
682
  def __init__(self):
@@ -684,177 +595,133 @@ class Demo:
684
 
685
  async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
686
  if not query or query.strip() == '':
687
- # query = random.choice(DEMO_LIST)['description'] # 랜덤 프롬프트 대신 사용자 입력 유도
688
- yield [
689
- gr.update(value="게임 설명을 입력해주세요."), # code_output에 메시지
690
- _history if _history else [],
691
- gr.update(value='<div style="padding: 20px; text-align: center; color: grey;">생성할 게임 설명을 입력하고 "전송" 버튼을 클릭하세요.</div>'), # sandbox
692
- gr.update(active_key="empty"), # state_tab
693
- gr.update(open=False) # code_drawer (열지 않음)
694
- ]
695
- return
696
-
697
 
698
  if _history is None:
699
  _history = []
700
 
701
- # LLM에게 전달할 최종 프롬프트 구성
702
- # 시스템 프롬프트에서 코드 길이 제한 등을 이미 강조하고 있으므로, query에는 게임 요청만 명확히 전달
703
- # query_prefix = """
704
- # 다음 게임을 제작해주세요.
705
- # 중요 요구사항:
706
- # 1. 모든 코드는 하나의 HTML 파일에 통합해주세요.
707
- # 2. 코드는 극도로 간결해야 하며, 200줄을 넘지 않도록 해주세요. (매우 중요)
708
- # 3. 불필요한 주석, 설명, 외부 라이브러리 사용은 피해주세요.
709
- # 4. 핵심 게임 기능만 구현하고, 부가 기능은 생략해주세요.
710
- # ---
711
- # 게임 요청:
712
- # """
713
- # final_query = query_prefix + query
714
- # SystemPrompt에 이미 상세 지침이 있으므로, 사용자 query는 그대로 사용
715
- final_query = query
716
 
717
  messages = history_to_messages(_history, _setting['system'])
718
- system_message = messages[0]['content'] # SystemPrompt
719
 
720
- # Claude API용 메시지 준비 (System role을 messages list 밖으로 빼는 방식)
721
- claude_messages_for_api = [
722
- msg for msg in messages[1:] if msg["content"].strip() != ''
 
723
  ]
724
- claude_messages_for_api.append({'role': Role.USER, 'content': final_query})
725
 
726
- # OpenAI API용 메시지 준비 (System role messages list 안에 포함)
727
- openai_messages_for_api = [{"role": "system", "content": system_message}]
728
- openai_messages_for_api.extend([
729
- msg for msg in messages[1:] if msg["content"].strip() != ''
730
- ])
731
- openai_messages_for_api.append({"role": "user", "content": final_query})
 
732
 
733
- # 초기 UI 업데이트: 로딩 시작
734
- yield [
735
- gr.update(value="코드를 생성하고 있습니다... 🎮"), # code_output
736
- _history,
737
- gr.update(value=None), # sandbox (로딩 중에는 비움)
738
- gr.update(active_key="loading"), # state_tab
739
- gr.update(open=False) # code_drawer (로딩 중에는 닫음)
740
- ]
741
- await asyncio.sleep(0.01) # UI 업데이트를 위한 짧은 지연
742
-
743
- collected_content = None
744
- error_occurred = False
745
- error_message = ""
746
-
747
- # API 호출 시도 (Claude 우선)
748
  try:
749
- logger.info("Claude API 호출 시도...")
750
- async for content_chunk in try_claude_api(system_message, claude_messages_for_api):
751
- collected_content = content_chunk
752
- yield [
753
- gr.update(value=collected_content + "\n\nClaude API로부터 스트리밍 중..."),
754
- _history, None, gr.update(active_key="loading"), gr.update(open=False)
755
- ]
756
- logger.info(f"Claude API 호출 성공. 내용 길이: {len(collected_content if collected_content else '')}")
757
- except Exception as e_claude:
758
- logger.warning(f"Claude API 실패: {e_claude}. OpenAI API로 재시도합니다.")
759
- collected_content = None # Claude 실패 시 내용 초기화
760
- # OpenAI 시도 전, 사용자에게 알림 (선택 사항)
761
- # yield [
762
- # gr.update(value=f"Claude API 호출에 실패했습니다. OpenAI API로 재시도합니다...\n에러: {str(e_claude)[:100]}..."),
763
- # _history, None, gr.update(active_key="loading"), gr.update(open=False)
764
- # ]
765
- # await asyncio.sleep(1) # 메시지 확인 시간
766
-
767
- try:
768
- logger.info("OpenAI API 호출 시도...")
769
- async for content_chunk in try_openai_api(openai_messages_for_api):
770
- collected_content = content_chunk
771
- yield [
772
- gr.update(value=collected_content + "\n\nOpenAI API로부터 스트리밍 중..."),
773
- _history, None, gr.update(active_key="loading"), gr.update(open=False)
774
- ]
775
- logger.info(f"OpenAI API 호출 성공. 내용 길이: {len(collected_content if collected_content else '')}")
776
- except Exception as e_openai:
777
- logger.error(f"OpenAI API도 실패: {e_openai}")
778
- error_occurred = True
779
- error_message = f"Claude와 OpenAI API 모두 호출에 실패했습니다.\nClaude: {str(e_claude)}\nOpenAI: {str(e_openai)}"
780
- collected_content = None
781
-
782
-
783
- if error_occurred or not collected_content:
784
- final_error_message = error_message if error_message else "API로부터 응답을 받지 못했습니다. 네트워크 연결 또는 API 키 설정을 확인해주세요."
785
- yield [
786
- gr.update(value=final_error_message),
787
- _history,
788
- gr.update(value=f'<div style="color:red; padding:10px;">{html.escape(final_error_message)}</div>'),
789
- gr.update(active_key="empty"),
790
- gr.update(open=True) # 오류 메시지 확인을 위해 코드 보기 창 열기
791
- ]
792
- return
793
-
794
- # API 호출 성공 후 처리
795
- clean_code_from_llm = remove_code_block(collected_content) # LLM 응답에서 코드만 추출
796
-
797
- # 코드 길이 경고 (LLM이 생성한 코드 기준)
798
- # 이 부분은 SystemPrompt에서 이미 강력하게 요청하고 있으므로, LLM이 잘 지켰을 것으로 기대
799
- code_lines = clean_code_from_llm.count('\n') + 1
800
- if code_lines > 250: # SystemPrompt는 200줄 요청, 약간의 여유
801
- warning_msg = f"""⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄).**
802
- 요청하신 게임의 복잡도 때문일 수 있습니다. SystemPrompt는 200줄 이내를 요청했습니다.
803
- 코드가 너무 길면 실행이 느리거나, 브라우저에서 오류가 발생할 수 있습니다.
804
- 더 간단한 게임을 요청하시거나, "코드" 버튼으로 직접 실행해보세요.
805
-
806
- --- 코드 시작 (일부만 표시) ---
807
- {html.escape(clean_code_from_llm[:1500])}
808
- ... (코드가 너무 길어 일부만 표시) ...
809
- """
810
- # sandbox_content = send_to_sandbox(clean_code_from_llm) # 긴 코드도 일단 실행 시도
811
- sandbox_content = f'<div style="padding:10px; background-color:#fff3cd; color:#664d03; border:1px solid #ffe69c; border-radius:5px;">{warning_msg.replace("--- 코드 시작 (일부만 표시) ---", "").replace("... (코드가 너무 길어 일부만 표시) ...","").replace("⚠️ **경고: 생성된 코드가 너무 깁니다", "⚠️ 경고: 생성된 코드가 너무 깁니다")} <br><br>그래도 미리보기를 시도합니다...</div>' + send_to_sandbox(clean_code_from_llm)
812
-
813
- yield [
814
- gr.update(value=warning_msg), # code_output에 경고와 코드 표시
815
- _history, # 히스토리는 업데이트하지 않음 (긴 코드는 저장하지 않거나, 사용자 선택)
816
- sandbox_content, # sandbox에는 경고와 함께 미리보기 시도
817
- gr.update(active_key="render"), # state_tab
818
- gr.update(open=True) # code_drawer 열어서 경고 확인
819
- ]
820
- else:
821
- # 정상적인 길이의 코드 처리
822
- # 히스토리 업데이트
823
- # final_query (사용자 입력)과 collected_content (LLM 응답)을 히스토리에 추가
824
- _history.append((final_query, collected_content))
825
-
826
  yield [
827
- gr.update(value=collected_content), # code_output에 전체 LLM 응답 (코드블록 포함)
828
  _history,
829
- send_to_sandbox(clean_code_from_llm), # sandbox에는 순수 코드 미리보기
830
- gr.update(active_key="render"), # state_tab
831
- gr.update(open=False) # code_drawer (성공 시 자동으로 열지 않음, 사용자가 원할 때 열도록)
832
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
 
834
  def clear_history(self):
835
- return [], gr.update(value="히스토리가 초기화되었습니다."), None, gr.update(active_key="empty") # Chatbot, code_output, sandbox, state_tab 순서로 가정
836
 
837
 
838
  ####################################################
839
  # 1) deploy_to_vercel 함수
840
  ####################################################
841
  def deploy_to_vercel(code: str):
842
- logger.debug(f"[VercelDeploy] 시작. 코드 길이: {len(code) if code else 0}")
843
  try:
844
- if not code or len(code.strip()) < 10: # HTML 기본 구조보다 짧으면 배포 의미 없음
845
- logger.info("[VercelDeploy] 배포 불가: 코드가 너무 짧습니다.")
846
- return "배포할 코드가 너무 짧습니다. 최소한의 HTML 구조를 포함해야 합니다."
847
 
848
- # Vercel API 토큰 (환경 변수 또는 직접 설정)
849
- # token = os.getenv("VERCEL_API_TOKEN")
850
- token = "A8IFZmgW2cqA4yUNlLPnci0N" # 제공된 토큰 사용 (보안상 환경변수 사용 권장)
851
  if not token:
852
- logger.error("[VercelDeploy] Vercel API 토큰이 설정되지 않았습니다.")
853
- return "Vercel API 토큰이 설정되지 않았습니다. 서버 환경 설정을 확인해주세요."
854
 
855
- # 프로젝트 이름 랜덤 생성 (충돌 방지)
856
- project_name = "gamecraft-" + ''.join(random.choice(string.ascii_lowercase + string.digits) for _ in range(8))
857
- logger.info(f"[VercelDeploy] 생성된 프로젝트 이름: {project_name}")
858
 
859
  deploy_url = "https://api.vercel.com/v13/deployments"
860
  headers = {
@@ -862,94 +729,99 @@ def deploy_to_vercel(code: str):
862
  "Content-Type": "application/json"
863
  }
864
 
865
- # Vercel 배포를 위한 package.json (Vite 사용 예시, 정적 HTML이므로 간단하게)
866
- # Vite를 사용하지 않고 순수 정적 파일만 배포할 경우, buildCommand 등이 필요 없을 수 있음
867
- # Vercel은 index.html을 자동으로 인식함
868
- package_json_content = {
869
  "name": project_name,
870
  "version": "1.0.0",
871
  "private": True,
872
- # "scripts": { # 순수 HTML만 있다면 빌드 스크립트 불필요
873
- # "build": "echo 'No build needed, deploying static HTML.' && mkdir -p dist && cp index.html dist/"
874
- # }
 
 
 
875
  }
876
 
877
- # 배포할 파일 목록
878
- files_to_deploy = [
879
  {"file": "index.html", "data": code},
880
- # {"file": "package.json", "data": json.dumps(package_json_content, indent=2)} # 순수 HTML만 있다면 package.json 불필요
881
  ]
882
-
883
- # 프로젝트 설정 (순수 HTML의 경우 framework: null 또는 생략 가능)
884
  project_settings = {
885
- # "buildCommand": "npm run build", # 빌드 명령 불필요
886
- # "outputDirectory": "dist", # 빌드 결과 디렉토리 불필요
887
- # "installCommand": "npm install", # 의존성 설치 불필요
888
- "framework": None # 정적 사이트로 명시
889
  }
890
 
891
- # 배포 요청 데이터
892
  deploy_data = {
893
- "name": project_name, # 프로젝트 이름 (Vercel 대시보드에 표시될 이름)
894
- "files": files_to_deploy,
895
- "target": "production", # 배포 환경 (production 또는 staging)
896
  "projectSettings": project_settings
897
  }
898
-
899
- logger.info("[VercelDeploy] Vercel API에 배포 요청 전송 중...")
900
- # 타임아웃 설정 (예: 60초)
901
- try:
902
- deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data, timeout=60)
903
- deploy_response.raise_for_status() # 오류 발생 시 예외 발생
904
- except requests.exceptions.RequestException as req_err:
905
- logger.error(f"[VercelDeploy] Vercel API 요청 실패: {req_err}")
906
- logger.error(f"[VercelDeploy] 응답 내용: {req_err.response.text if req_err.response else 'No response'}")
907
- return f"Vercel API 요청 실패: {req_err}. 응답: {req_err.response.text if req_err.response else 'No response'}"
908
-
909
-
910
- response_data = deploy_response.json()
911
- logger.debug(f"[VercelDeploy] Vercel API 응답 상태 코드: {deploy_response.status_code}")
912
- logger.debug(f"[VercelDeploy] Vercel API 응답 내용 (일부): {str(response_data)[:200]}...")
913
-
914
- # 응답에서 배포 URL 확인 (Vercel 응답 구조에 따라 다를 수 있음)
915
- # 일반적으로 response_data['url'] 또는 response_data['alias'][0] 등에 URL이 포함됨
916
- # Vercel API v13에서는 생성된 URL이 응답에 바로 포함됨
917
- deployment_final_url = response_data.get("url")
918
- if not deployment_final_url:
919
- # 때로는 project_name 기반으로 URL을 구성해야 할 수 있음
920
- # Vercel은 보통 <project_name>.vercel.app 또는 <project_name>-<unique_hash>.vercel.app 형식
921
- # 응답에 alias가 있다면 그것을 사용
922
- if response_data.get("alias") and len(response_data["alias"]) > 0:
923
- deployment_final_url = response_data["alias"][0]
924
- else: # 최후의 수단으로 project_name 사용 (정확하지 않을 수 있음)
925
- deployment_final_url = f"{project_name}.vercel.app"
926
-
927
- # URL에 https 스키마 추가 (Vercel은 항상 https)
928
- if not deployment_final_url.startswith("https://"):
929
- deployment_final_url = "https://" + deployment_final_url
930
 
 
 
 
931
 
932
- logger.info(f"[VercelDeploy] 배포 성공! 앱 URL: {deployment_final_url}")
933
-
934
- # 배포 DNS 전파 등을 위해 약간의 시간 대기 (선택 사항)
935
- # time.sleep(5) # 실제 서비스에서는 비동기 폴링 등으로 상태 확인 권장
 
 
 
936
 
937
  return (
938
- f"✅ **배포 완료!**\n"
939
- f"앱이 다음 주소에서 실행 중입니다:\n"
940
- f"[{deployment_final_url}]({deployment_final_url})" # 마크다운 링크 형식
941
  )
942
 
 
 
943
  except Exception as e:
944
- logger.error(f"[VercelDeploy] 배포 중 예기치 않은 오류 발생: {e}", exc_info=True)
945
- return f"배포 오류 발생: {str(e)}"
 
946
 
947
 
948
  # ------------------------
949
- # (3) handle_deploy_legacy - 이 함수는 get_deployment_update로 대체되었으므로 주석 처리 또는 삭제 가능
950
  # ------------------------
951
- # def handle_deploy_legacy(code):
952
- # # ... (이전 코드) ...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
953
 
954
 
955
  # ------------------------
@@ -957,128 +829,121 @@ def deploy_to_vercel(code: str):
957
  # ------------------------
958
  demo_instance = Demo()
959
  theme = gr.themes.Soft(
960
- primary_hue=gr.themes.colors.blue, # 직접 색상 객체 사용
961
- secondary_hue=gr.themes.colors.purple,
962
- neutral_hue=gr.themes.colors.slate,
963
  spacing_size=gr.themes.sizes.spacing_md,
964
  radius_size=gr.themes.sizes.radius_md,
965
  text_size=gr.themes.sizes.text_md,
966
  )
967
 
968
- with gr.Blocks(css="app.css", theme=theme) as demo: # css 파일명 직접 전달
969
- # header_html = gr.HTML(""" ... """) # 기존 HTML 사용 또는 Python에서 동적 생성
970
-
971
- gr.HTML("""
972
  <div class="app-header">
973
  <h1>🎮 Vibe Game Craft</h1>
974
- <p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 실시간 미리보기와 원클릭 배포 기능도 지원됩니다!</p>
975
  </div>
 
 
 
 
 
 
 
 
 
 
976
  """)
977
-
978
  history = gr.State([])
979
  setting = gr.State({"system": SystemPrompt})
980
- # deploy_status = gr.State(...) # 현재 사용되지 않으므로 제거 또는 필요시 재활성화
981
 
982
- with ms.Application() as app: # ModelScope Studio 컴포넌트 사용 시
983
- with antd.ConfigProvider(): # Ant Design 컴포넌트 사용 시
984
 
985
- with antd.Drawer(open=False, title="💻 생성된 코드 보기", placement="left", width="50%") as code_drawer:
986
- code_output = legacy.Markdown(elem_classes="code-markdown-output", संतति_rendering=False) # संतति_rendering 옵션 확인
987
 
988
- with antd.Drawer(open=False, title="📜 대화 히스토리", placement="left", width="60%") as history_drawer:
989
- history_output = legacy.Chatbot(
990
- show_label=False,
991
- flushing=False, # 실시간 스트리밍 시 flushing 동작 방식 확인
992
- height=700, # 높이 조정
993
- elem_classes="history_chatbot",
994
- show_copy_button=True
995
- )
996
 
997
  with antd.Drawer(
998
  open=False,
999
- title="🎲 게임 템플릿 선택",
1000
  placement="right",
1001
- width="60%", # 너비 조정
1002
  elem_classes="session-drawer"
1003
  ) as session_drawer:
1004
  with antd.Flex(vertical=True, gap="middle"):
1005
- # gr.Markdown("### 사용 가능한 게임 템플릿") # 제목은 Drawer title로 충분
1006
- session_history_html_output = gr.HTML(elem_classes="session-history-grid") # CSS 클래스명 변경 및 그리드 레이아웃용
1007
- close_btn_template_drawer = antd.Button("닫기", type="default", elem_classes="close-btn-template-drawer")
1008
 
 
1009
 
1010
- with antd.Row(gutter=[16, 16], align="top", elem_classes="main-layout-row"): # gutter 값 조정
1011
-
1012
- # 왼쪽 Col: 게임 미리보기
1013
- with antd.Col(xs=24, sm=24, md=14, lg=15, xl=16, elem_classes="preview-column"): # 반응형 컬럼 크기 조정
1014
- with ms.Div(elem_classes="preview-panel panel"): # 클래스명 변경
1015
  gr.HTML(r"""
1016
  <div class="render_header">
1017
- <span class="header_btn red"></span><span class="header_btn yellow"></span><span class="header_btn green"></span>
1018
- <span class="header_title">Game Preview</span>
1019
  </div>
1020
  """)
1021
- with antd.Tabs(active_key="empty", render_tab_bar="() => null", elem_classes="preview-tabs-content") as state_tab:
1022
  with antd.Tabs.Item(key="empty"):
1023
- empty = antd.Empty(description="게임 설명을 입력하고 '전송' 버튼을 누르면 여기에 미리보기가 나타납니다.", elem_classes="right_content empty-state")
1024
  with antd.Tabs.Item(key="loading"):
1025
- loading = antd.Spin(True, tip="게임 코드 생성 중... 잠시만 기다려주세요...", size="large", elem_classes="right_content loading-state")
1026
  with antd.Tabs.Item(key="render"):
1027
- sandbox = gr.HTML(elem_classes="html_content sandbox-iframe-container")
1028
-
1029
-
1030
- # 오른쪽 Col: 입력 및 컨트롤
1031
- with antd.Col(xs=24, sm=24, md=10, lg=9, xl=8, elem_classes="control-column"): # 반응형 컬럼 크기 조정
1032
- with antd.Flex(vertical=True, gap="middle", elem_classes="control-panel-flex"):
1033
- with antd.Flex(gap="small", elem_classes="setting-buttons-flex", justify="space-between"):
1034
- codeBtn = antd.Button("코드", type="default", icon="💻", elem_classes="control-button code-btn") # 아이콘 추가 (antd 아이콘 문자열 또는 컴포넌트)
1035
- historyBtn = antd.Button("히스토리", type="default", icon="📜", elem_classes="control-button history-btn")
1036
- template_btn = antd.Button("템플릿", type="default", icon="🎲", elem_classes="control-button template-btn")
1037
-
1038
- # 입력창을 위로 배치
1039
- with antd.Flex(vertical=True, gap="small", elem_classes="input-area-flex"):
1040
- input_text = antd.InputTextarea(
1041
- size="large",
1042
- allow_clear=True,
1043
- placeholder="예시: 벽돌깨기 게임을 만들어주세요. 공을 튕겨 벽돌을 맞추고, 패들은 마우스로 조작합니다.", # 플레이스홀더 개선
1044
- max_length=500, # 사용자 입력 길이 제한 (너무 길면 LLM 처리 어려움)
1045
- auto_size={"minRows": 5, "maxRows": 10}, # 자동 높이 조절
1046
- elem_id="main_prompt_input" # JS에서 선택하기 위한 ID
1047
- )
1048
- gr.HTML('<div class="help-text">💡 만들고 싶은 게임을 설명해주세요. 간결하고 명확할수록 좋아요!</div>')
1049
-
1050
- with antd.Flex(gap="small", justify="space-between", wrap="wrap", elem_classes="action-buttons-flex"): # wrap 추가
1051
- btn = antd.Button("🚀 전송", type="primary", size="large", elem_classes="action-button send-btn")
1052
- boost_btn = antd.Button("✨ 증강", type="default", size="large", elem_classes="action-button boost-btn")
1053
- execute_btn = antd.Button("🎮 코드 실행", type="default", size="large", elem_classes="action-button execute-btn")
1054
- deploy_btn = antd.Button("☁️ 배포", type="default", size="large", elem_classes="action-button deploy-btn")
1055
- clear_btn = antd.Button("🧹 클리어", type="default", size="large", elem_classes="action-button clear-btn")
1056
 
1057
- # 배포 결과 표시 컨테이너
1058
- deploy_result_container = gr.HTML(
1059
- value='<div class="no-deploy">아직 배포된 게임이 없습니다. "배포" 버튼을 클릭하여 게임을 공유하세요!</div>', # 초기 메시지 개선
1060
- visible=True, # 항상 보이도록 설정 (CSS로 내용 없을 때 스타일링)
1061
- elem_id="deploy_result_container",
1062
- elem_classes="deploy-output-container" # CSS 스타일링용 클래스
 
 
 
 
 
 
 
 
 
1063
  )
 
 
1064
 
1065
 
1066
- # Event Handlers
1067
  codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
1068
- # code_drawer.close(...) # antd Drawer는 자체 닫기 버튼 있음
1069
 
 
1070
  historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
1071
- # history_drawer.close(...)
1072
 
 
1073
  template_btn.click(
1074
  fn=lambda: (gr.update(open=True), load_all_templates()),
1075
- outputs=[session_drawer, session_history_html_output], # session_history -> session_history_html_output
1076
- queue=False # 빠른 UI 업데이트
1077
  )
1078
- close_btn_template_drawer.click(lambda: gr.update(open=False), outputs=[session_drawer])
1079
-
1080
 
1081
- # 전송 버튼 (코드 생성)
1082
  btn.click(
1083
  demo_instance.generation_code,
1084
  inputs=[input_text, setting, history],
@@ -1086,54 +951,38 @@ with gr.Blocks(css="app.css", theme=theme) as demo: # css 파일명 직접 전
1086
  )
1087
 
1088
  # 클리어 버튼
1089
- clear_btn.click(
1090
- demo_instance.clear_history,
1091
- inputs=[],
1092
- outputs=[history, code_output, sandbox, state_tab] # 초기화할 출력들 명시
1093
- ).then(
1094
- lambda: ( # 추가로 입력창과 배포 결과도 초기화
1095
- gr.update(value=""), # input_text
1096
- gr.update(value='<div class="no-deploy">아직 배포된 게임이 없습니다.</div>') # deploy_result_container
1097
- ),
1098
- outputs=[input_text, deploy_result_container]
1099
- )
1100
-
1101
 
1102
  # 증강 버튼
1103
  boost_btn.click(
1104
  fn=handle_boost,
1105
  inputs=[input_text],
1106
- outputs=[input_text, state_tab] # state_tab 업데이트는 증강 결과에 따라 필요 없을 수 있음
1107
  )
1108
 
1109
- # 코드 실행 버튼 (입력창의 코드를 직접 실행)
1110
  execute_btn.click(
1111
  fn=execute_code,
1112
- inputs=[input_text], # 입력창의 내용을 코드로 간주하고 실행
1113
  outputs=[sandbox, state_tab]
1114
  )
1115
 
1116
- # 배포 버튼 (생성된 코드(code_output)를 배포)
1117
  deploy_btn.click(
1118
- fn=get_deployment_update, # 제너레이터 함수이므로 outputs를 적절히 처리해야 함
1119
- inputs=[code_output], # code_output (Markdown 컴포넌트의 값)을 입력으로 사용
1120
- outputs=[deploy_result_container]
1121
- )
1122
-
1123
-
 
 
1124
  # ------------------------
1125
  # 9) 실행
1126
  # ------------------------
1127
  if __name__ == "__main__":
1128
  try:
1129
- # demo_instance = Demo() # 이미 위에서 생성됨
1130
- demo.queue(default_concurrency_limit=10, max_batch_size=4).launch(
1131
- ssr_mode=False,
1132
- # share=True, # 필요시 외부 공유 활성화
1133
- # server_name="0.0.0.0" # 모든 인터페이스에서 접속 허용
1134
- # auth=("user", "password") # 필요시 인증 추가
1135
- )
1136
  except Exception as e:
1137
- print(f"Gradio App 실행 중 오류 발생: {e}")
1138
- # logger 사용 가능하면 logger.critical(f"Gradio App 실행 중 오류 발생: {e}", exc_info=True)
1139
- raise
 
34
 
35
  import re
36
 
37
+ deploying_flag = False # 전역 플래그
 
 
 
 
 
 
 
38
 
39
+
40
+ def get_deployment_update(code_md: str):
41
+ clean = remove_code_block(code_md)
42
+ result = deploy_to_vercel(clean)
43
+
44
+ m = re.search(r"https?://[\w\.-]+\.vercel\.app", result)
45
+ if m: # ── 성공 ──
46
+ url = m.group(0)
47
+ md = (
48
+ "✅ **배포 완료!**\n\n"
49
+ f"➡️ [배포된 열기]({url})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  )
51
+ else: # ── 실패 ──
52
+ md = (
53
+ "❌ **배포 실패**\n\n"
54
+ f"```\n{result}\n```"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  )
56
 
57
+ # Markdown 컴포넌트이므로 gr.update 로 값/표시 전환
58
+ return gr.update(value=md, visible=True)
59
+
60
 
61
  # ------------------------
62
  # 1) DEMO_LIST 및 SystemPrompt
 
249
 
250
  start_time = time.time()
251
  with claude_client.messages.stream(
252
+ model="claude-3-7-sonnet-20250219",
253
+ max_tokens=19800,
254
  system=system_message_with_limit,
255
  messages=claude_messages,
256
  temperature=0.3,
 
263
  if chunk.type == "content_block_delta":
264
  collected_content += chunk.delta.text
265
  yield collected_content
266
+ await asyncio.sleep(0)
267
+ start_time = current_time
 
 
 
 
 
 
268
  except Exception as e:
 
269
  raise e
270
 
271
  async def try_openai_api(openai_messages):
 
277
  openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석은 최소화하고, 핵심 기능만 구현하세요."
278
 
279
  stream = openai_client.chat.completions.create(
280
+ model="o3",
281
  messages=openai_messages,
282
  stream=True,
283
+ max_tokens=19800,
284
  temperature=0.2
285
  )
286
  collected_content = ""
 
288
  if chunk.choices[0].delta.content is not None:
289
  collected_content += chunk.choices[0].delta.content
290
  yield collected_content
 
291
  except Exception as e:
 
292
  raise e
293
 
294
 
 
362
  """
363
  html_content += card_html
364
  html_content += r"""
 
365
  <script>
366
  function copyToInput(card) {
367
  const prompt = card.dataset.prompt;
368
+ const textarea = document.querySelector('.ant-input-textarea-large textarea');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
  if (textarea) {
370
  textarea.value = prompt;
371
+ textarea.dispatchEvent(new Event('input', { bubbles: true }));
372
+ // 템플릿 Drawer 닫기
373
+ document.querySelector('.session-drawer .close-btn').click();
 
 
 
 
 
 
 
 
374
  }
375
  }
376
  </script>
377
+ </div>
378
  """
379
  return gr.HTML(value=html_content)
380
 
 
387
  # ------------------------
388
 
389
  def remove_code_block(text):
390
+ pattern = r'```html\s*([\s\S]+?)\s*```'
391
+ match = re.search(pattern, text, re.DOTALL)
392
+ if match:
393
+ return match.group(1).strip()
 
 
 
 
 
 
 
 
 
 
 
394
 
395
+ pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
396
+ match = re.search(pattern, text, re.DOTALL)
397
+ if match:
398
+ return match.group(1).strip()
399
 
400
+ text = re.sub(r'```html\s*', '', text)
401
+ text = re.sub(r'\s*```', '', text)
402
+ return text.strip()
 
 
403
 
404
  def optimize_code(code: str) -> str:
405
  if not code or len(code.strip()) == 0:
406
  return code
407
 
408
  lines = code.split('\n')
409
+ if len(lines) <= 200:
410
+ return code
 
 
 
 
 
 
 
 
 
411
 
412
+ comment_patterns = [
413
+ r'/\*[\s\S]*?\*/',
414
+ r'//.*?$',
415
+ r'<!--[\s\S]*?-->'
416
+ ]
417
+ cleaned_code = code
418
+ for pattern in comment_patterns:
419
+ cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE)
420
 
 
421
  cleaned_lines = []
422
  empty_line_count = 0
423
  for line in cleaned_code.split('\n'):
424
+ if line.strip() == '':
 
425
  empty_line_count += 1
426
+ if empty_line_count <= 1:
427
  cleaned_lines.append('')
428
  else:
429
  empty_line_count = 0
430
+ cleaned_lines.append(line)
431
 
432
  cleaned_code = '\n'.join(cleaned_lines)
433
+ cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE)
434
+ cleaned_code = re.sub(r' {2,}', ' ', cleaned_code)
435
+ return cleaned_code
 
 
 
 
 
 
 
436
 
437
  def send_to_sandbox(code):
438
+ clean_code = remove_code_block(code)
439
+ clean_code = optimize_code(clean_code)
440
+
441
+ if clean_code.startswith('```html'):
442
+ clean_code = clean_code[7:].strip()
443
+ if clean_code.endswith('```'):
444
+ clean_code = clean_code[:-3].strip()
445
 
446
+ if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
 
 
 
 
 
 
 
447
  clean_code = f"""<!DOCTYPE html>
448
+ <html>
449
  <head>
450
+ <meta charset="UTF-8">
451
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
452
  <title>Game Preview</title>
 
 
 
 
453
  </head>
454
  <body>
455
  {clean_code}
456
  </body>
457
  </html>"""
 
 
 
458
  encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
459
  data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
460
+ return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>'
 
 
 
 
461
 
462
  def boost_prompt(prompt: str) -> str:
463
  if not prompt:
 
466
  주어진 프롬프트를 분석하여 더 명확하고 간결한 요구사항으로 변환하되,
467
  원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
468
 
469
+ 1. 게임플레이 핵심 메커니즘 명확히 정의
470
+ 2. 필수적인 상호작용 요소만 포함
471
+ 3. 핵심 UI 요소 간략히 기술
472
+ 4. 코드 간결성 유지를 위한 우선순위 설정
473
+ 5. 기본적인 게임 규칙과 승리/패배 조건 명시
474
 
475
  다음 중요 지침을 반드시 준수하세요:
476
+ - 불필요한 세부 사항이나 부가 기능은 제외
477
+ - 생성될 코드가 600줄을 넘지 않도록 기능을 제한
478
  - 명확하고 간결한 언어로 요구사항 작성
479
+ - 최소한의 필수 게임 요소만 포함
 
 
480
  """
481
  try:
 
482
  try:
483
  response = claude_client.messages.create(
484
+ model="claude-3-7-sonnet-20250219",
485
+ max_tokens=10000,
486
+ temperature=0.3,
487
  messages=[{
488
  "role": "user",
489
+ "content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}"
490
  }],
491
  system=boost_system_prompt
492
  )
493
+ if hasattr(response, 'content') and len(response.content) > 0:
494
+ return response.content[0].text
495
+ raise Exception("Claude API 응답 형식 오류")
496
+ except Exception:
497
+ completion = openai_client.chat.completions.create(
498
+ model="gpt-4",
499
+ messages=[
500
+ {"role": "system", "content": boost_system_prompt},
501
+ {"role": "user", "content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}"}
502
+ ],
503
+ max_tokens=10000,
504
+ temperature=0.3
505
+ )
506
+ if completion.choices and len(completion.choices) > 0:
507
+ return completion.choices[0].message.content
508
+ raise Exception("OpenAI API 응답 형식 오류")
509
+ except Exception:
510
+ return prompt
 
 
 
 
 
 
 
511
 
512
  def handle_boost(prompt: str):
 
 
513
  try:
514
  boosted_prompt = boost_prompt(prompt)
515
+ return boosted_prompt, gr.update(active_key="empty")
516
+ except Exception:
517
+ return prompt, gr.update(active_key="empty")
 
 
518
 
519
  def history_render(history: History):
 
 
 
 
 
520
  return gr.update(open=True), history
521
 
522
+ def execute_code(query: str):
 
523
  if not query or query.strip() == '':
524
+ return None, gr.update(active_key="empty")
525
  try:
 
 
526
  clean_code = remove_code_block(query)
527
+ if clean_code.startswith('```html'):
528
+ clean_code = clean_code[7:].strip()
529
+ if clean_code.endswith('```'):
530
+ clean_code = clean_code[:-3].strip()
531
+ if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
532
+ if not ('<body' in clean_code and '</body>' in clean_code):
533
+ clean_code = f"""<!DOCTYPE html>
534
+ <html>
535
+ <head>
536
+ <meta charset="UTF-8">
537
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
538
+ <title>Game Preview</title>
539
+ </head>
540
+ <body>
541
+ {clean_code}
542
+ </body>
543
+ </html>"""
544
  return send_to_sandbox(clean_code), gr.update(active_key="render")
545
  except Exception as e:
546
+ print(f"Execute code error: {str(e)}")
547
+ return None, gr.update(active_key="empty")
 
548
 
549
 
550
  # ------------------------
551
  # 6) 데모 클래스
552
  # ------------------------
553
+ # ── deploy 버튼용 함수 새로 작성 ─────────────────────────────
554
+ def deploy_and_show(code_md: str):
555
+ global deploying_flag
556
+
557
+ # ① 중복 클릭 차단 ---------------------------------
558
+ if deploying_flag:
559
+ return (gr.update(value="⏳ 이미 배포 중입니다...", visible=True),
560
+ None,
561
+ gr.update(active_key="empty"))
562
+ deploying_flag = True
563
+
564
+ # ② 코드 정리 & 배포 --------------------------------
565
+ clean = remove_code_block(code_md)
566
+ result = deploy_to_vercel(clean)
567
+
568
+ # ③ 결과 해석 ---------------------------------------
569
+
570
+ url_match = re.search(r"https?://[\w\.-]+\.vercel\.app", result)
571
+ if url_match:
572
+ url = url_match.group(0)
573
+
574
+ md_out = f"✅ **배포 완료!**\n\n➡️ [열기]({url})"
575
+ iframe = (f"<iframe src='{url}' width='100%' height='920px' "
576
+ "style='border:none;'></iframe>")
577
+
578
+ deploying_flag = False
579
+ return (gr.update(value=md_out, visible=True),
580
+ iframe,
581
+ gr.update(active_key="render"))
582
+
583
+ # ④ 실패 처리 ---------------------------------------
584
+ md_err = f"❌ **배포 실패**\n\n```\n{result}\n```"
585
+ deploying_flag = False
586
+ return (gr.update(value=md_err, visible=True),
587
+ None,
588
+ gr.update(active_key="empty"))
589
+
590
+
591
 
592
  class Demo:
593
  def __init__(self):
 
595
 
596
  async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
597
  if not query or query.strip() == '':
598
+ query = random.choice(DEMO_LIST)['description']
 
 
 
 
 
 
 
 
 
599
 
600
  if _history is None:
601
  _history = []
602
 
603
+ query = f"""
604
+ 다음 게임을 제작해주세요.
605
+ 중요 요구사항:
606
+ 1. 코드는 가능한 한 간결하게 작성할 것
607
+ 2. 불필요한 주석이나 설명은 제외할 것
608
+ 3. 코드는 600줄을 넘지 않을
609
+ 4. 모든 코드는 하나의 HTML 파일에 통합할
610
+ 5. 핵심 기능만 구현하고 부가 기능은 생략할
611
+ 게임 요청: {query}
612
+ """
 
 
 
 
 
613
 
614
  messages = history_to_messages(_history, _setting['system'])
615
+ system_message = messages[0]['content']
616
 
617
+ claude_messages = [
618
+ {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
619
+ for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
620
+ if msg["content"].strip() != ''
621
  ]
 
622
 
623
+ openai_messages = [{"role": "system", "content": system_message}]
624
+ for msg in messages[1:]:
625
+ openai_messages.append({
626
+ "role": msg["role"],
627
+ "content": msg["content"]
628
+ })
629
+ openai_messages.append({"role": "user", "content": query})
630
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
631
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
632
  yield [
633
+ "Generating code...",
634
  _history,
635
+ None,
636
+ gr.update(active_key="loading"),
637
+ gr.update(open=True)
638
  ]
639
+ await asyncio.sleep(0)
640
+ collected_content = None
641
+ try:
642
+ async for content in try_claude_api(system_message, claude_messages):
643
+ yield [
644
+ content,
645
+ _history,
646
+ None,
647
+ gr.update(active_key="loading"),
648
+ gr.update(open=True)
649
+ ]
650
+ await asyncio.sleep(0)
651
+ collected_content = content
652
+ except Exception:
653
+ async for content in try_openai_api(openai_messages):
654
+ yield [
655
+ content,
656
+ _history,
657
+ None,
658
+ gr.update(active_key="loading"),
659
+ gr.update(open=True)
660
+ ]
661
+ await asyncio.sleep(0)
662
+ collected_content = content
663
+
664
+ if collected_content:
665
+ clean_code = remove_code_block(collected_content)
666
+ code_lines = clean_code.count('\n') + 1
667
+ if code_lines > 700:
668
+ warning_msg = f"""
669
+ ⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄)**
670
+ 이로 인해 실행 시 오류가 발생할 수 있습니다. 다음과 같이 시도해 보세요:
671
+ 1. 더 간단한 게임을 요청하세요
672
+ 2. 특정 기능만 명시하여 요청하세요 (예: "간단한 Snake 게임, 점수 시스템 없이")
673
+ 3. "코드" 버튼을 사용하여 직접 실행해 보세요
674
+ ```html
675
+ {clean_code[:2000]}
676
+ ... (코드가 너무 깁니다) ... """
677
+ collected_content = warning_msg
678
+ yield [
679
+ collected_content,
680
+ _history,
681
+ None,
682
+ gr.update(active_key="empty"),
683
+ gr.update(open=True)
684
+ ]
685
+ else:
686
+ _history = messages_to_history([
687
+ {'role': Role.SYSTEM, 'content': system_message}
688
+ ] + claude_messages + [{
689
+ 'role': Role.ASSISTANT,
690
+ 'content': collected_content
691
+ }])
692
+ yield [
693
+ collected_content,
694
+ _history,
695
+ send_to_sandbox(clean_code),
696
+ gr.update(active_key="render"),
697
+ gr.update(open=True)
698
+ ]
699
+ else:
700
+ raise ValueError("No content was generated from either API")
701
+ except Exception as e:
702
+ raise ValueError(f'Error calling APIs: {str(e)}')
703
 
704
  def clear_history(self):
705
+ return []
706
 
707
 
708
  ####################################################
709
  # 1) deploy_to_vercel 함수
710
  ####################################################
711
  def deploy_to_vercel(code: str):
712
+ print(f"[DEBUG] deploy_to_vercel() 시작. code 길이: {len(code) if code else 0}")
713
  try:
714
+ if not code or len(code.strip()) < 10:
715
+ print("[DEBUG] 배포 불가: code가 짧음")
716
+ return "No code to deploy."
717
 
718
+ token = "A8IFZmgW2cqA4yUNlLPnci0N"
 
 
719
  if not token:
720
+ print("[DEBUG] Vercel 토큰이 없음.")
721
+ return "Vercel token is not set."
722
 
723
+ project_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6))
724
+ print(f"[DEBUG] 생성된 project_name: {project_name}")
 
725
 
726
  deploy_url = "https://api.vercel.com/v13/deployments"
727
  headers = {
 
729
  "Content-Type": "application/json"
730
  }
731
 
732
+ package_json = {
 
 
 
733
  "name": project_name,
734
  "version": "1.0.0",
735
  "private": True,
736
+ "dependencies": {"vite": "^5.0.0"},
737
+ "scripts": {
738
+ "dev": "vite",
739
+ "build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/",
740
+ "preview": "vite preview"
741
+ }
742
  }
743
 
744
+ files = [
 
745
  {"file": "index.html", "data": code},
746
+ {"file": "package.json", "data": json.dumps(package_json, indent=2)}
747
  ]
 
 
748
  project_settings = {
749
+ "buildCommand": "npm run build",
750
+ "outputDirectory": "dist",
751
+ "installCommand": "npm install",
752
+ "framework": None
753
  }
754
 
 
755
  deploy_data = {
756
+ "name": project_name,
757
+ "files": files,
758
+ "target": "production",
759
  "projectSettings": project_settings
760
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
761
 
762
+ print("[DEBUG] Vercel API 요청 전송중...")
763
+ deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data)
764
+ print("[DEBUG] 응답 status_code:", deploy_response.status_code)
765
 
766
+ if deploy_response.status_code != 200:
767
+ print("[DEBUG] 배포 실패:", deploy_response.text)
768
+ return f"Deployment failed: {deploy_response.text}"
769
+
770
+ deployment_url = f"https://{project_name}.vercel.app"
771
+ print(f"[DEBUG] 배포 성공 -> URL: {deployment_url}")
772
+ time.sleep(5)
773
 
774
  return (
775
+ "✅ **Deployment complete!** \n"
776
+ "Your app is live at: \n"
777
+ f"[**{deployment_url}**]({deployment_url})"
778
  )
779
 
780
+
781
+
782
  except Exception as e:
783
+ print("[ERROR] deploy_to_vercel() 예외:", e)
784
+ return f"Error during deployment: {str(e)}"
785
+
786
 
787
 
788
  # ------------------------
789
+ # (3) handle_deploy_legacy
790
  # ------------------------
791
+ def handle_deploy_legacy(code):
792
+ logger.debug(f"[handle_deploy_legacy] code 길이: {len(code) if code else 0}")
793
+ if not code or len(code.strip()) < 10:
794
+ logger.info("[handle_deploy_legacy] 코드가 짧음.")
795
+ return "<div style='color:red;'>배포할 코드가 없습니다.</div>"
796
+
797
+ # 1) 코드 블록 제거
798
+ clean_code = remove_code_block(code)
799
+
800
+ # 2) Vercel에 배포
801
+ result = deploy_to_vercel(clean_code)
802
+ logger.debug(f"[handle_deploy_legacy] deploy_to_vercel 결과: {result}")
803
+
804
+ # 3) 배포 URL 추출
805
+ import re
806
+
807
+ match = re.search(r'https?://[\w.-]+\.vercel\.app', result)
808
+ if match:
809
+ deployment_url = match.group(0)
810
+ # 4) iframe으로 직접 렌더링
811
+ iframe_html = (
812
+ f'<iframe src="{deployment_url}" '
813
+ 'width="100%" height="600px" style="border:none;" '
814
+ 'sandbox="allow-scripts allow-same-origin allow-popups"></iframe>'
815
+ )
816
+ logger.debug("[handle_deploy_legacy] iframe_html 반환")
817
+ return iframe_html
818
+
819
+ # 5) URL 못 찾으면 오류 메시지
820
+ logger.warning("[handle_deploy_legacy] 배포 URL을 찾을 수 없음")
821
+ safe_result = html.escape(result)
822
+ return f"<div style='color:red;'>배포 URL을 찾을 수 없습니다.<br>결과: {safe_result}</div>"
823
+
824
+
825
 
826
 
827
  # ------------------------
 
829
  # ------------------------
830
  demo_instance = Demo()
831
  theme = gr.themes.Soft(
832
+ primary_hue="blue",
833
+ secondary_hue="purple",
834
+ neutral_hue="slate",
835
  spacing_size=gr.themes.sizes.spacing_md,
836
  radius_size=gr.themes.sizes.radius_md,
837
  text_size=gr.themes.sizes.text_md,
838
  )
839
 
840
+ with gr.Blocks(css_paths=["app.css"], theme=theme) as demo:
841
+ header_html = gr.HTML("""
 
 
842
  <div class="app-header">
843
  <h1>🎮 Vibe Game Craft</h1>
844
+ <p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 실시간 미리보기와 배포 기능도 지원됩니다.</p>
845
  </div>
846
+ <!-- 배포 결과 박스 - 헤더 바로 아래 위치 -->
847
+ <div id="deploy-banner" style="display:none;" class="deploy-banner">
848
+ <!-- (생략) ... 배너 스타일/스크립트 ... -->
849
+ </div>
850
+ <style>
851
+ /* (생략) ... CSS ... */
852
+ </style>
853
+ <script>
854
+ /* (생략) ... JS copyBannerUrl / showDeployBanner ... */
855
+ </script>
856
  """)
857
+
858
  history = gr.State([])
859
  setting = gr.State({"system": SystemPrompt})
860
+ deploy_status = gr.State({"is_deployed": False,"status": "","url": "","message": ""})
861
 
862
+ with ms.Application() as app:
863
+ with antd.ConfigProvider():
864
 
865
+ with antd.Drawer(open=False, title="코드 보기", placement="left", width="750px") as code_drawer:
866
+ code_output = legacy.Markdown()
867
 
868
+ with antd.Drawer(open=False, title="히스토리", placement="left", width="900px") as history_drawer:
869
+ history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot")
 
 
 
 
 
 
870
 
871
  with antd.Drawer(
872
  open=False,
873
+ title="게임 템플릿",
874
  placement="right",
875
+ width="900px",
876
  elem_classes="session-drawer"
877
  ) as session_drawer:
878
  with antd.Flex(vertical=True, gap="middle"):
879
+ gr.Markdown("### 사용 가능한 게임 템플릿")
880
+ session_history = gr.HTML(elem_classes="session-history")
881
+ close_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
882
 
883
+ with antd.Row(gutter=[32, 12], align="top", elem_classes="equal-height-container") as layout:
884
 
885
+ # 왼쪽 Col
886
+ with antd.Col(span=24, md=16, elem_classes="equal-height-col"):
887
+ with ms.Div(elem_classes="right_panel panel"):
 
 
888
  gr.HTML(r"""
889
  <div class="render_header">
890
+ <span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span>
 
891
  </div>
892
  """)
893
+ with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
894
  with antd.Tabs.Item(key="empty"):
895
+ empty = antd.Empty(description="게임을 만들려면 설명을 입력하세요", elem_classes="right_content")
896
  with antd.Tabs.Item(key="loading"):
897
+ loading = antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content")
898
  with antd.Tabs.Item(key="render"):
899
+ sandbox = gr.HTML(elem_classes="html_content")
900
+
901
+ # 오른쪽 Col
902
+ with antd.Col(span=24, md=8, elem_classes="equal-height-col"):
903
+ with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"):
904
+ with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"):
905
+ codeBtn = antd.Button("🧑‍💻 코드 보기", type="default", elem_classes="code-btn")
906
+ historyBtn = antd.Button("📜 히스토리", type="default", elem_classes="history-btn")
907
+ template_btn = antd.Button("🎮 템플릿", type="default", elem_classes="template-btn")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
 
909
+ with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"):
910
+ btn = antd.Button("전송", type="primary", size="large", elem_classes="send-btn")
911
+ boost_btn = antd.Button("증강", type="default", size="large", elem_classes="boost-btn")
912
+ execute_btn = antd.Button("코드", type="default", size="large", elem_classes="execute-btn")
913
+ deploy_btn = antd.Button("배포", type="default", size="large", elem_classes="deploy-btn")
914
+ clear_btn = antd.Button("클리어", type="default", size="large", elem_classes="clear-btn")
915
+
916
+ with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"):
917
+ # ── 배포 결과 메시지가 가장 위에 오도록 ──
918
+ deploy_result_container = gr.Markdown(value="", visible=False)
919
+ input_text = antd.InputTextarea(
920
+ size="large",
921
+ allow_clear=True,
922
+ placeholder=random.choice(DEMO_LIST)['description'],
923
+ max_length=100000
924
  )
925
+ gr.HTML('<div class="help-text">💡 원하는 게임의 설명을 입력하세요. 예: "테트리스 게임 제작해줘."</div>')
926
+
927
 
928
 
929
+ # Code Drawer 열기/닫기
930
  codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
931
+ code_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[code_drawer])
932
 
933
+ # History Drawer 열기/닫기
934
  historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
935
+ history_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[history_drawer])
936
 
937
+ # Template Drawer
938
  template_btn.click(
939
  fn=lambda: (gr.update(open=True), load_all_templates()),
940
+ outputs=[session_drawer, session_history],
941
+ queue=False
942
  )
943
+ session_drawer.close(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
944
+ close_btn.click(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
945
 
946
+ # 전송 버튼
947
  btn.click(
948
  demo_instance.generation_code,
949
  inputs=[input_text, setting, history],
 
951
  )
952
 
953
  # 클리어 버튼
954
+ clear_btn.click(demo_instance.clear_history, inputs=[], outputs=[history])
 
 
 
 
 
 
 
 
 
 
 
955
 
956
  # 증강 버튼
957
  boost_btn.click(
958
  fn=handle_boost,
959
  inputs=[input_text],
960
+ outputs=[input_text, state_tab]
961
  )
962
 
963
+ # 코드 실행 버튼
964
  execute_btn.click(
965
  fn=execute_code,
966
+ inputs=[input_text],
967
  outputs=[sandbox, state_tab]
968
  )
969
 
 
970
  deploy_btn.click(
971
+ fn=deploy_and_show, # 반드시 함수
972
+ inputs=[code_output],
973
+ outputs=[
974
+ deploy_result_container, # 오른쪽 메시지
975
+ sandbox, # 왼쪽 iframe
976
+ state_tab # 탭 전환
977
+ ]
978
+ )
979
  # ------------------------
980
  # 9) 실행
981
  # ------------------------
982
  if __name__ == "__main__":
983
  try:
984
+ demo_instance = Demo()
985
+ demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
 
 
 
 
 
986
  except Exception as e:
987
+ print(f"Initialization error: {e}")
988
+ raise