openfree commited on
Commit
783df91
·
verified ·
1 Parent(s): 6fb9a50

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +891 -427
app.py CHANGED
@@ -24,10 +24,25 @@ import modelscope_studio.components.antd as antd
24
 
25
  # === [1] 로거 설정 ===
26
  log_stream = io.StringIO()
27
- handler = logging.StreamHandler(log_stream)
 
 
 
 
28
  logger = logging.getLogger()
29
- logger.setLevel(logging.DEBUG) # 원하는 레벨로 설정
30
- logger.addHandler(handler)
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  def get_logs():
33
  """StringIO에 쌓인 로그를 문자열로 반환"""
@@ -37,8 +52,7 @@ def get_logs():
37
  # ------------------------
38
  # 1) DEMO_LIST 및 SystemPrompt
39
  # ------------------------
40
-
41
-
42
  DEMO_LIST = [
43
  {"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요. 화살표 키로 조작하며, 가로줄이 채워지면 해당 줄이 제거되고 점수가 올라가는 메커니즘이 필요합니다. 난이도는 시간이 지날수록 블록이 빨라지도록 구현하고, 게임오버 조건과 점수 표시 기능을 포함해주세요."},
44
  {"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요. 기본적인 체스 규칙(킹, 퀸, 룩, 비숍, 나이트, 폰의 이동 규칙)을 구현하고, 체크와 체크메이트 감지 기능이 필요합니다. 드래그 앤 드롭으로 말을 움직일 수 있게 하며, 이동 기록도 표시해주세요."},
@@ -76,7 +90,6 @@ DEMO_LIST = [
76
  {"description": "탑다운 뷰의 간단한 액션 RPG 게임을 개발해주세요. WASD로 이동하고, 마우스 클릭으로 기본 공격, 1-4 키로 특수 스킬을 사용합니다. 플레이어는 몬스터를 처치하며 경험치와 아이템을 획득하고, 레벨업 시 능력치(공격력, 체력, 속도 등)를 향상시킵니다. 다양한 무기와 방어구를 착용할 수 있으며, 스킬 트리 시스템으로 캐릭터를 특화시킬 수 있습니다. 여러 지역과 보스 몬스터, 간단한 퀘스트 시스템도 구현해주세요."},
77
  ]
78
 
79
-
80
  SystemPrompt = """
81
  # GameCraft 시스템 프롬프트
82
 
@@ -118,7 +131,7 @@ SystemPrompt = """
118
  - **간결성 중시**: 코드는 최대한 간결하게 작성하고, 주석은 최소화
119
  - **모듈화**: 코드 기능별로 분리하되 불필요한 추상화 지양
120
  - **최적화**: 게임 루프와 렌더링 최적화에 집중
121
- - **코드 크기 제한**: 전체 코드는 200줄을 넘지 않도록 함
122
 
123
  ### 5.2 성능 최적화
124
  - DOM 조작 최소화
@@ -137,8 +150,8 @@ SystemPrompt = """
137
  - 핵심 접근성 기능에만 집중
138
 
139
  ## 8. 제약사항 및 유의사항
140
- - 외부 API 호출 금지
141
- - 코드 크기 최소화에 우선순위 (200줄 이내)
142
  - 주석 최소화 - 필수적인 설명만 포함
143
  - 불필요한 기능 구현 지양
144
 
@@ -153,20 +166,19 @@ SystemPrompt = """
153
  - 복잡한 기능보다 작동하는 기본 기능 우선
154
  - 불필요한 주석이나 장황한 코드 지양
155
  - 단일 파일에 모든 코드 포함
156
- - 코드 길이 제한: 완성된 게임 코드는 200줄 이내로 작성
157
 
158
  ## 11. 중요: 코드 생성 제한
159
- - 게임 코드는 반드시 200줄 이내로 제한
160
  - 불필요한 설명이나 주석 제외
161
  - 핵심 기능만 구현하고 부가 기능은 생략
162
  - 코드 크기가 커질 경우 기능을 간소화하거나 생략할 것
163
  """
164
 
165
-
166
  # ------------------------
167
  # 2) 공통 상수, 함수, 클래스
168
  # ------------------------
169
-
170
  class Role:
171
  SYSTEM = "system"
172
  USER = "user"
@@ -189,89 +201,212 @@ def get_image_base64(image_path):
189
  IMAGE_CACHE[image_path] = encoded_string
190
  return encoded_string
191
  except:
192
- return IMAGE_CACHE.get('default.png', '')
 
 
193
 
194
  def history_to_messages(history: History, system: str) -> Messages:
195
  messages = [{'role': Role.SYSTEM, 'content': system}]
196
- for h in history:
197
- messages.append({'role': Role.USER, 'content': h[0]})
198
- messages.append({'role': Role.ASSISTANT, 'content': h[1]})
 
 
 
 
 
199
  return messages
200
 
201
  def messages_to_history(messages: Messages) -> History:
202
- assert messages[0]['role'] == Role.SYSTEM
203
  history = []
204
- for q, r in zip(messages[1::2], messages[2::2]):
205
- history.append([q['content'], r['content']])
 
 
 
 
 
 
 
 
 
 
 
206
  return history
207
 
208
 
209
  # ------------------------
210
  # 3) API 연동 설정
211
  # ------------------------
212
-
213
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
214
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
215
 
216
- claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN)
217
- openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN)
218
-
219
- async def try_claude_api(system_message, claude_messages, timeout=15):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
  """
221
- Claude API 호출 (스트리밍)
222
  """
 
 
 
 
223
  try:
224
- system_message_with_limit = system_message + "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석을 최소화하고, 핵심 기능만 구현하세요."
225
-
 
226
  start_time = time.time()
227
- with claude_client.messages.stream(
228
- model="claude-3-7-sonnet-20250219",
229
- max_tokens=19800,
 
 
 
 
 
 
 
 
230
  system=system_message_with_limit,
231
- messages=claude_messages,
232
  temperature=0.3,
233
  ) as stream:
234
  collected_content = ""
235
- for chunk in stream:
236
  current_time = time.time()
237
  if current_time - start_time > timeout:
238
- raise TimeoutError("Claude API timeout")
239
- if chunk.type == "content_block_delta":
 
 
240
  collected_content += chunk.delta.text
241
  yield collected_content
242
- await asyncio.sleep(0)
243
- start_time = current_time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  except Exception as e:
245
- raise e
 
246
 
247
- async def try_openai_api(openai_messages):
248
  """
249
- OpenAI API 호출 (스트리밍) - 코드 길이 제한 강화
250
  """
 
 
 
 
251
  try:
 
252
  if openai_messages and openai_messages[0]["role"] == "system":
253
- openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 200줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석은 최소화하고, 핵심 기능만 구현하세요."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
- stream = openai_client.chat.completions.create(
256
- model="o3",
257
- messages=openai_messages,
 
 
258
  stream=True,
259
- max_tokens=19800,
260
- temperature=0.2
 
261
  )
 
262
  collected_content = ""
 
 
 
 
263
  for chunk in stream:
264
- if chunk.choices[0].delta.content is not None:
265
  collected_content += chunk.choices[0].delta.content
266
  yield collected_content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  except Exception as e:
268
- raise e
 
269
 
270
 
271
  # ------------------------
272
  # 4) 템플릿(하나로 통합)
273
  # ------------------------
274
-
275
  def load_json_data():
276
  data_list = []
277
  for item in DEMO_LIST:
@@ -301,6 +436,9 @@ def create_template_html(title, items):
301
  cursor: pointer;
302
  box-shadow: 0 4px 8px rgba(0,0,0,0.05);
303
  transition: all 0.3s ease;
 
 
 
304
  }
305
  .prompt-card:hover {
306
  transform: translateY(-4px);
@@ -311,49 +449,83 @@ def create_template_html(title, items):
311
  margin-bottom: 8px;
312
  font-size: 13px;
313
  color: #444;
 
 
 
 
 
 
 
 
 
314
  }
315
  .card-prompt {
316
  font-size: 11px;
317
  line-height: 1.4;
318
  color: #666;
319
  display: -webkit-box;
320
- -webkit-line-clamp: 7;
321
  -webkit-box-orient: vertical;
322
  overflow: hidden;
323
- height: 84px;
324
- background-color: #f8f9fa;
325
- padding: 8px;
326
- border-radius: 6px;
327
  }
328
  </style>
329
  <div class="prompt-grid">
330
  """
331
- import html as html_lib
332
  for item in items:
 
 
333
  card_html = f"""
334
- <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html_lib.escape(item.get('prompt', ''))}">
335
- <div class="card-name">{html_lib.escape(item.get('name', ''))}</div>
336
- <div class="card-prompt">{html_lib.escape(item.get('prompt', ''))}</div>
 
 
337
  </div>
338
  """
339
  html_content += card_html
340
  html_content += r"""
 
341
  <script>
342
  function copyToInput(card) {
343
  const prompt = card.dataset.prompt;
344
- const textarea = document.querySelector('.ant-input-textarea-large textarea');
 
345
  if (textarea) {
346
  textarea.value = prompt;
 
347
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
348
- // 템플릿 Drawer 닫기
349
- document.querySelector('.session-drawer .close-btn').click();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  }
351
  }
352
  </script>
353
- </div>
354
  """
 
355
  return gr.HTML(value=html_content)
356
 
 
357
  def load_all_templates():
358
  return create_template_html("🎮 모든 게임 템플릿", load_json_data())
359
 
@@ -362,165 +534,308 @@ def load_all_templates():
362
  # 5) 배포/부스트/기타 유틸
363
  # ------------------------
364
 
365
- def remove_code_block(text):
366
- pattern = r'```html\s*([\s\S]+?)\s*```'
367
- match = re.search(pattern, text, re.DOTALL)
368
- if match:
369
- return match.group(1).strip()
370
-
371
- pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
372
- match = re.search(pattern, text, re.DOTALL)
373
- if match:
374
- return match.group(1).strip()
375
 
376
- text = re.sub(r'```html\s*', '', text)
377
- text = re.sub(r'\s*```', '', text)
378
- return text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
380
  def optimize_code(code: str) -> str:
 
381
  if not code or len(code.strip()) == 0:
382
- return code
383
-
 
384
  lines = code.split('\n')
385
- if len(lines) <= 200:
386
- return code
387
-
388
- comment_patterns = [
389
- r'/\*[\s\S]*?\*/',
390
- r'//.*?$',
391
- r'<!--[\s\S]*?-->'
392
- ]
393
- cleaned_code = code
394
- for pattern in comment_patterns:
395
- cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE)
396
-
 
397
  cleaned_lines = []
398
  empty_line_count = 0
399
- for line in cleaned_code.split('\n'):
400
- if line.strip() == '':
 
401
  empty_line_count += 1
402
- if empty_line_count <= 1:
403
  cleaned_lines.append('')
404
  else:
405
  empty_line_count = 0
406
- cleaned_lines.append(line)
407
-
408
- cleaned_code = '\n'.join(cleaned_lines)
409
- cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE)
410
- cleaned_code = re.sub(r' {2,}', ' ', cleaned_code)
 
 
 
 
 
411
  return cleaned_code
412
 
 
413
  def send_to_sandbox(code):
414
- clean_code = remove_code_block(code)
415
- clean_code = optimize_code(clean_code)
416
-
417
- if clean_code.startswith('```html'):
418
- clean_code = clean_code[7:].strip()
419
- if clean_code.endswith('```'):
420
- clean_code = clean_code[:-3].strip()
421
-
422
- if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
423
- clean_code = f"""<!DOCTYPE html>
424
- <html>
 
 
 
 
 
 
 
425
  <head>
426
  <meta charset="UTF-8">
427
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
428
  <title>Game Preview</title>
 
429
  </head>
430
  <body>
431
  {clean_code}
432
  </body>
433
  </html>"""
434
- encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
435
- data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
436
- return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
437
 
438
  def boost_prompt(prompt: str) -> str:
 
439
  if not prompt:
440
  return ""
 
441
  boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다.
442
  주어진 프롬프트를 분석하여 더 명확하고 간결한 요구사항으로 변환하되,
443
  원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
444
 
445
- 1. 게임플레이 핵심 메커니즘 명확히 정의
446
- 2. 필수적인 상호작용 요소만 포함
447
- 3. 핵심 UI 요소 간략히 기술
448
- 4. 코드 간결성 유지를 위한 우선순위 설정
449
- 5. 기본적인 게임 규칙과 승리/패배 조건 명시
450
 
451
  다음 중요 지침을 반드시 준수하세요:
452
- - 불필요한 세부 사항이나 부가 기능은 제외
453
  - 생성될 코드가 600줄을 넘지 않도록 기능을 제한
454
- - 명확하고 간결한 언어로 요구사항 작성
455
  - 최소한의 필수 게임 요소만 포함
 
456
  """
 
 
 
457
  try:
458
- try:
 
459
  response = claude_client.messages.create(
460
- model="claude-3-7-sonnet-20250219",
461
- max_tokens=10000,
462
- temperature=0.3,
463
  messages=[{
464
  "role": "user",
465
- "content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}"
466
  }],
467
  system=boost_system_prompt
468
  )
469
- if hasattr(response, 'content') and len(response.content) > 0:
470
- return response.content[0].text
471
- raise Exception("Claude API 응답 형식 오류")
472
- except Exception:
 
 
 
 
 
 
473
  completion = openai_client.chat.completions.create(
474
- model="gpt-4",
475
  messages=[
476
  {"role": "system", "content": boost_system_prompt},
477
- {"role": "user", "content": f"다음 게임 프롬프트를 분석하고 증강하되, 간결함을 유지하세요: {prompt}"}
478
  ],
479
- max_tokens=10000,
480
- temperature=0.3
481
  )
482
- if completion.choices and len(completion.choices) > 0:
483
- return completion.choices[0].message.content
484
- raise Exception("OpenAI API 응답 형식 오류")
485
- except Exception:
486
- return prompt
 
 
 
 
 
 
 
 
 
 
 
 
 
 
487
 
488
  def handle_boost(prompt: str):
 
 
 
 
 
 
 
489
  try:
490
- boosted_prompt = boost_prompt(prompt)
491
- return boosted_prompt, gr.update(active_key="empty")
492
- except Exception:
493
- return prompt, gr.update(active_key="empty")
 
 
 
 
 
 
494
 
495
  def history_render(history: History):
 
 
 
 
496
  return gr.update(open=True), history
497
 
 
498
  def execute_code(query: str):
499
- if not query or query.strip() == '':
500
- return None, gr.update(active_key="empty")
 
 
 
 
501
  try:
 
502
  clean_code = remove_code_block(query)
503
- if clean_code.startswith('```html'):
504
- clean_code = clean_code[7:].strip()
505
- if clean_code.endswith('```'):
506
- clean_code = clean_code[:-3].strip()
507
- if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'):
508
- if not ('<body' in clean_code and '</body>' in clean_code):
509
- clean_code = f"""<!DOCTYPE html>
510
- <html>
511
- <head>
512
- <meta charset="UTF-8">
513
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
514
- <title>Game Preview</title>
515
- </head>
516
- <body>
517
- {clean_code}
518
- </body>
519
- </html>"""
520
- return send_to_sandbox(clean_code), gr.update(active_key="render")
521
  except Exception as e:
522
- print(f"Execute code error: {str(e)}")
523
- return None, gr.update(active_key="empty")
 
 
524
 
525
 
526
  # ------------------------
@@ -529,228 +844,341 @@ def execute_code(query: str):
529
 
530
  class Demo:
531
  def __init__(self):
532
- pass
533
-
534
  async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
535
- if not query or query.strip() == '':
 
 
 
 
 
536
  query = random.choice(DEMO_LIST)['description']
537
-
 
 
538
  if _history is None:
539
  _history = []
540
-
541
- query = f"""
542
- 다음 게임을 제작해주세요.
543
- 중요 요구사항:
544
- 1. 코드는 가능한 한 간결하게 작성할 것
545
- 2. 불필요한 주석이나 설명은 제외할 것
546
- 3. 코드는 600줄을 넘지 않을 것
547
- 4. 모든 코드는 하나의 HTML 파일에 통합할 것
548
- 5. 핵심 기능만 구현하고 부가 기능은 생략할
549
- 게임 요청: {query}
 
 
 
 
 
 
 
550
  """
551
-
552
- messages = history_to_messages(_history, _setting['system'])
553
- system_message = messages[0]['content']
554
-
555
- claude_messages = [
556
- {"role": msg["role"] if msg["role"] != "system" else "user", "content": msg["content"]}
557
- for msg in messages[1:] + [{'role': Role.USER, 'content': query}]
558
- if msg["content"].strip() != ''
559
- ]
560
-
561
- openai_messages = [{"role": "system", "content": system_message}]
562
- for msg in messages[1:]:
563
- openai_messages.append({
564
- "role": msg["role"],
565
- "content": msg["content"]
566
- })
567
- openai_messages.append({"role": "user", "content": query})
568
-
569
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  yield [
571
- "Generating code...",
572
  _history,
573
  None,
574
- gr.update(active_key="loading"),
575
- gr.update(open=True)
576
  ]
577
- await asyncio.sleep(0)
578
- collected_content = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
579
  try:
580
- async for content in try_claude_api(system_message, claude_messages):
 
 
 
 
581
  yield [
582
- content,
583
  _history,
584
  None,
585
  gr.update(active_key="loading"),
586
  gr.update(open=True)
587
  ]
588
- await asyncio.sleep(0)
589
- collected_content = content
590
- except Exception:
591
- async for content in try_openai_api(openai_messages):
 
 
 
 
 
 
 
 
 
 
 
 
 
592
  yield [
593
- content,
594
  _history,
595
  None,
596
  gr.update(active_key="loading"),
597
  gr.update(open=True)
598
  ]
599
- await asyncio.sleep(0)
600
- collected_content = content
601
-
602
- if collected_content:
603
- clean_code = remove_code_block(collected_content)
604
- code_lines = clean_code.count('\n') + 1
605
- if code_lines > 700:
606
- warning_msg = f"""
607
- ⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄)**
608
- 이로 인해 실행 시 오류가 발생할 수 있습니다. 다음과 같이 시도해 보세요:
609
- 1. 간단한 게임을 요청하세요
610
- 2. 특정 기능만 명시하여 요청하세요 (예: "간단한 Snake 게임, 점수 시스템 없이")
611
- 3. "코드" 버튼을 사용하여 직접 실행해 보세요
 
 
 
 
 
 
 
 
 
 
 
 
 
 
612
  ```html
613
- {clean_code[:2000]}
614
- ... (코드가 너무 깁니다) ... """
615
- collected_content = warning_msg
616
- yield [
617
- collected_content,
618
- _history,
619
- None,
620
- gr.update(active_key="empty"),
621
- gr.update(open=True)
622
- ]
623
- else:
624
- _history = messages_to_history([
625
- {'role': Role.SYSTEM, 'content': system_message}
626
- ] + claude_messages + [{
627
- 'role': Role.ASSISTANT,
628
- 'content': collected_content
629
- }])
630
- yield [
631
- collected_content,
632
- _history,
633
- send_to_sandbox(clean_code),
634
- gr.update(active_key="render"),
635
- gr.update(open=True)
636
- ]
637
  else:
638
- raise ValueError("No content was generated from either API")
639
- except Exception as e:
640
- raise ValueError(f'Error calling APIs: {str(e)}')
641
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  def clear_history(self):
643
- return []
 
 
 
 
 
 
 
 
 
 
 
644
 
645
 
646
  ####################################################
647
- # 1) deploy_to_vercel 함수
648
  ####################################################
649
  def deploy_to_vercel(code: str):
650
- print(f"[DEBUG] deploy_to_vercel() 시작. code 길이: {len(code) if code else 0}")
651
- try:
652
- if not code or len(code.strip()) < 10:
653
- print("[DEBUG] 배포 불가: code가 짧음")
654
- return "No code to deploy."
655
-
656
- token = "A8IFZmgW2cqA4yUNlLPnci0N"
657
- if not token:
658
- print("[DEBUG] Vercel 토큰이 없음.")
659
- return "Vercel token is not set."
660
-
661
- project_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6))
662
- print(f"[DEBUG] 생성된 project_name: {project_name}")
663
-
664
- deploy_url = "https://api.vercel.com/v13/deployments"
665
- headers = {
666
- "Authorization": f"Bearer {token}",
667
- "Content-Type": "application/json"
668
- }
 
669
 
670
- package_json = {
671
- "name": project_name,
672
- "version": "1.0.0",
673
- "private": True,
674
- "dependencies": {"vite": "^5.0.0"},
675
- "scripts": {
676
- "dev": "vite",
677
- "build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/",
678
- "preview": "vite preview"
679
- }
680
- }
681
 
682
- files = [
683
- {"file": "index.html", "data": code},
684
- {"file": "package.json", "data": json.dumps(package_json, indent=2)}
685
- ]
686
- project_settings = {
687
- "buildCommand": "npm run build",
688
- "outputDirectory": "dist",
689
- "installCommand": "npm install",
690
- "framework": None
691
- }
692
 
693
- deploy_data = {
694
- "name": project_name,
695
- "files": files,
696
- "target": "production",
697
- "projectSettings": project_settings
 
 
 
 
698
  }
 
699
 
700
- print("[DEBUG] Vercel API 요청 전송중...")
701
- deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data)
702
- print("[DEBUG] 응답 status_code:", deploy_response.status_code)
 
703
 
704
- if deploy_response.status_code != 200:
705
- print("[DEBUG] 배포 실패:", deploy_response.text)
706
- return f"Deployment failed: {deploy_response.text}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
707
 
708
- deployment_url = f"https://{project_name}.vercel.app"
709
- print(f"[DEBUG] 배포 성공 -> URL: {deployment_url}")
710
- time.sleep(5)
711
 
 
 
 
712
 
713
- # Markdown 링크로 반환
714
- return f"""
715
- ✅ **Deployment complete!**
716
- Your app is live at:
717
- [**{deployment_url}**]({deployment_url})
718
- """
719
 
 
 
 
 
720
 
721
-
722
- except Exception as e:
723
- print("[ERROR] deploy_to_vercel() 예외:", e)
724
- return f"Error during deployment: {str(e)}"
725
 
 
 
 
 
726
 
 
 
 
727
 
728
- # ------------------------
729
- # (3) handle_deploy_legacy
730
- # ------------------------
731
- def handle_deploy_legacy(code):
732
- logger.debug(f"[handle_deploy_legacy] code 길이: {len(code) if code else 0}")
733
- if not code or len(code.strip()) < 10:
734
- logger.info("[handle_deploy_legacy] 코드가 짧음.")
735
- return "<div style='color:red;'>배포할 코드가 없습니다.</div>"
736
-
737
- clean_code = remove_code_block(code)
738
- logger.debug(f"[handle_deploy_legacy] remove_code_block 후 길이: {len(clean_code)}")
739
-
740
- result_html = deploy_to_vercel(clean_code)
741
- logger.debug(f"[handle_deploy_legacy] 배포 결과 HTML 길이: {len(result_html)}")
742
-
743
- encoded_html = base64.b64encode(result_html.encode('utf-8')).decode()
744
- data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
745
-
746
- iframe_html = f"""
747
- <iframe src="{data_uri}"
748
- style="width:100%; height:600px; border:none;"
749
- sandbox="allow-scripts allow-same-origin allow-popups">
750
- </iframe>
751
- """
752
- logger.debug("[handle_deploy_legacy] iframe_html 반환")
753
- return iframe_html
754
 
755
 
756
  # ------------------------
@@ -766,152 +1194,188 @@ theme = gr.themes.Soft(
766
  text_size=gr.themes.sizes.text_md,
767
  )
768
 
769
- with gr.Blocks(css_paths=["app.css"], theme=theme) as demo:
770
- header_html = gr.HTML("""
 
771
  <div class="app-header">
772
  <h1>🎮 Vibe Game Craft</h1>
773
- <p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 실시간 미리보기와 배포 기능도 지원됩니다.</p>
774
  </div>
775
- <!-- 배포 결과 박스 - 헤더 바로 아래 위치 -->
776
- <div id="deploy-banner" style="display:none;" class="deploy-banner">
777
- <!-- (생략) ... 배너 스타일/스크립트 ... -->
778
- </div>
779
- <style>
780
- /* (생략) ... CSS ... */
781
- </style>
782
- <script>
783
- /* (생략) ... JS copyBannerUrl / showDeployBanner ... */
784
- </script>
785
- """)
786
 
 
787
  history = gr.State([])
788
  setting = gr.State({"system": SystemPrompt})
789
- deploy_status = gr.State({"is_deployed": False,"status": "","url": "","message": ""})
790
 
791
  with ms.Application() as app:
792
- with antd.ConfigProvider():
793
-
794
- with antd.Drawer(open=False, title="코드 보기", placement="left", width="750px") as code_drawer:
795
- code_output = legacy.Markdown()
796
-
797
- with antd.Drawer(open=False, title="히스토리", placement="left", width="900px") as history_drawer:
798
- history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot")
799
-
 
 
 
 
 
 
 
 
 
 
800
  with antd.Drawer(
801
  open=False,
802
- title="게임 템플릿",
803
  placement="right",
804
- width="900px",
 
805
  elem_classes="session-drawer"
806
  ) as session_drawer:
807
  with antd.Flex(vertical=True, gap="middle"):
808
- gr.Markdown("### 사용 가능한 게임 템플릿")
809
- session_history = gr.HTML(elem_classes="session-history")
810
- close_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
 
811
 
812
- with antd.Row(gutter=[32, 12], align="top", elem_classes="equal-height-container") as layout:
 
813
 
814
- # 왼쪽 Col
815
- with antd.Col(span=24, md=16, elem_classes="equal-height-col"):
816
- with ms.Div(elem_classes="right_panel panel"):
817
  gr.HTML(r"""
818
- <div class="render_header">
819
- <span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span>
820
- </div>
821
- """)
822
- with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
 
 
823
  with antd.Tabs.Item(key="empty"):
824
- empty = antd.Empty(description="게임을 만들려면 설명을 입력하세요", elem_classes="right_content")
825
  with antd.Tabs.Item(key="loading"):
826
- loading = antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content")
827
  with antd.Tabs.Item(key="render"):
 
828
  sandbox = gr.HTML(elem_classes="html_content")
829
 
830
- # 오른쪽 Col
831
- with antd.Col(span=24, md=8, elem_classes="equal-height-col"):
832
- with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"):
833
- with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"):
834
- codeBtn = antd.Button("🧑‍💻 코드 보기", type="default", elem_classes="code-btn")
835
- historyBtn = antd.Button("📜 히스토리", type="default", elem_classes="history-btn")
836
- template_btn = antd.Button("🎮 템플릿", type="default", elem_classes="template-btn")
837
-
838
- with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"):
839
- btn = antd.Button("전송", type="primary", size="large", elem_classes="send-btn")
840
- boost_btn = antd.Button("증강", type="default", size="large", elem_classes="boost-btn")
841
- execute_btn = antd.Button("코드", type="default", size="large", elem_classes="execute-btn")
842
- deploy_btn = antd.Button("배포", type="default", size="large", elem_classes="deploy-btn")
843
- clear_btn = antd.Button("클리어", type="default", size="large", elem_classes="clear-btn")
844
-
845
- with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"):
846
  input_text = antd.InputTextarea(
847
  size="large",
848
  allow_clear=True,
849
- placeholder=random.choice(DEMO_LIST)['description'],
850
- max_length=100000
 
 
 
851
  )
852
- gr.HTML('<div class="help-text">💡 원하는 게임의 설명을 입력하세요. 예: "테트리스 게임 제작해줘."</div>')
853
-
854
- # Markdown으로 배포 결과 표시
 
 
 
 
 
 
 
 
 
 
855
  deploy_result_container = gr.Markdown(
856
- value="아직 배포된 게임이 없습니다.",
857
- label="Deployment Result"
 
858
  )
859
 
860
- # Code Drawer 열기/닫기
 
 
861
  codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
862
- code_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[code_drawer])
863
 
864
- # History Drawer 열기/닫기
865
  historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
866
- history_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[history_drawer])
867
 
868
- # Template Drawer
869
  template_btn.click(
870
  fn=lambda: (gr.update(open=True), load_all_templates()),
871
- outputs=[session_drawer, session_history],
872
- queue=False
873
  )
874
- session_drawer.close(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
875
- close_btn.click(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history])
 
876
 
877
- # 전송 버튼
878
  btn.click(
879
  demo_instance.generation_code,
880
  inputs=[input_text, setting, history],
881
- outputs=[code_output, history, sandbox, state_tab, code_drawer]
 
882
  )
883
 
884
- # 클리어 버튼
885
- clear_btn.click(demo_instance.clear_history, inputs=[], outputs=[history])
 
 
 
 
 
886
 
887
- # 증강 버튼
888
  boost_btn.click(
889
  fn=handle_boost,
890
  inputs=[input_text],
891
- outputs=[input_text, state_tab]
 
892
  )
893
 
894
- # 코드 실행 버튼
895
  execute_btn.click(
896
  fn=execute_code,
897
- inputs=[input_text],
898
- outputs=[sandbox, state_tab]
 
899
  )
900
 
901
- # 배포 버튼 → deploy_result_container (Markdown)
 
902
  deploy_btn.click(
903
- fn=lambda code: deploy_to_vercel(remove_code_block(code)) if code else "No code generated.",
904
- inputs=[code_output],
905
- outputs=[deploy_result_container]
 
906
  )
907
 
908
  # ------------------------
909
  # 9) 실행
910
  # ------------------------
911
  if __name__ == "__main__":
 
912
  try:
913
- demo_instance = Demo()
914
- demo.queue(default_concurrency_limit=20).launch(ssr_mode=False)
 
 
 
 
 
 
915
  except Exception as e:
916
- print(f"Initialization error: {e}")
917
- raise
 
24
 
25
  # === [1] 로거 설정 ===
26
  log_stream = io.StringIO()
27
+ # Use StreamHandler to output to console *and* capture in StringIO
28
+ console_handler = logging.StreamHandler()
29
+ stringio_handler = logging.StreamHandler(log_stream)
30
+
31
+ # Configure root logger
32
  logger = logging.getLogger()
33
+ logger.setLevel(logging.DEBUG) # Set level for root logger
34
+ logger.handlers.clear() # Clear existing handlers if any
35
+ logger.addHandler(console_handler) # Add console output
36
+ logger.addHandler(stringio_handler) # Add StringIO capture
37
+
38
+ # Configure specific loggers if needed (optional)
39
+ logging.getLogger("httpx").setLevel(logging.WARNING)
40
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
41
+ logging.getLogger("openai").setLevel(logging.WARNING)
42
+ logging.getLogger("anthropic").setLevel(logging.WARNING)
43
+ logging.getLogger("requests").setLevel(logging.WARNING)
44
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
45
+
46
 
47
  def get_logs():
48
  """StringIO에 쌓인 로그를 문자열로 반환"""
 
52
  # ------------------------
53
  # 1) DEMO_LIST 및 SystemPrompt
54
  # ------------------------
55
+ # (DEMO_LIST and SystemPrompt remain the same)
 
56
  DEMO_LIST = [
57
  {"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요. 화살표 키로 조작하며, 가로줄이 채워지면 해당 줄이 제거되고 점수가 올라가는 메커니즘이 필요합니다. 난이도는 시간이 지날수록 블록이 빨라지도록 구현하고, 게임오버 조건과 점수 표시 기능을 포함해주세요."},
58
  {"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요. 기본적인 체스 규칙(킹, 퀸, 룩, 비숍, 나이트, 폰의 이동 규칙)을 구현하고, 체크와 체크메이트 감지 기능이 필요합니다. 드래그 앤 드롭으로 말을 움직일 수 있게 하며, 이동 기록도 표시해주세요."},
 
90
  {"description": "탑다운 뷰의 간단한 액션 RPG 게임을 개발해주세요. WASD로 이동하고, 마우스 클릭으로 기본 공격, 1-4 키로 특수 스킬을 사용합니다. 플레이어는 몬스터를 처치하며 경험치와 아이템을 획득하고, 레벨업 시 능력치(공격력, 체력, 속도 등)를 향상시킵니다. 다양한 무기와 방어구를 착용할 수 있으며, 스킬 트리 시스템으로 캐릭터를 특화시킬 수 있습니다. 여러 지역과 보스 몬스터, 간단한 퀘스트 시스템도 구현해주세요."},
91
  ]
92
 
 
93
  SystemPrompt = """
94
  # GameCraft 시스템 프롬프트
95
 
 
131
  - **간결성 중시**: 코드는 최대한 간결하게 작성하고, 주석은 최소화
132
  - **모듈화**: 코드 기능별로 분리하되 불필요한 추상화 지양
133
  - **최적화**: 게임 루프와 렌더링 최적화에 집중
134
+ - **코드 크기 제한**: 전체 코드는 200줄을 넘지 않도록 함 (API 호출 시에는 600줄 제한 고려)
135
 
136
  ### 5.2 성능 최적화
137
  - DOM 조작 최소화
 
150
  - 핵심 접근성 기능에만 집중
151
 
152
  ## 8. 제약사항 및 유의사항
153
+ - 외부 API 호출 금지 (Vercel 배포 제외)
154
+ - 코드 크기 최소화에 우선순위 (API 호출 시 600줄 이내, 최종 목표 200줄)
155
  - 주석 최소화 - 필수적인 설명만 포함
156
  - 불필요한 기능 구현 지양
157
 
 
166
  - 복잡한 기능보다 작동하는 기본 기능 우선
167
  - 불필요한 주석이나 장황한 코드 지양
168
  - 단일 파일에 모든 코드 포함
169
+ - 코드 길이 제한: 완성된 게임 코드는 600줄 이내로 작성 (API 호출 시), 최종 목표 200줄
170
 
171
  ## 11. 중요: 코드 생성 제한
172
+ - 게임 코드는 반드시 600줄 이내로 제한 (API 호출 시), 최종 목표 200줄
173
  - 불필요한 설명이나 주석 제외
174
  - 핵심 기능만 구현하고 부가 기능은 생략
175
  - 코드 크기가 커질 경우 기능을 간소화하거나 생략할 것
176
  """
177
 
 
178
  # ------------------------
179
  # 2) 공통 상수, 함수, 클래스
180
  # ------------------------
181
+ # (Role, History, Messages, IMAGE_CACHE, get_image_base64, history_to_messages, messages_to_history remain the same)
182
  class Role:
183
  SYSTEM = "system"
184
  USER = "user"
 
201
  IMAGE_CACHE[image_path] = encoded_string
202
  return encoded_string
203
  except:
204
+ # Provide a default placeholder if needed, or handle error
205
+ logger.error(f"Failed to read image: {image_path}")
206
+ return IMAGE_CACHE.get('default.png', '') # Assuming you might have a default
207
 
208
  def history_to_messages(history: History, system: str) -> Messages:
209
  messages = [{'role': Role.SYSTEM, 'content': system}]
210
+ if history: # Check if history is not None and not empty
211
+ for h in history:
212
+ # Ensure h is a tuple/list of length 2
213
+ if isinstance(h, (list, tuple)) and len(h) == 2:
214
+ messages.append({'role': Role.USER, 'content': h[0]})
215
+ messages.append({'role': Role.ASSISTANT, 'content': h[1]})
216
+ else:
217
+ logger.warning(f"Skipping invalid history item: {h}")
218
  return messages
219
 
220
  def messages_to_history(messages: Messages) -> History:
 
221
  history = []
222
+ # Ensure messages list has the expected structure
223
+ if not messages or messages[0]['role'] != Role.SYSTEM:
224
+ logger.error("Invalid messages format for conversion to history.")
225
+ return history # Return empty history
226
+
227
+ # Iterate through pairs of user/assistant messages
228
+ for i in range(1, len(messages), 2):
229
+ if i + 1 < len(messages) and messages[i]['role'] == Role.USER and messages[i+1]['role'] == Role.ASSISTANT:
230
+ history.append([messages[i]['content'], messages[i+1]['content']])
231
+ else:
232
+ # Log if the structure is not as expected, but continue if possible
233
+ logger.warning(f"Skipping unexpected message sequence at index {i} during history conversion.")
234
+ # break # Option: stop conversion if structure is broken
235
  return history
236
 
237
 
238
  # ------------------------
239
  # 3) API 연동 설정
240
  # ------------------------
241
+ # (API clients and functions remain the same)
242
  YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip()
243
  YOUR_OPENAI_TOKEN = os.getenv('OPENAI_API_KEY', '').strip()
244
 
245
+ # Add basic error handling for missing keys
246
+ if not YOUR_ANTHROPIC_TOKEN:
247
+ logger.warning("ANTHROPIC_API_KEY is not set. Claude API calls will fail.")
248
+ # Optionally disable Claude features or raise an error
249
+ if not YOUR_OPENAI_TOKEN:
250
+ logger.warning("OPENAI_API_KEY is not set. OpenAI API calls will fail.")
251
+ # Optionally disable OpenAI features or raise an error
252
+
253
+ # Initialize clients, handle potential errors during initialization
254
+ try:
255
+ claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN) if YOUR_ANTHROPIC_TOKEN else None
256
+ except Exception as e:
257
+ logger.error(f"Failed to initialize Anthropic client: {e}")
258
+ claude_client = None
259
+
260
+ try:
261
+ openai_client = openai.OpenAI(api_key=YOUR_OPENAI_TOKEN) if YOUR_OPENAI_TOKEN else None
262
+ except Exception as e:
263
+ logger.error(f"Failed to initialize OpenAI client: {e}")
264
+ openai_client = None
265
+
266
+
267
+ async def try_claude_api(system_message, claude_messages, timeout=30): # Increased timeout
268
  """
269
+ Claude API 호출 (스트리밍) - 코드 길이 제한 강화 및 에러 핸들링
270
  """
271
+ if not claude_client:
272
+ logger.error("Claude client not initialized. Cannot call API.")
273
+ raise ConnectionError("Claude client not available.")
274
+
275
  try:
276
+ # Ensure system message has the length constraint
277
+ system_message_with_limit = system_message + "\n\n추가 중요 지침: 생성하는 코드는 절대로 600줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석을 최소화하고, 핵심 기능만 구현하세요."
278
+
279
  start_time = time.time()
280
+ logger.debug(f"Calling Claude API. System prompt length: {len(system_message_with_limit)}, Messages count: {len(claude_messages)}")
281
+
282
+ # Filter out empty messages just before API call
283
+ valid_claude_messages = [msg for msg in claude_messages if msg.get("content", "").strip()]
284
+ if not valid_claude_messages:
285
+ logger.warning("No valid messages to send to Claude API after filtering.")
286
+ raise ValueError("No content provided for Claude API call.")
287
+
288
+ async with claude_client.messages.stream(
289
+ model="claude-3-5-sonnet-20240620", # Use the latest Sonnet model
290
+ max_tokens=4000, # Adjusted max_tokens based on model limits and typical game code size
291
  system=system_message_with_limit,
292
+ messages=valid_claude_messages,
293
  temperature=0.3,
294
  ) as stream:
295
  collected_content = ""
296
+ async for chunk in stream:
297
  current_time = time.time()
298
  if current_time - start_time > timeout:
299
+ logger.warning("Claude API call timed out.")
300
+ raise TimeoutError(f"Claude API timeout after {timeout} seconds")
301
+
302
+ if chunk.type == "content_block_delta" and hasattr(chunk, 'delta') and hasattr(chunk.delta, 'text'):
303
  collected_content += chunk.delta.text
304
  yield collected_content
305
+ # No sleep needed here, await stream handles it
306
+ elif chunk.type == "message_stop":
307
+ logger.debug("Claude stream finished.")
308
+ break # Explicitly break on stop event
309
+ # Add handling for other chunk types if necessary (e.g., errors)
310
+ elif chunk.type == "error":
311
+ logger.error(f"Claude API stream error: {chunk.error}")
312
+ raise anthropic.APIError(f"Claude stream error: {chunk.error}")
313
+
314
+
315
+ # Ensure final content is yielded if stream ends without a final delta
316
+ # yield collected_content # This might yield duplicate final content, handled by loop structure
317
+
318
+ except anthropic.APIConnectionError as e:
319
+ logger.error(f"Claude API connection error: {e}")
320
+ raise ConnectionError(f"Failed to connect to Anthropic API: {e}")
321
+ except anthropic.RateLimitError as e:
322
+ logger.error(f"Claude API rate limit exceeded: {e}")
323
+ raise ConnectionAbortedError(f"Anthropic API rate limit hit: {e}")
324
+ except anthropic.APIStatusError as e:
325
+ logger.error(f"Claude API status error: {e.status_code} - {e.response}")
326
+ raise ConnectionRefusedError(f"Anthropic API error ({e.status_code}): {e.message}")
327
+ except TimeoutError as e:
328
+ # Already logged above
329
+ raise e
330
  except Exception as e:
331
+ logger.error(f"Unexpected error during Claude API call: {e}", exc_info=True)
332
+ raise RuntimeError(f"An unexpected error occurred with Claude API: {e}")
333
 
334
+ async def try_openai_api(openai_messages, timeout=30): # Added timeout
335
  """
336
+ OpenAI API 호출 (스트리밍) - 코드 길이 제한 강화 및 에러 핸들링
337
  """
338
+ if not openai_client:
339
+ logger.error("OpenAI client not initialized. Cannot call API.")
340
+ raise ConnectionError("OpenAI client not available.")
341
+
342
  try:
343
+ # Ensure system message has the length constraint if present
344
  if openai_messages and openai_messages[0]["role"] == "system":
345
+ openai_messages[0]["content"] += "\n\n추가 중요 지침: 생성하는 코드는 절대로 600줄을 넘지 마세요. 코드 간결성이 최우선입니다. 주석은 최소화하고, 핵심 기능만 구현하세요."
346
+ system_prompt_length = len(openai_messages[0]["content"])
347
+ else:
348
+ # Add system message if missing? Or handle differently? Assuming it should exist.
349
+ logger.warning("OpenAI messages do not start with a system role.")
350
+ system_prompt_length = 0
351
+
352
+
353
+ logger.debug(f"Calling OpenAI API (gpt-4o-mini). System prompt length: {system_prompt_length}, Messages count: {len(openai_messages)}")
354
+
355
+ # Filter out empty messages just before API call
356
+ valid_openai_messages = [msg for msg in openai_messages if msg.get("content", "").strip()]
357
+ if not valid_openai_messages:
358
+ logger.warning("No valid messages to send to OpenAI API after filtering.")
359
+ raise ValueError("No content provided for OpenAI API call.")
360
 
361
+
362
+ stream = await asyncio.to_thread( # Use asyncio.to_thread for blocking sync call
363
+ openai_client.chat.completions.create,
364
+ model="gpt-4o-mini", # Use a faster/cheaper model if appropriate
365
+ messages=valid_openai_messages,
366
  stream=True,
367
+ max_tokens=4000, # Adjusted
368
+ temperature=0.2,
369
+ timeout=timeout # Pass timeout to the API call itself
370
  )
371
+
372
  collected_content = ""
373
+ # Iterate over the stream (sync iterator needs to be handled carefully in async)
374
+ # This part might need adjustment depending on how the sync stream behaves in `asyncio.to_thread`
375
+ # A simple loop might block. Consider processing the stream differently if needed.
376
+ # For simplicity, assuming the sync iterator works okay here, but watch for blocking.
377
  for chunk in stream:
378
+ if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content is not None:
379
  collected_content += chunk.choices[0].delta.content
380
  yield collected_content
381
+ # await asyncio.sleep(0) # Allow other tasks to run
382
+ # Handle finish reason if needed
383
+ if chunk.choices and chunk.choices[0].finish_reason:
384
+ logger.debug(f"OpenAI stream finished. Reason: {chunk.choices[0].finish_reason}")
385
+ break
386
+
387
+ # yield collected_content # Yield final content if needed (handled by loop)
388
+
389
+ except openai.APIConnectionError as e:
390
+ logger.error(f"OpenAI API connection error: {e}")
391
+ raise ConnectionError(f"Failed to connect to OpenAI API: {e}")
392
+ except openai.RateLimitError as e:
393
+ logger.error(f"OpenAI API rate limit exceeded: {e}")
394
+ raise ConnectionAbortedError(f"OpenAI API rate limit hit: {e}")
395
+ except openai.APIStatusError as e:
396
+ logger.error(f"OpenAI API status error: {e.status_code} - {e.response}")
397
+ raise ConnectionRefusedError(f"OpenAI API error ({e.status_code}): {e.message}")
398
+ except openai.APITimeoutError as e:
399
+ logger.warning(f"OpenAI API call timed out: {e}")
400
+ raise TimeoutError(f"OpenAI API timeout: {e}")
401
  except Exception as e:
402
+ logger.error(f"Unexpected error during OpenAI API call: {e}", exc_info=True)
403
+ raise RuntimeError(f"An unexpected error occurred with OpenAI API: {e}")
404
 
405
 
406
  # ------------------------
407
  # 4) 템플릿(하나로 통합)
408
  # ------------------------
409
+ # (load_json_data, create_template_html, load_all_templates remain the same)
410
  def load_json_data():
411
  data_list = []
412
  for item in DEMO_LIST:
 
436
  cursor: pointer;
437
  box-shadow: 0 4px 8px rgba(0,0,0,0.05);
438
  transition: all 0.3s ease;
439
+ height: 150px; /* Fixed height */
440
+ display: flex; /* Use flexbox for layout */
441
+ flex-direction: column; /* Stack elements vertically */
442
  }
443
  .prompt-card:hover {
444
  transform: translateY(-4px);
 
449
  margin-bottom: 8px;
450
  font-size: 13px;
451
  color: #444;
452
+ flex-shrink: 0; /* Prevent name from shrinking */
453
+ }
454
+ .card-prompt-wrapper {
455
+ flex-grow: 1; /* Allow prompt wrapper to take remaining space */
456
+ overflow: hidden; /* Hide overflow */
457
+ background-color: #f8f9fa;
458
+ padding: 8px;
459
+ border-radius: 6px;
460
+ height: 100%; /* Ensure it tries to fill parent */
461
  }
462
  .card-prompt {
463
  font-size: 11px;
464
  line-height: 1.4;
465
  color: #666;
466
  display: -webkit-box;
467
+ -webkit-line-clamp: 5; /* Adjust line clamp based on fixed height */
468
  -webkit-box-orient: vertical;
469
  overflow: hidden;
470
+ /* height: 84px; Removed fixed height here */
471
+ max-height: 100%; /* Allow it to fill the wrapper */
 
 
472
  }
473
  </style>
474
  <div class="prompt-grid">
475
  """
476
+ # Use html.escape for security
477
  for item in items:
478
+ escaped_prompt = html.escape(item.get('prompt', ''))
479
+ escaped_name = html.escape(item.get('name', ''))
480
  card_html = f"""
481
+ <div class="prompt-card" onclick="copyToInput(this)" data-prompt="{escaped_prompt}">
482
+ <div class="card-name">{escaped_name}</div>
483
+ <div class="card-prompt-wrapper">
484
+ <div class="card-prompt">{escaped_prompt}</div>
485
+ </div>
486
  </div>
487
  """
488
  html_content += card_html
489
  html_content += r"""
490
+ </div>
491
  <script>
492
  function copyToInput(card) {
493
  const prompt = card.dataset.prompt;
494
+ // More robust selector targeting the specific textarea within the input panel
495
+ const textarea = document.querySelector('.input-panel .ant-input-textarea-large textarea');
496
  if (textarea) {
497
  textarea.value = prompt;
498
+ // Ensure the input event triggers potential state updates in Gradio/React
499
  textarea.dispatchEvent(new Event('input', { bubbles: true }));
500
+ textarea.dispatchEvent(new Event('change', { bubbles: true })); // Add change event too
501
+
502
+ // Close the drawer - find the close button more reliably
503
+ const drawer = card.closest('.ant-drawer'); // Find the parent drawer
504
+ if (drawer) {
505
+ // Find the close button *within* that specific drawer's header
506
+ const closeButton = drawer.querySelector('.ant-drawer-header .ant-drawer-close');
507
+ if (closeButton) {
508
+ closeButton.click();
509
+ } else {
510
+ console.warn('Could not find the close button for the template drawer.');
511
+ // Fallback for the original selector if needed, though less reliable
512
+ const fallbackCloseBtn = document.querySelector('.session-drawer .close-btn');
513
+ if(fallbackCloseBtn) fallbackCloseBtn.click();
514
+ }
515
+ } else {
516
+ console.warn('Could not find the parent drawer for the template card.');
517
+ }
518
+ } else {
519
+ console.error('Could not find the target textarea.');
520
+ alert('입력 영역을 찾을 수 없습니다.'); // User feedback
521
  }
522
  }
523
  </script>
 
524
  """
525
+ # Use gr.HTML to render the content safely
526
  return gr.HTML(value=html_content)
527
 
528
+
529
  def load_all_templates():
530
  return create_template_html("🎮 모든 게임 템플릿", load_json_data())
531
 
 
534
  # 5) 배포/부스트/기타 유틸
535
  # ------------------------
536
 
537
+ def remove_code_block(text: Optional[str]) -> str:
538
+ """Extracts code from Markdown code blocks (html, js, css, or generic)."""
539
+ if not text:
540
+ return ""
 
 
 
 
 
 
541
 
542
+ # Pattern for ```html ... ```
543
+ pattern_html = r'```html\s*([\s\S]+?)\s*```'
544
+ match_html = re.search(pattern_html, text, re.DOTALL | re.IGNORECASE)
545
+ if match_html:
546
+ logger.debug("Extracted code using ```html block.")
547
+ return match_html.group(1).strip()
548
+
549
+ # Pattern for ```javascript ... ``` or ```js ... ```
550
+ pattern_js = r'```(?:javascript|js)\s*([\s\S]+?)\s*```'
551
+ match_js = re.search(pattern_js, text, re.DOTALL | re.IGNORECASE)
552
+ if match_js:
553
+ logger.debug("Extracted code using ```javascript/js block.")
554
+ # If it's just JS, wrap it in basic HTML for execution
555
+ js_code = match_js.group(1).strip()
556
+ return f"""<!DOCTYPE html>
557
+ <html>
558
+ <head><meta charset="UTF-8"><title>JS Code</title></head>
559
+ <body><script>{js_code}</script></body>
560
+ </html>"""
561
+
562
+ # Pattern for ```css ... ```
563
+ pattern_css = r'```css\s*([\s\S]+?)\s*```'
564
+ match_css = re.search(pattern_css, text, re.DOTALL | re.IGNORECASE)
565
+ if match_css:
566
+ logger.debug("Extracted code using ```css block.")
567
+ # If it's just CSS, wrap it in basic HTML
568
+ css_code = match_css.group(1).strip()
569
+ return f"""<!DOCTYPE html>
570
+ <html>
571
+ <head><meta charset="UTF-8"><title>CSS Code</title><style>{css_code}</style></head>
572
+ <body><p>CSS Only Preview</p></body>
573
+ </html>"""
574
+
575
+ # Generic pattern for ``` ... ``` (if specific language blocks fail)
576
+ pattern_generic = r'```(?:\w+)?\s*([\s\S]+?)\s*```'
577
+ match_generic = re.search(pattern_generic, text, re.DOTALL)
578
+ if match_generic:
579
+ logger.debug("Extracted code using generic ``` block.")
580
+ # Assume it's HTML if it looks like it, otherwise wrap JS/CSS or return as is?
581
+ # Let's assume HTML for now if it contains tags
582
+ potential_code = match_generic.group(1).strip()
583
+ if '<' in potential_code and '>' in potential_code:
584
+ return potential_code
585
+ else:
586
+ # Could be JS, CSS, or something else. Defaulting to HTML wrap for safety.
587
+ return f"""<!DOCTYPE html>
588
+ <html>
589
+ <head><meta charset="UTF-8"><title>Code Preview</title></head>
590
+ <body><pre><code>{html.escape(potential_code)}</code></pre></body>
591
+ </html>"""
592
+
593
+
594
+ # If no code blocks found, check if the text itself looks like HTML
595
+ text_stripped = text.strip()
596
+ if text_stripped.startswith('<!DOCTYPE') or text_stripped.startswith('<html') or (text_stripped.startswith('<') and text_stripped.endswith('>')):
597
+ logger.debug("Assuming the entire input is HTML code (no code blocks found).")
598
+ return text_stripped
599
+
600
+ logger.debug("No code blocks found, and input doesn't look like HTML. Returning original text.")
601
+ # Return empty string if no code is likely present, or the original text?
602
+ # Returning empty might be safer if the expectation is code extraction.
603
+ return "" # Or return text if raw text might be intended
604
+
605
 
606
  def optimize_code(code: str) -> str:
607
+ # (Optimization logic remains the same, ensure it handles empty input)
608
  if not code or len(code.strip()) == 0:
609
+ return ""
610
+
611
+ logger.debug(f"Optimizing code (initial length: {len(code)})")
612
  lines = code.split('\n')
613
+ # No need to check length here, optimize regardless
614
+
615
+ # Remove comments more carefully
616
+ def remove_comments(line):
617
+ line = re.sub(r'//.*$', '', line) # Remove // comments
618
+ line = re.sub(r'/\*.*?\*/', '', line) # Remove /* */ comments on same line
619
+ return line
620
+
621
+ # Remove multi-line /* */ comments
622
+ code = re.sub(r'/\*[\s\S]*?\*/', '', code, flags=re.MULTILINE)
623
+ # Remove <!-- --> comments
624
+ code = re.sub(r'<!--[\s\S]*?-->', '', code, flags=re.MULTILINE)
625
+
626
  cleaned_lines = []
627
  empty_line_count = 0
628
+ for line in code.split('\n'):
629
+ line = remove_comments(line).strip() # Remove single-line comments and strip whitespace
630
+ if line == '':
631
  empty_line_count += 1
632
+ if empty_line_count <= 1: # Allow at most one consecutive empty line
633
  cleaned_lines.append('')
634
  else:
635
  empty_line_count = 0
636
+ # Remove console.log statements
637
+ line = re.sub(r'console\.log\(.*?\);?', '', line)
638
+ # Reduce multiple spaces to single space (be careful not to break strings)
639
+ # This is risky, might be better to skip unless absolutely necessary
640
+ # line = re.sub(r' {2,}', ' ', line)
641
+ if line: # Add line only if it's not empty after cleaning
642
+ cleaned_lines.append(line)
643
+
644
+ cleaned_code = '\n'.join(cleaned_lines).strip()
645
+ logger.debug(f"Optimized code length: {len(cleaned_code)}")
646
  return cleaned_code
647
 
648
+
649
  def send_to_sandbox(code):
650
+ """Encodes HTML code for display in an iframe sandbox."""
651
+ logger.debug(f"Preparing code for sandbox (length: {len(code)})")
652
+ # No need to call remove_code_block here, assume input is already cleaned code
653
+ # clean_code = remove_code_block(code) # Remove this line
654
+ clean_code = optimize_code(code) # Optimize the already extracted code
655
+
656
+ if not clean_code.strip():
657
+ logger.warning("Cannot send empty code to sandbox.")
658
+ # Return an empty iframe or an error message?
659
+ return "<p style='color: red;'>No code to display in sandbox.</p>"
660
+
661
+ # Basic HTML structure check and wrapping
662
+ if not re.search(r'<!DOCTYPE html>', clean_code, re.IGNORECASE) and not re.search(r'<html.*?>', clean_code, re.IGNORECASE):
663
+ logger.debug("Wrapping code in basic HTML structure for sandbox.")
664
+ # Check if body tags exist, if not, wrap the whole thing
665
+ if not re.search(r'<body.*?>', clean_code, re.IGNORECASE):
666
+ clean_code = f"""<!DOCTYPE html>
667
+ <html lang="en">
668
  <head>
669
  <meta charset="UTF-8">
670
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
671
  <title>Game Preview</title>
672
+ <style>body {{ margin: 0; overflow: hidden; }}</style> {/* Basic styling */}
673
  </head>
674
  <body>
675
  {clean_code}
676
  </body>
677
  </html>"""
678
+ else:
679
+ # If body exists, just ensure doctype and html/head are present
680
+ clean_code = f"""<!DOCTYPE html>
681
+ <html lang="en">
682
+ <head>
683
+ <meta charset="UTF-8">
684
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
685
+ <title>Game Preview</title>
686
+ <style>body {{ margin: 0; overflow: hidden; }}</style>
687
+ </head>
688
+ {clean_code}
689
+ </html>"""
690
+
691
+
692
+ try:
693
+ # Encode the final HTML
694
+ encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
695
+ data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
696
+ # Define sandbox attributes for security
697
+ sandbox_attributes = "allow-scripts allow-same-origin allow-forms" # Allow necessary permissions
698
+ iframe_html = f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;" sandbox="{sandbox_attributes}"></iframe>'
699
+ logger.debug("Sandbox iframe HTML generated.")
700
+ return iframe_html
701
+ except Exception as e:
702
+ logger.error(f"Error encoding HTML for sandbox: {e}", exc_info=True)
703
+ return f"<p style='color: red;'>Error creating sandbox preview: {html.escape(str(e))}</p>"
704
+
705
 
706
  def boost_prompt(prompt: str) -> str:
707
+ # (Boost prompt logic remains the same, ensure API clients are checked)
708
  if not prompt:
709
  return ""
710
+
711
  boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다.
712
  주어진 프롬프트를 분석하여 더 명확하고 간결한 요구사항으로 변환하되,
713
  원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
714
 
715
+ 1. 게임플레이 핵심 메커니즘 명확히 정의 (1-2 문장)
716
+ 2. 필수적인 상호작용 요소만 포함 (예: 키보드 조작, 클릭)
717
+ 3. 핵심 UI 요소 간략히 기술 (예: 점수 표시, 게임 영역)
718
+ 4. 코드 간결성 유지를 위한 우선순위 설정 (가장 중요한 기능 1-2가지 명시)
719
+ 5. 기본적인 게임 규칙과 승리/패배 조건 명시 (간단하게)
720
 
721
  다음 중요 지침을 반드시 준수하세요:
722
+ - 불필요한 세부 사항이나 부가 기능은 제외 (예: 사운드, 애니메이션, 레벨 디자인 복잡화)
723
  - 생성될 코드가 600줄을 넘지 않도록 기능을 제한
724
+ - 명확하고 간결한 언어로 요구사항 작성 (총 5-7 문장 이내)
725
  - 최소한의 필수 게임 요소만 포함
726
+ - 최종 결과는 증강된 프롬프트 텍스트만 반환 (추가 설명 없이)
727
  """
728
+ logger.debug(f"Boosting prompt: {prompt[:100]}...")
729
+ boosted_prompt = prompt # Default to original if APIs fail
730
+
731
  try:
732
+ if claude_client:
733
+ logger.debug("Attempting boost with Claude.")
734
  response = claude_client.messages.create(
735
+ model="claude-3-haiku-20240307", # Use Haiku for faster/cheaper boosting
736
+ max_tokens=500, # Reduced max_tokens for concise output
737
+ temperature=0.2, # Lower temperature for more focused output
738
  messages=[{
739
  "role": "user",
740
+ "content": f"다음 게임 프롬프트를 분석하고 위의 지침에 따라 간결하게 증강하세요:\n\n{prompt}"
741
  }],
742
  system=boost_system_prompt
743
  )
744
+ if response.content and len(response.content) > 0 and hasattr(response.content[0], 'text'):
745
+ boosted_prompt = response.content[0].text.strip()
746
+ logger.debug(f"Claude boosted prompt: {boosted_prompt[:100]}...")
747
+ return boosted_prompt
748
+ else:
749
+ logger.warning("Claude boost response format unexpected.")
750
+ raise anthropic.APIError("Invalid response format from Claude.") # Trigger fallback
751
+
752
+ elif openai_client:
753
+ logger.debug("Claude failed or unavailable, attempting boost with OpenAI.")
754
  completion = openai_client.chat.completions.create(
755
+ model="gpt-3.5-turbo", # Use a cheaper/faster model for boosting
756
  messages=[
757
  {"role": "system", "content": boost_system_prompt},
758
+ {"role": "user", "content": f"다음 게임 프롬프트를 분석하고 위의 지침에 따라 간결하게 증강하세요:\n\n{prompt}"}
759
  ],
760
+ max_tokens=500, # Reduced
761
+ temperature=0.2 # Lowered
762
  )
763
+ if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
764
+ boosted_prompt = completion.choices[0].message.content.strip()
765
+ logger.debug(f"OpenAI boosted prompt: {boosted_prompt[:100]}...")
766
+ return boosted_prompt
767
+ else:
768
+ logger.warning("OpenAI boost response format unexpected.")
769
+ # Fall through to return original prompt
770
+
771
+ else:
772
+ logger.warning("Neither Claude nor OpenAI client available for boosting.")
773
+ # Fall through to return original prompt
774
+
775
+ except Exception as e:
776
+ logger.error(f"Error during prompt boosting: {e}", exc_info=True)
777
+ # Fall through to return original prompt
778
+
779
+ logger.debug("Boosting failed or skipped, returning original prompt.")
780
+ return boosted_prompt # Return original if all attempts fail
781
+
782
 
783
  def handle_boost(prompt: str):
784
+ """Handles the boost button click event."""
785
+ logger.info("Boost button clicked.")
786
+ if not prompt or not prompt.strip():
787
+ logger.warning("Boost requested for empty prompt.")
788
+ # Return original empty prompt and don't change tab state
789
+ return "", gr.update() # Use gr.update() for no change
790
+
791
  try:
792
+ boosted = boost_prompt(prompt)
793
+ # Return the boosted prompt to the input textarea
794
+ # Keep the active tab as is (don't switch to empty)
795
+ return boosted, gr.update() # Use gr.update() for no change to the tab state
796
+ except Exception as e:
797
+ logger.error(f"Error in handle_boost: {e}", exc_info=True)
798
+ # Return the original prompt in case of error, maybe show a notification?
799
+ # For now, just return original prompt
800
+ return prompt, gr.update()
801
+
802
 
803
  def history_render(history: History):
804
+ """Prepares history for display in the chatbot component."""
805
+ logger.debug(f"Rendering history. Number of turns: {len(history)}")
806
+ # The history format should already be correct for the legacy.Chatbot
807
+ # Just need to trigger the drawer opening
808
  return gr.update(open=True), history
809
 
810
+
811
  def execute_code(query: str):
812
+ """Handles the 'Code' (Execute) button click."""
813
+ logger.info("Execute code button clicked.")
814
+ if not query or not query.strip():
815
+ logger.warning("Execute code requested for empty input.")
816
+ return None, gr.update(active_key="empty") # Stay on empty tab
817
+
818
  try:
819
+ # Extract code first
820
  clean_code = remove_code_block(query)
821
+ logger.debug(f"Code extracted for execution (length: {len(clean_code)})")
822
+
823
+ if not clean_code or not clean_code.strip():
824
+ logger.warning("No valid code found in input to execute.")
825
+ # Maybe return an error message in the sandbox?
826
+ error_html = "<p style='color:orange;'>입력에서 실행할 코드를 찾을 없습니다. 코드 블록(```html ... ```) 안에 코드를 넣어보세요.</p>"
827
+ return error_html, gr.update(active_key="render") # Show error in render tab
828
+
829
+ # Send the extracted code to the sandbox
830
+ sandbox_html = send_to_sandbox(clean_code)
831
+ logger.debug("Switching tab to 'render' for code execution.")
832
+ return sandbox_html, gr.update(active_key="render")
833
+
 
 
 
 
 
834
  except Exception as e:
835
+ logger.error(f"Error during execute_code: {e}", exc_info=True)
836
+ error_html = f"<p style='color: red;'>코드 실행 중 오류 발생: {html.escape(str(e))}</p>"
837
+ # Show error message in the render tab
838
+ return error_html, gr.update(active_key="render")
839
 
840
 
841
  # ------------------------
 
844
 
845
  class Demo:
846
  def __init__(self):
847
+ pass # No state needed here for now
848
+
849
  async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
850
+ """Generates game code based on user query and history."""
851
+ logger.info(f"Generation request received. Query: '{query[:50]}...', History length: {len(_history) if _history else 0}")
852
+
853
+ # Handle empty query - use random demo
854
+ if not query or not query.strip():
855
+ logger.info("Empty query received, using random demo description.")
856
  query = random.choice(DEMO_LIST)['description']
857
+ # Update the input field visually? Might be complex. For now, just use it internally.
858
+
859
+ # Initialize history if None
860
  if _history is None:
861
  _history = []
862
+ logger.debug("History was None, initialized to empty list.")
863
+
864
+ # Prepare the prompt with constraints
865
+ # Reduced line count constraint slightly for flexibility
866
+ constrained_query = f"""
867
+ 다음 게임을 제작해주세요.
868
+ 중요 요구사항:
869
+ 1. 코드는 HTML, CSS, JavaScript 만을 사용하여 단일 HTML 파일로 작성해주세요.
870
+ 2. 모든 코드는 `<script>` `<style>` 태그를 포함하여 HTML 파일 내부에 인라인으로 작성해야 합니다. 외부 파일 참조는 금지합니다.
871
+ 3. 코드는 가능한 한 간결하게 작성하고, 불필요한 주석, 설명, 공백 라인은 최소화해주세요.
872
+ 4. 생성되는 총 코드 라인 수는 **600줄**을 넘지 않도록 엄격히 제한합니다. (HTML, CSS, JS 포함)
873
+ 5. 게임의 핵심 기능만 구현하고, 복잡한 애니메이션, 사운드, 여러 레벨 등 부가 기능은 생략해주세요.
874
+ 6. 라이브러리나 프레임워크 사용 없이 바닐라 JavaScript (ES6+)를 사용해주세요.
875
+ 7. 즉시 실행 가능한 완전한 HTML 코드만 생성하고, 코드 앞뒤에 설명이나 ```html 같은 마크다운 표시는 절대 포함하지 마세요.
876
+
877
+ 게임 요청:
878
+ {query}
879
  """
880
+ logger.debug(f"Prepared constrained query for LLM.")
881
+
882
+ # Prepare messages for APIs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
883
  try:
884
+ messages = history_to_messages(_history, _setting.get('system', SystemPrompt)) # Use default if missing
885
+ system_message = messages[0]['content']
886
+
887
+ # Claude messages (User/Assistant roles only)
888
+ claude_messages = []
889
+ for msg in messages[1:]: # Skip system message
890
+ # Ensure content exists before adding
891
+ if msg.get("content", "").strip():
892
+ claude_messages.append({"role": msg["role"], "content": msg["content"]})
893
+ # Add the current user query
894
+ claude_messages.append({'role': Role.USER, 'content': constrained_query})
895
+
896
+ # OpenAI messages (System + User/Assistant)
897
+ openai_messages = [{"role": "system", "content": system_message}]
898
+ for msg in messages[1:]: # Skip system message
899
+ if msg.get("content", "").strip():
900
+ openai_messages.append({"role": msg["role"], "content": msg["content"]})
901
+ openai_messages.append({"role": "user", "content": constrained_query})
902
+
903
+ logger.debug(f"Prepared messages. Claude: {len(claude_messages)}, OpenAI: {len(openai_messages)}")
904
+
905
+ except Exception as e:
906
+ logger.error(f"Error preparing messages: {e}", exc_info=True)
907
  yield [
908
+ f"Error preparing messages: {html.escape(str(e))}",
909
  _history,
910
  None,
911
+ gr.update(active_key="empty"),
912
+ gr.update(open=False) # Keep code drawer closed on error
913
  ]
914
+ return # Stop generation
915
+
916
+ # Initial UI update: Show loading state
917
+ yield [
918
+ "⏳ 게임 코드 생성 시작...", # Initial message in code view
919
+ _history,
920
+ None, # No sandbox content yet
921
+ gr.update(active_key="loading"), # Switch to loading tab
922
+ gr.update(open=True) # Open code drawer to show progress
923
+ ]
924
+ await asyncio.sleep(0.1) # Allow UI to update
925
+
926
+ collected_content = None
927
+ error_message = None
928
+ api_used = None
929
+
930
+ # Try Claude first
931
+ if claude_client:
932
  try:
933
+ logger.info("Attempting code generation with Claude...")
934
+ api_used = "Claude"
935
+ # Use async for loop correctly
936
+ async for content_chunk in try_claude_api(system_message, claude_messages):
937
+ collected_content = content_chunk # Update collected content with each chunk
938
  yield [
939
+ f"```html\n{collected_content}\n```", # Show streaming progress in code block
940
  _history,
941
  None,
942
  gr.update(active_key="loading"),
943
  gr.update(open=True)
944
  ]
945
+ # No sleep needed, handled by async iterator
946
+ logger.info("Claude generation successful.")
947
+
948
+ except Exception as e:
949
+ logger.warning(f"Claude API failed: {e}", exc_info=False) # Log less verbosely for expected fallbacks
950
+ error_message = f"Claude API Error: {e}"
951
+ collected_content = None # Reset content on error
952
+
953
+ # Fallback to OpenAI if Claude failed or is unavailable
954
+ if collected_content is None and openai_client:
955
+ logger.info("Falling back to OpenAI for code generation...")
956
+ api_used = "OpenAI"
957
+ error_message = None # Clear previous error
958
+ try:
959
+ # Use async for loop correctly
960
+ async for content_chunk in try_openai_api(openai_messages):
961
+ collected_content = content_chunk # Update collected content
962
  yield [
963
+ f"```html\n{collected_content}\n```", # Show streaming progress
964
  _history,
965
  None,
966
  gr.update(active_key="loading"),
967
  gr.update(open=True)
968
  ]
969
+ # No sleep needed
970
+ logger.info("OpenAI generation successful.")
971
+
972
+ except Exception as e:
973
+ logger.error(f"OpenAI API also failed: {e}", exc_info=True)
974
+ error_message = f"OpenAI API Error: {e}. Both APIs failed."
975
+ collected_content = None
976
+
977
+ # Process the final result
978
+ if collected_content:
979
+ logger.info(f"Code generation completed using {api_used}. Final content length: {len(collected_content)}")
980
+ # Clean the final generated code (remove potential markdown fences if LLM added them)
981
+ final_code = remove_code_block(collected_content)
982
+ if not final_code: # If remove_code_block failed or LLM output was just markdown
983
+ final_code = collected_content # Use raw content as fallback
984
+
985
+ # Validate code length (using the raw code before optimization for check)
986
+ code_lines = final_code.count('\n') + 1
987
+ # Increased limit slightly based on system prompt
988
+ line_limit = 700
989
+ if code_lines > line_limit:
990
+ logger.warning(f"Generated code is too long: {code_lines} lines (limit: {line_limit}).")
991
+ warning_msg = f"""
992
+ ⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄 / {line_limit}줄 제한)**
993
+ 코드가 너무 길어 브라우저에서 느리거나 오류가 발생할 수 있습니다.
994
+ 더 간결한 게임을 요청하거나, "코드" 버튼으로 직접 실행해보세요.
995
+
996
  ```html
997
+ {html.escape(final_code[:2000])}
998
+ ... (코드가 너무 길어 일부만 표시) ...
999
+ ```"""
1000
+ # Update history with the failure/warning? Or just show warning?
1001
+ # Let's just show the warning in the code output for now.
1002
+ yield [
1003
+ warning_msg,
1004
+ _history, # History remains unchanged on length warning
1005
+ None, # No sandbox for overly long code
1006
+ gr.update(active_key="empty"), # Go back to empty state
1007
+ gr.update(open=True) # Keep code drawer open to show warning
1008
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
1009
  else:
1010
+ # Success: Update history and show code + sandbox
1011
+ logger.info("Code length within limits. Preparing final output.")
1012
+ # Prepare messages for history update (use the *original* query, not constrained one)
1013
+ messages_for_history = messages + [{'role': Role.USER, 'content': query}, {'role': Role.ASSISTANT, 'content': final_code}]
1014
+ _updated_history = messages_to_history(messages_for_history)
1015
+
1016
+ # Send final, cleaned code to sandbox
1017
+ sandbox_html = send_to_sandbox(final_code)
1018
+
1019
+ yield [
1020
+ f"```html\n{final_code}\n```", # Show final code in code block
1021
+ _updated_history,
1022
+ sandbox_html, # Render in sandbox
1023
+ gr.update(active_key="render"), # Switch to render tab
1024
+ gr.update(open=True) # Keep code drawer open
1025
+ ]
1026
+ else:
1027
+ # Both APIs failed
1028
+ logger.error(f"Code generation failed using both APIs. Last error: {error_message}")
1029
+ yield [
1030
+ f"❌ 코드 생성 실패: {html.escape(error_message)}",
1031
+ _history, # History remains unchanged on failure
1032
+ None,
1033
+ gr.update(active_key="empty"),
1034
+ gr.update(open=False) # Close drawer on total failure
1035
+ ]
1036
+
1037
  def clear_history(self):
1038
+ """Clears the conversation history."""
1039
+ logger.info("Clear history called.")
1040
+ # Also clear the code output and sandbox, and reset tabs
1041
+ return (
1042
+ [], # Cleared history state
1043
+ "", # Clear code output
1044
+ None, # Clear sandbox
1045
+ gr.update(active_key="empty"), # Reset tab to empty
1046
+ gr.update(open=False), # Close code drawer
1047
+ gr.update(value=""), # Clear input text
1048
+ gr.update(value="배포 결과가 여기에 표시됩니다.") # Reset deploy result
1049
+ )
1050
 
1051
 
1052
  ####################################################
1053
+ # Vercel Deployment Functions
1054
  ####################################################
1055
  def deploy_to_vercel(code: str):
1056
+ """Deploys the given HTML code to Vercel."""
1057
+ logger.info(f"[deploy_to_vercel] Attempting deployment. Code length: {len(code)}")
1058
+ vercel_token = os.getenv("VERCEL_API_TOKEN", "A8IFZmgW2cqA4yUNlLPnci0N") # Use env var first
1059
+
1060
+ if not vercel_token:
1061
+ logger.error("[deploy_to_vercel] Vercel API token (VERCEL_API_TOKEN or hardcoded) not found.")
1062
+ return "⚠️ **배포 실패:** Vercel API 토큰이 설정되지 않았습니다."
1063
+
1064
+ if not code or len(code.strip()) < 50: # Increased minimum length
1065
+ logger.warning("[deploy_to_vercel] Code is too short or empty for deployment.")
1066
+ return "⚠️ **배포 실패:** 배포할 코드가 너무 짧습니다 (최소 50자 필요)."
1067
+
1068
+ # Ensure code is wrapped in basic HTML if it isn't already
1069
+ if not re.search(r'<!DOCTYPE html>', code, re.IGNORECASE) and not re.search(r'<html.*?>', code, re.IGNORECASE):
1070
+ logger.debug("[deploy_to_vercel] Wrapping raw code in basic HTML structure for deployment.")
1071
+ code = f"""<!DOCTYPE html>
1072
+ <html lang="en">
1073
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Deployed Game</title></head>
1074
+ <body>{code}</body>
1075
+ </html>"""
1076
 
1077
+ project_name = 'gamecraft-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
1078
+ logger.info(f"[deploy_to_vercel] Generated project name: {project_name}")
 
 
 
 
 
 
 
 
 
1079
 
1080
+ deploy_url = "https://api.vercel.com/v13/deployments"
1081
+ headers = {
1082
+ "Authorization": f"Bearer {vercel_token}",
1083
+ "Content-Type": "application/json"
1084
+ }
1085
+
1086
+ # Define files for deployment (only index.html needed for simple static deployment)
1087
+ files = [
1088
+ {"file": "index.html", "data": code}
1089
+ ]
1090
 
1091
+ # Define deployment data (simpler structure for static deployment)
1092
+ deploy_data = {
1093
+ "name": project_name,
1094
+ "files": files,
1095
+ "target": "production",
1096
+ # Specify project settings to ensure it's treated as static
1097
+ "projectSettings": {
1098
+ "framework": None, # Explicitly set to null for static
1099
+ "outputDirectory": None # Not needed if deploying files directly
1100
  }
1101
+ }
1102
 
1103
+ try:
1104
+ logger.debug("[deploy_to_vercel] Sending deployment request to Vercel API...")
1105
+ deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data, timeout=90) # Increased timeout
1106
+ logger.debug(f"[deploy_to_vercel] Vercel API response status: {deploy_response.status_code}")
1107
 
1108
+ response_data = {}
1109
+ try:
1110
+ response_data = deploy_response.json()
1111
+ except json.JSONDecodeError:
1112
+ logger.warning("[deploy_to_vercel] Vercel response was not valid JSON.")
1113
+
1114
+ if deploy_response.status_code not in [200, 201]: # Vercel might return 201 Created
1115
+ error_message = response_data.get('error', {}).get('message', deploy_response.text)
1116
+ logger.error(f"[deploy_to_vercel] Deployment failed: {deploy_response.status_code} - {error_message}")
1117
+ # Limit error message length shown to user
1118
+ return f"⚠️ **배포 실패:** Vercel API 오류 ({deploy_response.status_code}).\n```\n{html.escape(error_message[:500])}\n```"
1119
+
1120
+ # Extract the deployment URL from the response
1121
+ deployment_url = response_data.get('url')
1122
+ if not deployment_url:
1123
+ logger.error("[deploy_to_vercel] Deployment succeeded but URL not found in response.")
1124
+ return "⚠️ **배포 실패:** 배포는 성공했지만 URL을 가져올 수 없습니다."
1125
+
1126
+ # Vercel usually adds https:// automatically, but ensure it's there
1127
+ if not deployment_url.startswith(('http://', 'https://')):
1128
+ deployment_url = f"https://{deployment_url}"
1129
+
1130
+ logger.info(f"[deploy_to_vercel] Deployment successful! URL: {deployment_url}")
1131
+
1132
+ # Return Markdown link
1133
+ result_markdown = f"""
1134
+ ✅ **배포 완료!**
1135
+ 게임이 다음 주소에 배포되었습니다:
1136
+ [**{deployment_url}**]({deployment_url})
1137
+
1138
+ *참고: 배포된 사이트가 활성화되기까지 몇 분 정도 소요될 수 있습니다.*
1139
+ """
1140
+ logger.debug("[deploy_to_vercel] Returning success Markdown.")
1141
+ return result_markdown.strip() # Strip leading/trailing whitespace
1142
+
1143
+ except requests.exceptions.Timeout:
1144
+ logger.error("[deploy_to_vercel] Request to Vercel API timed out.")
1145
+ return "⚠️ **배포 실패:** Vercel API 요청 시간 초과."
1146
+ except requests.exceptions.RequestException as e:
1147
+ logger.error(f"[deploy_to_vercel] Network or Request Error: {e}", exc_info=True)
1148
+ return f"⚠️ **배포 실패:** 네트워크 또는 요청 오류 발생.\n```\n{html.escape(str(e))}\n```"
1149
+ except Exception as e:
1150
+ logger.error(f"[deploy_to_vercel] Unexpected Error during deployment: {e}", exc_info=True)
1151
+ return f"⚠️ **배포 실패:** 예상치 못한 오류 발생.\n```\n{html.escape(str(e))}\n```"
1152
 
 
 
 
1153
 
1154
+ def handle_deploy_click(code_output_value: Optional[str]):
1155
+ """Handles the deploy button click event."""
1156
+ logger.info("Deploy button clicked.")
1157
 
1158
+ # Add an immediate feedback message
1159
+ yield "⏳ 배포를 시작합니다... Vercel API 호출 중..."
 
 
 
 
1160
 
1161
+ if not code_output_value:
1162
+ logger.warning("[handle_deploy_click] No code available in code_output.")
1163
+ yield "⚠️ **배포 실패:** 생성된 코드가 없습니다."
1164
+ return
1165
 
1166
+ # Extract the actual code from the Markdown component's value
1167
+ # The value might contain ```html ... ```, so remove it.
1168
+ clean_code = remove_code_block(code_output_value)
1169
+ logger.debug(f"[handle_deploy_click] Extracted code for deployment (length: {len(clean_code)}).")
1170
 
1171
+ if not clean_code or len(clean_code.strip()) < 50:
1172
+ logger.warning("[handle_deploy_click] Cleaned code is too short for deployment.")
1173
+ yield "⚠️ **배포 실패:** 배포할 유효한 코드가 부족합니다 (최소 50자 필요)."
1174
+ return
1175
 
1176
+ # Call the deployment function
1177
+ deployment_result = deploy_to_vercel(clean_code)
1178
+ logger.info(f"[handle_deploy_click] Deployment result received.")
1179
 
1180
+ # Yield the final result to update the Markdown component
1181
+ yield deployment_result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1182
 
1183
 
1184
  # ------------------------
 
1194
  text_size=gr.themes.sizes.text_md,
1195
  )
1196
 
1197
+ with gr.Blocks(css="app.css", theme=theme) as demo: # Use css parameter directly
1198
+ # Removed header_html and integrated title/description into gr.Blocks or Markdown
1199
+ gr.Markdown("""
1200
  <div class="app-header">
1201
  <h1>🎮 Vibe Game Craft</h1>
1202
+ <p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 실시간 미리보기와 원클릭 배포 기능을 지원합니다.</p>
1203
  </div>
1204
+ """, elem_classes="header-markdown") # Added class for potential styling
 
 
 
 
 
 
 
 
 
 
1205
 
1206
+ # State variables
1207
  history = gr.State([])
1208
  setting = gr.State({"system": SystemPrompt})
1209
+ # Removed deploy_status state as we display directly in Markdown
1210
 
1211
  with ms.Application() as app:
1212
+ with antd.ConfigProvider(): # Provides context for Ant Design components
1213
+
1214
+ # Drawers for Code and History
1215
+ with antd.Drawer(open=False, title="코드 보기", placement="left", width="50%", name="code_drawer") as code_drawer:
1216
+ # Use gr.Code for better syntax highlighting if legacy.Markdown causes issues
1217
+ code_output = gr.Code(language="html", label="Generated Code", interactive=False)
1218
+ # code_output = legacy.Markdown(elem_classes="code-output-markdown") # Keep if preferred
1219
+
1220
+ with antd.Drawer(open=False, title="히스토리", placement="left", width="60%", name="history_drawer") as history_drawer:
1221
+ history_output = legacy.Chatbot(
1222
+ show_label=False,
1223
+ flushing=False, # Keep flushing false for standard updates
1224
+ height=800, # Adjust height as needed
1225
+ elem_classes="history_chatbot",
1226
+ show_copy_button=True # Add copy button to chat messages
1227
+ )
1228
+
1229
+ # Drawer for Templates
1230
  with antd.Drawer(
1231
  open=False,
1232
+ title="🎮 게임 템플릿",
1233
  placement="right",
1234
+ width="60%", # Adjust width
1235
+ name="session_drawer", # Use name for easier selection if needed
1236
  elem_classes="session-drawer"
1237
  ) as session_drawer:
1238
  with antd.Flex(vertical=True, gap="middle"):
1239
+ # Use gr.HTML for the template display
1240
+ session_history_html = gr.HTML(elem_classes="session-history-html") # Renamed variable
1241
+ # Button inside the drawer to close it
1242
+ close_template_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
1243
 
1244
+ # Main Layout (Row with two Columns)
1245
+ with antd.Row(gutter=[16, 16], align="top", elem_classes="main-layout-row"): # Reduced gutter
1246
 
1247
+ # Left Column: Game Preview Area
1248
+ with antd.Col(xs=24, sm=24, md=14, lg=15, xl=16, elem_classes="preview-column"): # Responsive spans
1249
+ with ms.Div(elem_classes="preview-panel panel"): # Use ms.Div or standard gr.Column/Group
1250
  gr.HTML(r"""
1251
+ <div class="render_header">
1252
+ <span class="header_btn red"></span><span class="header_btn yellow"></span><span class="header_btn green"></span>
1253
+ <span class="render-title">Game Preview</span>
1254
+ </div>
1255
+ """, elem_classes="preview-header-html")
1256
+ # Tabs for different preview states
1257
+ with antd.Tabs(active_key="empty", render_tab_bar="() => null", name="state_tab") as state_tab:
1258
  with antd.Tabs.Item(key="empty"):
1259
+ antd.Empty(description="게임을 만들려면 설명을 입력하거나 템플릿을 선택하세요.", elem_classes="right_content")
1260
  with antd.Tabs.Item(key="loading"):
1261
+ antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content")
1262
  with antd.Tabs.Item(key="render"):
1263
+ # Use gr.HTML for the sandbox iframe
1264
  sandbox = gr.HTML(elem_classes="html_content")
1265
 
1266
+ # Right Column: Controls and Input
1267
+ with antd.Col(xs=24, sm=24, md=10, lg=9, xl=8, elem_classes="control-column"): # Responsive spans
1268
+ with antd.Flex(vertical=True, gap="middle", elem_classes="control-panel"): # Use Flex for vertical layout
1269
+
1270
+ # Top Buttons (Code, History, Templates)
1271
+ with antd.Flex(gap="small", justify="space-between", elem_classes="top-buttons"):
1272
+ codeBtn = antd.Button("🧑‍💻 코드", type="default", icon="<i class='fas fa-code'></i>") # Use icon font if available or text
1273
+ historyBtn = antd.Button("📜 히스토리", type="default", icon="<i class='fas fa-history'></i>")
1274
+ template_btn = antd.Button("🎮 템플릿", type="default", icon="<i class='fas fa-gamepad'></i>")
1275
+
1276
+ # Input Textarea
 
 
 
 
 
1277
  input_text = antd.InputTextarea(
1278
  size="large",
1279
  allow_clear=True,
1280
+ placeholder="예: 벽돌깨기 게임 만들어줘 (간단하게)", # Shorter placeholder
1281
+ rows=6, # Adjust rows for desired initial height
1282
+ max_length=2000, # Sensible max length for input
1283
+ show_count=True, # Show character count
1284
+ elem_classes="main-input-textarea"
1285
  )
1286
+ gr.HTML('<div class="help-text">💡 원하는 게임을 설명하거나 템플릿을 사용하세요. 간결할수록 좋습니다!</div>')
1287
+
1288
+ # Action Buttons (Send, Boost, Execute, Deploy, Clear)
1289
+ with antd.Flex(gap="small", wrap='wrap', justify="space-between", elem_classes="action-buttons"): # Allow wrapping
1290
+ btn = antd.Button("🚀 생성", type="primary", size="middle") # Changed text, adjusted size
1291
+ boost_btn = antd.Button("✨ 증강", type="default", size="middle")
1292
+ execute_btn = antd.Button("▶️ 코드 실행", type="default", size="middle") # Changed text
1293
+ deploy_btn = antd.Button("☁️ 배포", type="default", size="middle")
1294
+ clear_btn = antd.Button("🧹 클리어", type="default", size="middle", danger=True) # Added danger style
1295
+
1296
+ # Deployment Result Area
1297
+ gr.Markdown("---") # Separator
1298
+ gr.Markdown("### ☁️ 배포 결과", elem_classes="deploy-header")
1299
  deploy_result_container = gr.Markdown(
1300
+ value="아직 배포된 게임이 없습니다. '배포' 버튼을 클릭하세요.",
1301
+ label="Deployment Result", # Label might not be visible depending on theme/CSS
1302
+ elem_classes="deploy-result-markdown" # Add class for styling
1303
  )
1304
 
1305
+ # --- Event Listeners ---
1306
+
1307
+ # Drawer Controls
1308
  codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
1309
+ # code_drawer.change(lambda x: gr.update(open=x), inputs=[code_drawer], outputs=[code_drawer]) # Close via change event if needed
1310
 
 
1311
  historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
1312
+ # history_drawer.change(lambda x: gr.update(open=x), inputs=[history_drawer], outputs=[history_drawer])
1313
 
1314
+ # Template Drawer Controls
1315
  template_btn.click(
1316
  fn=lambda: (gr.update(open=True), load_all_templates()),
1317
+ outputs=[session_drawer, session_history_html], # Target the gr.HTML component
1318
+ queue=False # No queue needed for simple UI update
1319
  )
1320
+ # Close button inside the template drawer
1321
+ close_template_btn.click(lambda: gr.update(open=False), outputs=[session_drawer])
1322
+ # Also close when clicking outside (default drawer behavior) or via the 'X' icon
1323
 
1324
+ # Main Action: Generate Code
1325
  btn.click(
1326
  demo_instance.generation_code,
1327
  inputs=[input_text, setting, history],
1328
+ outputs=[code_output, history, sandbox, state_tab, code_drawer], # Ensure code_drawer is opened
1329
+ concurrency_limit=5 # Limit concurrent requests
1330
  )
1331
 
1332
+ # Clear Button
1333
+ clear_btn.click(
1334
+ demo_instance.clear_history,
1335
+ inputs=[],
1336
+ # Outputs: history state, code output, sandbox, state tab, code drawer, input text, deploy result
1337
+ outputs=[history, code_output, sandbox, state_tab, code_drawer, input_text, deploy_result_container]
1338
+ )
1339
 
1340
+ # Boost Button
1341
  boost_btn.click(
1342
  fn=handle_boost,
1343
  inputs=[input_text],
1344
+ outputs=[input_text, state_tab], # Update input text, don't change tab
1345
+ queue=False
1346
  )
1347
 
1348
+ # Execute Code Button
1349
  execute_btn.click(
1350
  fn=execute_code,
1351
+ inputs=[input_text], # Takes code directly from input for execution
1352
+ outputs=[sandbox, state_tab], # Updates sandbox and switches tab
1353
+ queue=False
1354
  )
1355
 
1356
+ # Deploy Button
1357
+ # Use the new handle_deploy_click function which is a generator
1358
  deploy_btn.click(
1359
+ fn=handle_deploy_click,
1360
+ inputs=[code_output], # Takes code from the code output drawer
1361
+ outputs=[deploy_result_container], # Updates the deployment result Markdown
1362
+ # queue=True # Enable queue as deployment can take time
1363
  )
1364
 
1365
  # ------------------------
1366
  # 9) 실행
1367
  # ------------------------
1368
  if __name__ == "__main__":
1369
+ logger.info("Starting Gradio application...")
1370
  try:
1371
+ # demo_instance is already created
1372
+ # Use share=True for public link if needed, debug=True for more logs
1373
+ demo.queue(default_concurrency_limit=10).launch(
1374
+ server_name="0.0.0.0", # Listen on all interfaces for docker/cloud
1375
+ # share=True, # Uncomment for public link
1376
+ debug=True, # Enable Gradio debug mode for more logs
1377
+ # prevent_thread_lock=True # May help in some environments
1378
+ )
1379
  except Exception as e:
1380
+ logger.critical(f"Failed to launch Gradio demo: {e}", exc_info=True)
1381
+ raise