Spaces:
Paused
Paused
Delete app.py
Browse files
app.py
DELETED
@@ -1,1393 +0,0 @@
|
|
1 |
-
import os
|
2 |
-
import re
|
3 |
-
import random
|
4 |
-
import time
|
5 |
-
import html
|
6 |
-
import base64
|
7 |
-
import string
|
8 |
-
import json
|
9 |
-
import asyncio
|
10 |
-
import requests
|
11 |
-
import anthropic
|
12 |
-
import openai
|
13 |
-
import io
|
14 |
-
import logging
|
15 |
-
|
16 |
-
from http import HTTPStatus
|
17 |
-
from typing import Dict, List, Optional, Tuple
|
18 |
-
from functools import partial
|
19 |
-
|
20 |
-
import gradio as gr
|
21 |
-
import modelscope_studio.components.base as ms
|
22 |
-
import modelscope_studio.components.legacy as legacy
|
23 |
-
import modelscope_studio.components.antd as antd
|
24 |
-
|
25 |
-
# === [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에 쌓인 로그를 문자열로 반환"""
|
49 |
-
return log_stream.getvalue()
|
50 |
-
|
51 |
-
|
52 |
-
# ------------------------
|
53 |
-
# 1) DEMO_LIST 및 SystemPrompt
|
54 |
-
# ------------------------
|
55 |
-
# (DEMO_LIST and SystemPrompt remain the same)
|
56 |
-
DEMO_LIST = [
|
57 |
-
{"description": "블록이 위에서 떨어지는 클래식 테트리스 게임을 개발해주세요. 화살표 키로 조작하며, 가로줄이 채워지면 해당 줄이 제거되고 점수가 올라가는 메커니즘이 필요합니다. 난이도는 시간이 지날수록 블록이 빨라지도록 구현하고, 게임오버 조건과 점수 표시 기능을 포함해주세요."},
|
58 |
-
{"description": "두 명이 번갈아가며 플레이할 수 있는 체스 게임을 만들어주세요. 기본적인 체스 규칙(킹, 퀸, 룩, 비숍, 나이트, 폰의 이동 규칙)을 구현하고, 체크와 체크메이트 감지 기능이 필요합니다. 드래그 앤 드롭으로 말을 움직일 수 있게 하며, 이동 기록도 표시해주세요."},
|
59 |
-
{"description": "짝을 맞추는 메모리 카드 게임을 개발해주세요. 카드를 뒤집으면 그림이 나타나고, 같은 그림의 카드 두 장을 찾으면 점수를 얻는 방식입니다. 카드 뒤집기 애니메이션과 함께 시도 횟수를 기록하는 점수 시스템, 그리고 쉬움/보통/어려움 난이도 선택 기능(카드 수 변경)도 구현해주세요."},
|
60 |
-
{"description": "플레이어가 우주선을 조종하여 적 우주선을 파괴하는 슈팅 게임을 만들어주세요. 키보드 방향키로 움직이고 스페이스바로 발사하며, 다양한 적 웨이브가 공격해오는 구조입니다. 충돌 감지 시스템과 함께 파워업 아이템(방패, 다중 발사, 속도 증가 등)을 구현하고, 난이도가 점진적으로 증가하는 시스템을 추가해주세요."},
|
61 |
-
{"description": "3x3 또는 4x4 크기의 슬라이드 퍼즐 게임을 만들어주세요. 숫자나 이미지 조각을 섞은 후, 빈 칸을 이용해 조각들을 올바른 위치로 밀어 맞추는 게임입니다. 섞기 기능과 이동 횟수 카운터, 완성 시 축하 메시지를 표시하고, 난이도 설정(크기 변경)도 구현해주세요."},
|
62 |
-
{"description": "고전적인 뱀 게임을 구현해주세요. 플레이어는 방향키로 뱀을 조종하여 필드에 랜덤하게 생성되는 먹이를 먹으며, 먹이를 먹을 때마다 뱀의 길이가 늘어납니다. 자신의 몸에 부딪히거나 벽에 부딪히면 게임이 종료되며, 점수는 먹은 먹이의 수에 비례합니다. 시간이 지날수록 뱀의 이동 속도가 빨라지는 난이도 조절 기능도 추가해주세요."},
|
63 |
-
{"description": "화면 상단에 여러 줄의 벽돌이 배치된 브레이크아웃 게임을 만들어주세요. 플레이어는 화면 하단의 패들을 좌우로 움직여 공을 튕겨내어 벽돌을 깨야 합니다. 벽돌을 모두 깨면 스테이지 클리어, 공이 바닥에 떨어지면 생명이 감소합니다. 공의 속도는 시간이 지날수록 증가하며, 특수 벽돌(추가 생명, 패들 확장 등)도 구현해주세요."},
|
64 |
-
{"description": "길을 따라 이동하는 적들을 방어하는 타워 디펜스 게임을 개발해주세요. 플레이어는 맵의 특정 위치에 다양한 타워(기본 공격, 범위 공격, 감속 효과 등)를 설치하여 적을 물리쳐야 합니다. 웨이브 시스템으로 난이도가 점진적으로 증가하며, 적을 처치하면 자원을 얻어 타워를 업그레이드하거나 새 타워를 건설할 수 있는 경제 시스템을 구현해주세요."},
|
65 |
-
{"description": "캐릭터가 끝없이 달리며 장애물을 뛰어넘는 엔드리스 러너 게임을 만들어주세요. 스페이스바나 마우스 클릭으로 점프하여 다가오는 장애물(바위, 구덩이, 적 등)을 피해야 합니다. 거리에 따라 점수가 증가하며, 코인 등의 수집품을 모으는 요소와 파워업(일시적 무적, 자석 효과 등)도 추가해주세요. 시간이 지날수록 게임 속도가 빨라지는 난이도 시스템도 구현해주세요."},
|
66 |
-
{"description": "2D 플랫포머 게임을 개발해주세요. 플레이어는 방향키로 캐릭터를 조종하여 발판 위를 이동하고, 스페이스바로 점프하며 코인이나 보석 같은 아이템을 수집합니다. 적 캐릭터(간단한 AI로 움직임)와 함정(가시, 떨어지는 발판 등)을 피해 목표 지점까지 도달하는 레벨 기반 구조로 만들어주세요. 체력 시스템과 체크포인트 기능도 구현해주세요."},
|
67 |
-
{"description": "매번 새로운 미로를 자동 생성하는 미로 게임을 만들어주세요. 플레이어는 시작점에서 출발하여 방향키로 캐릭터를 조종해 출구를 찾아야 합니다. 미로 생성 알고리즘(예: 깊이 우선 탐색, 프림 알고리즘 등)을 활용하여 다양한 크기와 복잡도의 미로를 만들고, 타이머로 시간을 측정하며, 선택적으로 최단 경로를 보여주는 힌트 기능도 구현해주세요."},
|
68 |
-
{"description": "간단한 턴제 RPG 게임을 개발해주세요. 플레이어는 탑다운 뷰에서 타일 기반으로 이동하며, 몬스터와 마주치면 턴제 전투가 시작됩니다. 기본 공격, 특수 스킬, 아이템 사용 등의 전투 옵션과 함께 레벨업 시스템(경험치, 능력치 상승)을 구현해주세요. 또한 전투에서 승리하면 골드와 아이템을 획득할 수 있으며, 상점에서 장비를 구매하는 기능도 추가해주세요."},
|
69 |
-
{"description": "같은 색상/모양의 아이템 3개 이상을 일렬로 맞추는 매치-3 퍼즐 게임을 만들어주세요. 아이템을 스와이프하여 위치를 바꾸고, 매치되면 아이템이 사라지며 점수를 얻는 방식입니다. 특수 매치(4개 이상, T자 모양 등)는 특수 아이템을 생성하며, 연속 매치(콤보)는 추가 점수나 보너스 효과를 제공합니다. 목표 점수 또는 제한 시간/이동 횟수 모드를 구현해주세요."},
|
70 |
-
{"description": "플래피 버드 스타일의 게임을 개발해주세요. 플레이어는 스페이스바나 마우스 클릭으로 새를 점프시켜 위아래로 움직이는 파이프 사이를 통과해야 합니다. 파이프에 부딪히거나 화면 상단/하단에 닿으면 게임 오버이며, 통과한 파이프 쌍마다 점수가 1점씩 증가합니다. 파이프 간격은 랜덤하게 생성되며, 최고 점수를 로컬 스토리지에 저장하는 기능도 구현해주세요."},
|
71 |
-
{"description": "두 개의 유사한 이미지에서 차이점을 찾는 게임을 만들어주세요. 5-10개의 차이점이 있는 이미지 쌍을 준비하고, 플레이어가 차이점을 클릭하면 표시되도록 합니다. 제한 시간 내에 모든 차이점을 찾아야 하며, 오답 클릭 시 시간 패널티가 부과됩니다. 힌트 시스템(차이점 하나를 자동으로 표시)과 난이도 선택(쉬움: 차이점이 명확, 어려움: 미묘한 차이)도 구현해주세요."},
|
72 |
-
{"description": "화면 상단에서 단어가 떨어지는 타이핑 게임을 개발해주세요. 플레이어는 키보드로 해당 단어를 정확히 입력하여 단어가 바닥에 닿기 전에 제거해야 합니다. 정확히 입력한 단어는 사라지고 점수를 얻으며, 난이도에 따라 단어의 길이와 떨어지는 속도가 조절됩니다. 특수 단어(빨간색 등)는 보너스 점수나 시간 추가 등의 효과를 제공하며, 일정 시간/점수마다 난이도가 상승하는 시스템도 구현해주세요."},
|
73 |
-
{"description": "물리 엔진 기반의 미니 골프 게임을 만들어주세요. 플레이어는 마우스 드래그로 공을 치는 방향과 세기를 조절하여 홀에 공을 넣어야 합니다. 다양한 장애물(모래 함정, 물웅덩이, 경사로 등)이 있는 여러 개의 코스를 구현하고, 각 홀마다 타수를 기록하여 최종 점수를 계산합니다. 바람 방향/세기 같은 환경 요소와 함께 궤적 미리보기 기능도 추가해주세요."},
|
74 |
-
{"description": "플레이어가 낚시를 즐기는 시뮬레이션 게임을 개발해주세요. 마우스 클릭으로 낚싯줄을 던지고, 물고기가 물면 타이밍 맞추기 미니게임으로 물고기를 낚아야 합니다. 다양한 종류의 물고기(희귀도별 점수 차등)를 구현하고, 낚은 물고기에 따라 골드를 획득하여 더 좋은 낚싯대, 미끼 등을 구매할 수 있는 업그레이드 시스템을 추가해주세요. 시간대나 날씨에 따라 출현하는 물고기가 달라지는 기���도 구현해주세요."},
|
75 |
-
{"description": "1인용 또는 AI 대전 빙고 게임을 만들어주세요. 5x5 그리드에 1-25 숫자를 무작위로 배치하고, 번갈아가며 숫자를 선택하여 해당 칸을 마킹합니다. 가로, 세로, 대각선으로 5개의 연속된 마킹이 완성되면 빙고가 되며, 먼저 3빙고를 달성하는 쪽이 승리합니다. 컴퓨터 AI는 랜덤하게 또는 전략적으로(빙고에 가까운 라인 우선) 숫자를 선택하도록 구현하고, 타이머와 승/패 기록 시스템도 추가해주세요."},
|
76 |
-
{"description": "화면 하단에서 상단으로 노트가 올라오면 정확한 타이밍에 키를 눌러 점수를 얻는 리듬 게임을 개발해주세요. 4개의 레인(D, F, J, K 키)에 노트가 등장하며, 타이밍 정확도에 따라 Perfect, Good, Miss 등급이 표시됩니다. 배경 음악에 맞춰 노트가 생성되며, 연속 성공 시 콤보 시스템으로 추가 점수를 제공합니다. 난이도 선택(노트 속도와 밀도 조절)과 함께 최종 결과 화면(정확도, 콤보, 등급)도 구현해주세요."},
|
77 |
-
{"description": "탑다운 뷰의 2D 레이싱 게임을 만들어주세요. 플레이어는 방향키로 자동차를 조종하여 트랙을 따라 주행하며, 트랙 이탈 시 감속되는 메커니즘을 구현합니다. 여러 AI 경쟁자들과 경쟁하며 3바퀴를 가장 빨리 완주하는 게임 모드와 함께, 시간 제한 내에 체크포인트를 통과하는 타임 어택 모드도 구현해주세요. 다양한 차량 선택지(속도와 핸들링 특성 차등)와 부스트 아이템, 장애물 등도 추가해주세요."},
|
78 |
-
{"description": "다양한 카테고리의 퀴즈를 풀어나가는 게임을 개발해주세요. 주어진 질문에 4개의 보기 중 정답을 선택하는 방식으로, 정답 시 점수를 획득하고 오답 시 생명이 감소합니다. 30초 제한 시간 내에 답을 선택해야 하며, 난이도에 따라 질문의 복잡도와 제한 시간이 조절됩니다. 50:50 힌트(오답 2개 제거), 시간 추가 등의 도움 아이템과 함께 최종 결과 요약(정답률, 카테고리별 성적)도 구현해주세요."},
|
79 |
-
{"description": "움직이는 표적을 맞추는 사격 갤러리 게임을 만들어주세요. 마우스 클릭으로 발사하며, 다양한 속도와 패턴으로 움직이는 표적(오리, 병, 풍선 등)을 맞추면 점수를 획득합니다. 제한된 시간과 총알 수 안에 최대한 많은 점수를 얻는 것이 목표이며, 특수 표적(황금 표적 등)은 보너스 점수나 추가 시간/총알을 제공합니다. 연속 명중 시 점수 배율이 증가하는 콤보 시스템과 함께 다양한 난이도 레벨(표적 속도/수 증가)도 구현해주세요."},
|
80 |
-
{"description": "가상 주사위를 굴려 보드판을 돌아다니는 보드 게임을 개발해주세요. 플레이어는 차례대로 1-6 주사위를 굴려 말을 이동시키며, 도착한 칸에 따라 다양한 이벤트(앞으로/뒤로 이동, 한 턴 쉬기, 미니게임 등)가 발생합니다. 특수 아이템(추가 주사위, 이벤트 회피 등)을 수집하고 사용할 수 있으며, 먼저 결승점에 도달하거나 가장 많은 포인트를 모은 플레이어가 승리합니다. 1-4명의 로컬 멀티플레이어를 지원하며, AI 플레이어도 구현해주세요."},
|
81 |
-
{"description": "탑다운 뷰의 좀비 서바이벌 게임을 만들어주세요. WASD로 이동하고 마우스로 조준/발사하며, 끊임없이 몰려오는 좀비 웨이브를 최대한 오래 생존하는 것이 목표입니다. 다양한 무기(권총, 샷건, 기관총 등)와 제한된 탄약, 그리고 체력 회복 아이템과 폭탄 같은 특수 아이템을 맵에서 획득할 수 있습니다. 시간이 지날수록 좀비의 수와 속도가 증가하며, 특수 좀비(탱커, 러너 등)도 등장하는 난이도 시스템을 구현해주세요."},
|
82 |
-
{"description": "축구 페널티킥 게임을 개발해주세요. 공격 시에는 방향과 파워를 조절하여 슛을 날리고, 수비 시에는 골키퍼를 좌/중앙/우 중 한 방향으로 다이빙시켜 공을 막아야 합니다. 5번의 키커-골키퍼 대결 후 더 많은 골을 넣은 쪽이 승리하며, 동점일 경우 서든데스로 승부를 가립니다. 슛의 정확도와 파워에 따라 결과가 달라지며, 골키퍼 AI는 패턴 학습을 통해 플레이어의 경향성을 파악하도록 구현해주세요. 1인 플레이와 2인 로컬 대전 모드를 모두 지원해주세요."},
|
83 |
-
{"description": "클래식한 지뢰찾기 게임을 구현해주세요. NxN 크기의 그리드에 M개의 지뢰가 무작위로 배치되며, 플레이어는 좌클릭으로 칸을 열고 우클릭으로 지뢰 위치에 깃발을 표시합니다. 열린 칸에는 주변 8칸의 지뢰 수가 표시되며, 주변에 지뢰가 없는 칸을 열면 연쇄적으로 주변 칸들이 열립니다. 지뢰가 있는 칸을 열면 게임 오버, 지뢰가 아닌 모든 칸을 열면 승리입니다. 난이도 설정(쉬움: 9x9/10개, 중간: 16x16/40개, 어려움: 30x16/99개)과 함께 첫 클릭은 항상 안전하도록 구현해주세요."},
|
84 |
-
{"description": "두 플레이어가 번갈아가며 7x6 그리드에 색깔 디스크를 떨어뜨려 가로, 세로, 대각선으로 4개의 연속된 디스크를 만드는 Connect Four 게임을 개발해주세요. 플레이어는 열을 클릭하여 디스크를 해당 열의 가장 아래 빈 칸에 배치합니다. 4개의 연속된 디스크를 먼저 만드는 플레이어가 승리하며, 모든 칸이 차면 무승부입니다. 1인 플레이(AI 대전)과 2인 로컬 대전 모드를 구현하고, AI는 최소한 1단계 앞을 내다보는 논리로 작동하도록 해주세요."},
|
85 |
-
{"description": "글자 타일을 배치하여 단어를 만드는 스크래블 스타일의 단어 게임을 만들어주세요. 각 플레이어는 7개의 글자 타일을 받고, 이를 보드에 배치하여 가로나 세로로 단어를 형성합니다. 새 단어는 기존 단어와 반드시 연결되어야 하며, 각 타일에는 점수가 있어 단어의 총점이 계산됩니다. 특수 칸(2배 글자 점수, 3배 단어 점수 등)을 활용한 전략적 배치가 가능하며, 사전 검증 기능으로 유효한 단어만 허용합니다. 1-4인 로컬 멀티플레이어와 AI 대전을 지원해주세요."},
|
86 |
-
{"description": "2D 환경에서 진행되는 탱크 전투 게임을 개발해주세요. 플레이어는 WASD로 탱크를 조종하고, 마우스로 포탑을 조준하여 클릭으로 발사합니다. 파괴 가능한 지형(벽돌, 나무 등)과 파괴 불가능한 장애물(강철, 물 등)이 있는 맵에서 적 탱크들과 전투를 벌입니다. 다양한 무기(기본 포탄, 확산탄, 레이저 등)와 아이템(속도 증가, 방어력 강화, 추가 생명 등)을 구현하고, 스테이지별로 증가하는 적 AI 난이도와 보스 전투도 추가해주세요."},
|
87 |
-
{"description": "3개 이상의 같은 보석을 맞추어 제거하는 퍼즐 게임을 만들어주세요. 인접한 두 보석을 스왑하여 매치를 만들며, 매치된 보석이 사라지면 위의 보석들이 떨어지고 새 보석이 채워집니다. 4개 이상 매치 시 특수 보석(가로/세로 폭발, 주변 9칸 폭발 등)이 생성되며, 연쇄 매치가 발생하면 콤보 점수가 추가됩니다. 제한 시간 또는 제한 이동 횟수 내에 목표 점수를 달성하는 레벨 기반 진행 구조와 함께, 특수 미션(특정 색상 N개 제거, 장애물 파괴 등)도 구현해주세요."},
|
88 |
-
{"description": "단일 타워가 끊임없이 몰려오는 적들을 격퇴하는 타워 디펜스 게임을 개발해주세요. 화면 중앙의 타워는 자동으로 가장 가까운 적을 향해 발사하며, 플레이어는 웨이브 사이에 획득한 자원으로 타워를 업그레이드(공격력, 공격 속도, 범위 등)할 수 있습니다. 시간이 지날수록 더 강력하고 다양한 적(빠른 적, 방어력 높은 적, 분열하는 적 등)이 등장하며, 타워의 체력이 0이 되면 게임 오버입니다. 특수 능력(범위 공격, 일시 정지, 즉시 회복 등)과 함께 생존한 웨이브 수에 따른 랭킹 시스템도 구현해주세요."},
|
89 |
-
{"description": "캐릭터가 끝없이 달리며 좀비와 장애물을 피하는 사이드 스크롤링 러너 게임을 만들어주세요. 스페이스바로 점프, S키로 슬라이딩하여 다양한 장애물(웅덩이, 장벽, 좀비 무리 등)을 피해야 합니다. 코인과 파워업(일시적 무적, 자석 효과, 속도 감소 등)을 수집하며, 특정 구간마다 미니 보스 좀비와의 간단한 전투도 포함됩니다. 거리에 따라 점수가 증가하고, 코인으로 캐릭터 업그레이드(더블 점프, 체력 증가 등)를 구매할 수 있는 시스템도 구현해주세요."},
|
90 |
-
{"description": "탑다운 뷰의 간단한 액션 RPG 게임을 개발해주세요. WASD로 이동하고, 마우스 클릭으로 기본 공격, 1-4 키로 특수 스킬을 사용합니다. 플레이어는 몬스터를 처치하며 경험치와 아이템을 획득하고, 레벨업 시 능력치(공격력, 체력, 속도 등)를 향상시킵니다. 다양한 무기와 방어구를 착용할 수 있으며, 스킬 트리 시스템으로 캐릭터를 특화시킬 수 있습니다. 여러 지역과 보스 몬스터, 간단한 퀘스트 시스템도 구현해주세요."},
|
91 |
-
]
|
92 |
-
|
93 |
-
SystemPrompt = """
|
94 |
-
# GameCraft 시스템 프롬프트
|
95 |
-
|
96 |
-
## 1. 기본 정보 및 역할
|
97 |
-
당신의 이름은 'GameCraft'입니다. 당신은 게임플레이 메커니즘, 인터랙티브 디자인, 성능 최적화에 뛰어난 웹 게임 개발 전문가입니다. HTML, JavaScript, CSS를 활용하여 간결하고 효율적인 웹 기반 게임을 제작하는 것이 당신의 임무입니다.
|
98 |
-
|
99 |
-
## 2. 핵심 기술 스택
|
100 |
-
- **프론트엔드**: HTML5, CSS3, JavaScript(ES6+)
|
101 |
-
- **렌더링 방식**: 브라우저에서 직접 렌더링 가능한 코드 생성
|
102 |
-
- **코드 스타일**: 바닐라 JavaScript 우선, 외부 라이브러리 최소화
|
103 |
-
|
104 |
-
## 3. 게임 유형별 특화 지침
|
105 |
-
### 3.1 아케이드/액션 게임
|
106 |
-
- 간결한 충돌 감지 시스템 구현
|
107 |
-
- 키보드/터치 입력 최적화
|
108 |
-
- 기본적인 점수 시스템
|
109 |
-
|
110 |
-
### 3.2 퍼즐 게임
|
111 |
-
- 명확한 게임 규칙 및 승리 조건
|
112 |
-
- 기본 난이도 구현
|
113 |
-
- 핵심 게임 메커니즘에 집중
|
114 |
-
|
115 |
-
### 3.3 카드/보드 게임
|
116 |
-
- 간소화된 턴 기반 시스템
|
117 |
-
- 기본 게임 규칙 자동화
|
118 |
-
- 핵심 게임 로직 중심
|
119 |
-
|
120 |
-
### 3.4 시뮬레이션 게임
|
121 |
-
- 효율적인 상태 관리
|
122 |
-
- 가장 중요한 상호작용 구현
|
123 |
-
- 핵심 요소만 포함
|
124 |
-
|
125 |
-
## 4. 이모지 활용 지침 🎮
|
126 |
-
- 게임 UI 요소에 이모지 활용 (예: 생명력 ❤️, 코인 💰, 시간 ⏱️)
|
127 |
-
- 이모지 사용은 핵심 요소에만 집중
|
128 |
-
|
129 |
-
## 5. 기술적 구현 가이드라인
|
130 |
-
### 5.1 코드 구조
|
131 |
-
- **간결성 중시**: 코드는 최대한 간결하게 작성하고, 주석은 최소화
|
132 |
-
- **모듈화**: 코드 기능별로 분리하되 불필요한 추상화 지양
|
133 |
-
- **최적화**: 게임 루프와 렌더링 최적화에 집중
|
134 |
-
- **코드 크기 제한**: 전체 코드는 200줄을 넘지 않도록 함 (API 호출 시에는 600줄 제한 고려)
|
135 |
-
|
136 |
-
### 5.2 성능 최적화
|
137 |
-
- DOM 조작 최소화
|
138 |
-
- 불필요한 변수와 함수 제거
|
139 |
-
- 메모리 관리에 주의
|
140 |
-
|
141 |
-
### 5.3 반응형 디자인
|
142 |
-
- 기본적인 반응형 지원
|
143 |
-
- 핵심 기능에 집중한 심플한 UI
|
144 |
-
|
145 |
-
## 6. 외부 라이브러리
|
146 |
-
- 라이브러리 사용은 최소화하고, 필요한 경우에만 사용
|
147 |
-
- 라이브러리 사용 시 CDN으로 가져올 것
|
148 |
-
|
149 |
-
## 7. 접근성 및 포용성
|
150 |
-
- 핵심 접근성 기능에만 집중
|
151 |
-
|
152 |
-
## 8. 제약사항 및 유의사항
|
153 |
-
- 외부 API 호출 금지 (Vercel 배포 제외)
|
154 |
-
- 코드 크기 최소화에 우선순위 (API 호출 시 600줄 이내, 최종 목표 200줄)
|
155 |
-
- 주석 최소화 - 필수적인 설명만 포함
|
156 |
-
- 불필요한 기능 구현 지양
|
157 |
-
|
158 |
-
## 9. 출력 형식
|
159 |
-
- HTML 코드 블록으로만 코드 반환
|
160 |
-
- 추가 설명 없이 즉시 실행 가능한 코드만 제공
|
161 |
-
- 모든 코드는 단일 HTML 파일에 인라인으로 포함
|
162 |
-
|
163 |
-
## 10. 코드 품질 기준
|
164 |
-
- 효율성과 간결함이 최우선
|
165 |
-
- 핵심 게임플레이 메커니즘에만 집중
|
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"
|
185 |
-
ASSISTANT = "assistant"
|
186 |
-
|
187 |
-
History = List[Tuple[str, str]]
|
188 |
-
Messages = List[Dict[str, str]]
|
189 |
-
|
190 |
-
IMAGE_CACHE = {}
|
191 |
-
|
192 |
-
def get_image_base64(image_path):
|
193 |
-
"""
|
194 |
-
이미지 파일을 base64로 읽어서 캐싱
|
195 |
-
"""
|
196 |
-
if image_path in IMAGE_CACHE:
|
197 |
-
return IMAGE_CACHE[image_path]
|
198 |
-
try:
|
199 |
-
with open(image_path, "rb") as image_file:
|
200 |
-
encoded_string = base64.b64encode(image_file.read()).decode()
|
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:
|
413 |
-
data_list.append({
|
414 |
-
"name": f"[게임] {item['description'][:20]}...",
|
415 |
-
"prompt": item['description']
|
416 |
-
})
|
417 |
-
return data_list
|
418 |
-
|
419 |
-
def create_template_html(title, items):
|
420 |
-
"""
|
421 |
-
이미지 없이 템플릿 HTML 생성
|
422 |
-
"""
|
423 |
-
html_content = r"""
|
424 |
-
<style>
|
425 |
-
.prompt-grid {
|
426 |
-
display: grid;
|
427 |
-
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
428 |
-
gap: 16px;
|
429 |
-
padding: 12px;
|
430 |
-
}
|
431 |
-
.prompt-card {
|
432 |
-
background: white;
|
433 |
-
border: 1px solid #eee;
|
434 |
-
border-radius: 12px;
|
435 |
-
padding: 12px;
|
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);
|
445 |
-
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
|
446 |
-
}
|
447 |
-
.card-name {
|
448 |
-
font-weight: bold;
|
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 |
-
|
532 |
-
|
533 |
-
# ------------------------
|
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 |
-
def send_to_sandbox(code):
|
649 |
-
"""Encodes HTML code for display in an iframe sandbox."""
|
650 |
-
logger.debug(f"Preparing code for sandbox (length: {len(code)})")
|
651 |
-
# No need to call remove_code_block here, assume input is already cleaned code
|
652 |
-
# clean_code = remove_code_block(code) # Remove this line
|
653 |
-
clean_code = optimize_code(code) # Optimize the already extracted code
|
654 |
-
|
655 |
-
if not clean_code.strip():
|
656 |
-
logger.warning("Cannot send empty code to sandbox.")
|
657 |
-
# Return an empty iframe or an error message?
|
658 |
-
return "<p style='color: red;'>No code to display in sandbox.</p>"
|
659 |
-
|
660 |
-
# Basic HTML structure check and wrapping
|
661 |
-
if not re.search(r'<!DOCTYPE html>', clean_code, re.IGNORECASE) and not re.search(r'<html.*?>', clean_code, re.IGNORECASE):
|
662 |
-
logger.debug("Wrapping code in basic HTML structure for sandbox.")
|
663 |
-
# Check if body tags exist, if not, wrap the whole thing
|
664 |
-
if not re.search(r'<body.*?>', clean_code, re.IGNORECASE):
|
665 |
-
# --- 수정된 부분: f-string 내 주석 제거 ---
|
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>
|
673 |
-
</head>
|
674 |
-
<body>
|
675 |
-
{clean_code}
|
676 |
-
</body>
|
677 |
-
</html>"""
|
678 |
-
# --- 수정 끝 ---
|
679 |
-
else:
|
680 |
-
# If body exists, just ensure doctype and html/head are present
|
681 |
-
# --- 수정된 부분: f-string 내 주석 제거 (else 블록도 확인) ---
|
682 |
-
clean_code = f"""<!DOCTYPE html>
|
683 |
-
<html lang="en">
|
684 |
-
<head>
|
685 |
-
<meta charset="UTF-8">
|
686 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
687 |
-
<title>Game Preview</title>
|
688 |
-
<style>body {{ margin: 0; overflow: hidden; }}</style>
|
689 |
-
</head>
|
690 |
-
{clean_code}
|
691 |
-
</html>"""
|
692 |
-
# --- 수정 끝 ---
|
693 |
-
|
694 |
-
|
695 |
-
try:
|
696 |
-
# Encode the final HTML
|
697 |
-
encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8')
|
698 |
-
data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}"
|
699 |
-
# Define sandbox attributes for security
|
700 |
-
sandbox_attributes = "allow-scripts allow-same-origin allow-forms" # Allow necessary permissions
|
701 |
-
iframe_html = f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;" sandbox="{sandbox_attributes}"></iframe>'
|
702 |
-
logger.debug("Sandbox iframe HTML generated.")
|
703 |
-
return iframe_html
|
704 |
-
except Exception as e:
|
705 |
-
logger.error(f"Error encoding HTML for sandbox: {e}", exc_info=True)
|
706 |
-
return f"<p style='color: red;'>Error creating sandbox preview: {html.escape(str(e))}</p>"
|
707 |
-
|
708 |
-
|
709 |
-
|
710 |
-
|
711 |
-
def boost_prompt(prompt: str) -> str:
|
712 |
-
# (Boost prompt logic remains the same, ensure API clients are checked)
|
713 |
-
if not prompt:
|
714 |
-
return ""
|
715 |
-
|
716 |
-
boost_system_prompt = """당신은 웹 게임 개발 프롬프트 전문가입니다.
|
717 |
-
주어진 프롬프트를 분석하여 더 명확하고 간결한 요구사항으로 변환하되,
|
718 |
-
원래 의도와 목적은 그대로 유지하면서 다음 관점들을 고려하여 증강하십시오:
|
719 |
-
|
720 |
-
1. 게임플레이 핵심 메커니즘 명확히 정의 (1-2 문장)
|
721 |
-
2. 필수적인 상호작용 요소만 포함 (예: 키보드 조작, 클릭)
|
722 |
-
3. 핵심 UI 요소 간략히 기술 (예: 점수 표시, 게임 영역)
|
723 |
-
4. 코드 간결성 유지를 위한 우선순위 설정 (가장 중요한 기능 1-2가지 명시)
|
724 |
-
5. 기본적인 게임 규칙과 승리/패배 조건 명시 (간단하게)
|
725 |
-
|
726 |
-
다음 중요 지침을 반드시 준수하세요:
|
727 |
-
- 불필요한 세부 사항이나 부가 기능은 제외 (예: 사운드, 애니메이션, 레벨 디자인 복잡화)
|
728 |
-
- 생성될 코드가 600줄을 넘지 않도록 기능을 제한
|
729 |
-
- 명확하고 간결한 언어로 요구사항 작성 (총 5-7 문장 이내)
|
730 |
-
- 최소한의 필수 게임 요소만 포함
|
731 |
-
- 최종 결과는 증강된 프롬프트 텍스트만 반환 (추가 설명 없이)
|
732 |
-
"""
|
733 |
-
logger.debug(f"Boosting prompt: {prompt[:100]}...")
|
734 |
-
boosted_prompt = prompt # Default to original if APIs fail
|
735 |
-
|
736 |
-
try:
|
737 |
-
if claude_client:
|
738 |
-
logger.debug("Attempting boost with Claude.")
|
739 |
-
response = claude_client.messages.create(
|
740 |
-
model="claude-3-haiku-20240307", # Use Haiku for faster/cheaper boosting
|
741 |
-
max_tokens=500, # Reduced max_tokens for concise output
|
742 |
-
temperature=0.2, # Lower temperature for more focused output
|
743 |
-
messages=[{
|
744 |
-
"role": "user",
|
745 |
-
"content": f"다음 게임 프롬프트를 분석하고 위의 지침에 따라 간결하게 증강하세요:\n\n{prompt}"
|
746 |
-
}],
|
747 |
-
system=boost_system_prompt
|
748 |
-
)
|
749 |
-
if response.content and len(response.content) > 0 and hasattr(response.content[0], 'text'):
|
750 |
-
boosted_prompt = response.content[0].text.strip()
|
751 |
-
logger.debug(f"Claude boosted prompt: {boosted_prompt[:100]}...")
|
752 |
-
return boosted_prompt
|
753 |
-
else:
|
754 |
-
logger.warning("Claude boost response format unexpected.")
|
755 |
-
raise anthropic.APIError("Invalid response format from Claude.") # Trigger fallback
|
756 |
-
|
757 |
-
elif openai_client:
|
758 |
-
logger.debug("Claude failed or unavailable, attempting boost with OpenAI.")
|
759 |
-
completion = openai_client.chat.completions.create(
|
760 |
-
model="gpt-3.5-turbo", # Use a cheaper/faster model for boosting
|
761 |
-
messages=[
|
762 |
-
{"role": "system", "content": boost_system_prompt},
|
763 |
-
{"role": "user", "content": f"다음 게임 프롬프트를 분석하고 위의 지침에 따라 간결하게 증강하세요:\n\n{prompt}"}
|
764 |
-
],
|
765 |
-
max_tokens=500, # Reduced
|
766 |
-
temperature=0.2 # Lowered
|
767 |
-
)
|
768 |
-
if completion.choices and len(completion.choices) > 0 and completion.choices[0].message:
|
769 |
-
boosted_prompt = completion.choices[0].message.content.strip()
|
770 |
-
logger.debug(f"OpenAI boosted prompt: {boosted_prompt[:100]}...")
|
771 |
-
return boosted_prompt
|
772 |
-
else:
|
773 |
-
logger.warning("OpenAI boost response format unexpected.")
|
774 |
-
# Fall through to return original prompt
|
775 |
-
|
776 |
-
else:
|
777 |
-
logger.warning("Neither Claude nor OpenAI client available for boosting.")
|
778 |
-
# Fall through to return original prompt
|
779 |
-
|
780 |
-
except Exception as e:
|
781 |
-
logger.error(f"Error during prompt boosting: {e}", exc_info=True)
|
782 |
-
# Fall through to return original prompt
|
783 |
-
|
784 |
-
logger.debug("Boosting failed or skipped, returning original prompt.")
|
785 |
-
return boosted_prompt # Return original if all attempts fail
|
786 |
-
|
787 |
-
|
788 |
-
def handle_boost(prompt: str):
|
789 |
-
"""Handles the boost button click event."""
|
790 |
-
logger.info("Boost button clicked.")
|
791 |
-
if not prompt or not prompt.strip():
|
792 |
-
logger.warning("Boost requested for empty prompt.")
|
793 |
-
# Return original empty prompt and don't change tab state
|
794 |
-
return "", gr.update() # Use gr.update() for no change
|
795 |
-
|
796 |
-
try:
|
797 |
-
boosted = boost_prompt(prompt)
|
798 |
-
# Return the boosted prompt to the input textarea
|
799 |
-
# Keep the active tab as is (don't switch to empty)
|
800 |
-
return boosted, gr.update() # Use gr.update() for no change to the tab state
|
801 |
-
except Exception as e:
|
802 |
-
logger.error(f"Error in handle_boost: {e}", exc_info=True)
|
803 |
-
# Return the original prompt in case of error, maybe show a notification?
|
804 |
-
# For now, just return original prompt
|
805 |
-
return prompt, gr.update()
|
806 |
-
|
807 |
-
|
808 |
-
def history_render(history: History):
|
809 |
-
"""Prepares history for display in the chatbot component."""
|
810 |
-
logger.debug(f"Rendering history. Number of turns: {len(history)}")
|
811 |
-
# The history format should already be correct for the legacy.Chatbot
|
812 |
-
# Just need to trigger the drawer opening
|
813 |
-
return gr.update(open=True), history
|
814 |
-
|
815 |
-
|
816 |
-
def execute_code(query: str):
|
817 |
-
"""Handles the 'Code' (Execute) button click."""
|
818 |
-
logger.info("Execute code button clicked.")
|
819 |
-
if not query or not query.strip():
|
820 |
-
logger.warning("Execute code requested for empty input.")
|
821 |
-
return None, gr.update(active_key="empty") # Stay on empty tab
|
822 |
-
|
823 |
-
try:
|
824 |
-
# Extract code first
|
825 |
-
clean_code = remove_code_block(query)
|
826 |
-
logger.debug(f"Code extracted for execution (length: {len(clean_code)})")
|
827 |
-
|
828 |
-
if not clean_code or not clean_code.strip():
|
829 |
-
logger.warning("No valid code found in input to execute.")
|
830 |
-
# Maybe return an error message in the sandbox?
|
831 |
-
error_html = "<p style='color:orange;'>입력에서 실행할 코드를 찾을 수 없습니다. 코드 블록(```html ... ```) 안에 코드를 넣어보세요.</p>"
|
832 |
-
return error_html, gr.update(active_key="render") # Show error in render tab
|
833 |
-
|
834 |
-
# Send the extracted code to the sandbox
|
835 |
-
sandbox_html = send_to_sandbox(clean_code)
|
836 |
-
logger.debug("Switching tab to 'render' for code execution.")
|
837 |
-
return sandbox_html, gr.update(active_key="render")
|
838 |
-
|
839 |
-
except Exception as e:
|
840 |
-
logger.error(f"Error during execute_code: {e}", exc_info=True)
|
841 |
-
error_html = f"<p style='color: red;'>코드 실행 중 오류 발생: {html.escape(str(e))}</p>"
|
842 |
-
# Show error message in the render tab
|
843 |
-
return error_html, gr.update(active_key="render")
|
844 |
-
|
845 |
-
|
846 |
-
# ------------------------
|
847 |
-
# 6) 데모 클래스
|
848 |
-
# ------------------------
|
849 |
-
|
850 |
-
class Demo:
|
851 |
-
def __init__(self):
|
852 |
-
pass # No state needed here for now
|
853 |
-
|
854 |
-
async def generation_code(self, query: Optional[str], _setting: Dict[str, str], _history: Optional[History]):
|
855 |
-
"""Generates game code based on user query and history."""
|
856 |
-
logger.info(f"Generation request received. Query: '{query[:50]}...', History length: {len(_history) if _history else 0}")
|
857 |
-
|
858 |
-
# Handle empty query - use random demo
|
859 |
-
if not query or not query.strip():
|
860 |
-
logger.info("Empty query received, using random demo description.")
|
861 |
-
query = random.choice(DEMO_LIST)['description']
|
862 |
-
# Update the input field visually? Might be complex. For now, just use it internally.
|
863 |
-
|
864 |
-
# Initialize history if None
|
865 |
-
if _history is None:
|
866 |
-
_history = []
|
867 |
-
logger.debug("History was None, initialized to empty list.")
|
868 |
-
|
869 |
-
# Prepare the prompt with constraints
|
870 |
-
# Reduced line count constraint slightly for flexibility
|
871 |
-
constrained_query = f"""
|
872 |
-
다음 게임을 제작해주세요.
|
873 |
-
중요 요구사항:
|
874 |
-
1. 코드는 HTML, CSS, JavaScript 만을 사용하여 단일 HTML 파일로 작성해주세요.
|
875 |
-
2. 모든 코드는 `<script>` 와 `<style>` 태그를 포함하여 HTML 파일 내부에 인라인으로 작성해야 합니다. 외부 파일 참조는 금지합니다.
|
876 |
-
3. 코드는 가능한 한 간결하게 작성하고, 불필요한 주석, 설명, 공백 라인은 최소화해주세요.
|
877 |
-
4. 생성되는 총 코드 라인 수는 **600줄**을 넘지 않도록 엄격히 제한합니다. (HTML, CSS, JS 포함)
|
878 |
-
5. 게임의 핵심 기능만 구현하고, 복잡한 애니메이션, 사운드, 여러 레벨 등 부가 기능은 생략해주세요.
|
879 |
-
6. 라이브러리나 프레임워크 사용 없이 바닐라 JavaScript (ES6+)를 사용해주세요.
|
880 |
-
7. 즉시 실행 가능한 완전한 HTML 코드만 생성하고, 코드 앞뒤에 설명이나 ```html 같은 마크다운 표시는 절대 포함하지 마세요.
|
881 |
-
|
882 |
-
게임 요청:
|
883 |
-
{query}
|
884 |
-
"""
|
885 |
-
logger.debug(f"Prepared constrained query for LLM.")
|
886 |
-
|
887 |
-
# Prepare messages for APIs
|
888 |
-
try:
|
889 |
-
messages = history_to_messages(_history, _setting.get('system', SystemPrompt)) # Use default if missing
|
890 |
-
system_message = messages[0]['content']
|
891 |
-
|
892 |
-
# Claude messages (User/Assistant roles only)
|
893 |
-
claude_messages = []
|
894 |
-
for msg in messages[1:]: # Skip system message
|
895 |
-
# Ensure content exists before adding
|
896 |
-
if msg.get("content", "").strip():
|
897 |
-
claude_messages.append({"role": msg["role"], "content": msg["content"]})
|
898 |
-
# Add the current user query
|
899 |
-
claude_messages.append({'role': Role.USER, 'content': constrained_query})
|
900 |
-
|
901 |
-
# OpenAI messages (System + User/Assistant)
|
902 |
-
openai_messages = [{"role": "system", "content": system_message}]
|
903 |
-
for msg in messages[1:]: # Skip system message
|
904 |
-
if msg.get("content", "").strip():
|
905 |
-
openai_messages.append({"role": msg["role"], "content": msg["content"]})
|
906 |
-
openai_messages.append({"role": "user", "content": constrained_query})
|
907 |
-
|
908 |
-
logger.debug(f"Prepared messages. Claude: {len(claude_messages)}, OpenAI: {len(openai_messages)}")
|
909 |
-
|
910 |
-
except Exception as e:
|
911 |
-
logger.error(f"Error preparing messages: {e}", exc_info=True)
|
912 |
-
yield [
|
913 |
-
f"Error preparing messages: {html.escape(str(e))}",
|
914 |
-
_history,
|
915 |
-
None,
|
916 |
-
gr.update(active_key="empty"),
|
917 |
-
gr.update(open=False) # Keep code drawer closed on error
|
918 |
-
]
|
919 |
-
return # Stop generation
|
920 |
-
|
921 |
-
# Initial UI update: Show loading state
|
922 |
-
yield [
|
923 |
-
"⏳ 게임 코드 생성 시작...", # Initial message in code view
|
924 |
-
_history,
|
925 |
-
None, # No sandbox content yet
|
926 |
-
gr.update(active_key="loading"), # Switch to loading tab
|
927 |
-
gr.update(open=True) # Open code drawer to show progress
|
928 |
-
]
|
929 |
-
await asyncio.sleep(0.1) # Allow UI to update
|
930 |
-
|
931 |
-
collected_content = None
|
932 |
-
error_message = None
|
933 |
-
api_used = None
|
934 |
-
|
935 |
-
# Try Claude first
|
936 |
-
if claude_client:
|
937 |
-
try:
|
938 |
-
logger.info("Attempting code generation with Claude...")
|
939 |
-
api_used = "Claude"
|
940 |
-
# Use async for loop correctly
|
941 |
-
async for content_chunk in try_claude_api(system_message, claude_messages):
|
942 |
-
collected_content = content_chunk # Update collected content with each chunk
|
943 |
-
yield [
|
944 |
-
f"```html\n{collected_content}\n```", # Show streaming progress in code block
|
945 |
-
_history,
|
946 |
-
None,
|
947 |
-
gr.update(active_key="loading"),
|
948 |
-
gr.update(open=True)
|
949 |
-
]
|
950 |
-
# No sleep needed, handled by async iterator
|
951 |
-
logger.info("Claude generation successful.")
|
952 |
-
|
953 |
-
except Exception as e:
|
954 |
-
logger.warning(f"Claude API failed: {e}", exc_info=False) # Log less verbosely for expected fallbacks
|
955 |
-
error_message = f"Claude API Error: {e}"
|
956 |
-
collected_content = None # Reset content on error
|
957 |
-
|
958 |
-
# Fallback to OpenAI if Claude failed or is unavailable
|
959 |
-
if collected_content is None and openai_client:
|
960 |
-
logger.info("Falling back to OpenAI for code generation...")
|
961 |
-
api_used = "OpenAI"
|
962 |
-
error_message = None # Clear previous error
|
963 |
-
try:
|
964 |
-
# Use async for loop correctly
|
965 |
-
async for content_chunk in try_openai_api(openai_messages):
|
966 |
-
collected_content = content_chunk # Update collected content
|
967 |
-
yield [
|
968 |
-
f"```html\n{collected_content}\n```", # Show streaming progress
|
969 |
-
_history,
|
970 |
-
None,
|
971 |
-
gr.update(active_key="loading"),
|
972 |
-
gr.update(open=True)
|
973 |
-
]
|
974 |
-
# No sleep needed
|
975 |
-
logger.info("OpenAI generation successful.")
|
976 |
-
|
977 |
-
except Exception as e:
|
978 |
-
logger.error(f"OpenAI API also failed: {e}", exc_info=True)
|
979 |
-
error_message = f"OpenAI API Error: {e}. Both APIs failed."
|
980 |
-
collected_content = None
|
981 |
-
|
982 |
-
# Process the final result
|
983 |
-
if collected_content:
|
984 |
-
logger.info(f"Code generation completed using {api_used}. Final content length: {len(collected_content)}")
|
985 |
-
# Clean the final generated code (remove potential markdown fences if LLM added them)
|
986 |
-
final_code = remove_code_block(collected_content)
|
987 |
-
if not final_code: # If remove_code_block failed or LLM output was just markdown
|
988 |
-
final_code = collected_content # Use raw content as fallback
|
989 |
-
|
990 |
-
# Validate code length (using the raw code before optimization for check)
|
991 |
-
code_lines = final_code.count('\n') + 1
|
992 |
-
# Increased limit slightly based on system prompt
|
993 |
-
line_limit = 700
|
994 |
-
if code_lines > line_limit:
|
995 |
-
logger.warning(f"Generated code is too long: {code_lines} lines (limit: {line_limit}).")
|
996 |
-
warning_msg = f"""
|
997 |
-
⚠️ **경고: 생성된 코드가 너무 깁니다 ({code_lines}줄 / {line_limit}줄 제한)**
|
998 |
-
코드가 너무 길어 브라우저에서 느리거나 오류가 발생할 수 있습니다.
|
999 |
-
더 간결한 게임을 요청하거나, "코드" 버튼으로 직접 실행해보세요.
|
1000 |
-
|
1001 |
-
```html
|
1002 |
-
{html.escape(final_code[:2000])}
|
1003 |
-
... (코드가 너무 길어 일부만 표시) ...
|
1004 |
-
```"""
|
1005 |
-
# Update history with the failure/warning? Or just show warning?
|
1006 |
-
# Let's just show the warning in the code output for now.
|
1007 |
-
yield [
|
1008 |
-
warning_msg,
|
1009 |
-
_history, # History remains unchanged on length warning
|
1010 |
-
None, # No sandbox for overly long code
|
1011 |
-
gr.update(active_key="empty"), # Go back to empty state
|
1012 |
-
gr.update(open=True) # Keep code drawer open to show warning
|
1013 |
-
]
|
1014 |
-
else:
|
1015 |
-
# Success: Update history and show code + sandbox
|
1016 |
-
logger.info("Code length within limits. Preparing final output.")
|
1017 |
-
# Prepare messages for history update (use the *original* query, not constrained one)
|
1018 |
-
messages_for_history = messages + [{'role': Role.USER, 'content': query}, {'role': Role.ASSISTANT, 'content': final_code}]
|
1019 |
-
_updated_history = messages_to_history(messages_for_history)
|
1020 |
-
|
1021 |
-
# Send final, cleaned code to sandbox
|
1022 |
-
sandbox_html = send_to_sandbox(final_code)
|
1023 |
-
|
1024 |
-
yield [
|
1025 |
-
f"```html\n{final_code}\n```", # Show final code in code block
|
1026 |
-
_updated_history,
|
1027 |
-
sandbox_html, # Render in sandbox
|
1028 |
-
gr.update(active_key="render"), # Switch to render tab
|
1029 |
-
gr.update(open=True) # Keep code drawer open
|
1030 |
-
]
|
1031 |
-
else:
|
1032 |
-
# Both APIs failed
|
1033 |
-
logger.error(f"Code generation failed using both APIs. Last error: {error_message}")
|
1034 |
-
yield [
|
1035 |
-
f"❌ 코드 생성 실패: {html.escape(error_message)}",
|
1036 |
-
_history, # History remains unchanged on failure
|
1037 |
-
None,
|
1038 |
-
gr.update(active_key="empty"),
|
1039 |
-
gr.update(open=False) # Close drawer on total failure
|
1040 |
-
]
|
1041 |
-
|
1042 |
-
def clear_history(self):
|
1043 |
-
"""Clears the conversation history."""
|
1044 |
-
logger.info("Clear history called.")
|
1045 |
-
# Also clear the code output and sandbox, and reset tabs
|
1046 |
-
return (
|
1047 |
-
[], # Cleared history state
|
1048 |
-
"", # Clear code output
|
1049 |
-
None, # Clear sandbox
|
1050 |
-
gr.update(active_key="empty"), # Reset tab to empty
|
1051 |
-
gr.update(open=False), # Close code drawer
|
1052 |
-
gr.update(value=""), # Clear input text
|
1053 |
-
gr.update(value="배포 결과가 여기에 표시됩니다.") # Reset deploy result
|
1054 |
-
)
|
1055 |
-
|
1056 |
-
|
1057 |
-
####################################################
|
1058 |
-
# Vercel Deployment Functions
|
1059 |
-
####################################################
|
1060 |
-
def deploy_to_vercel(code: str):
|
1061 |
-
"""Deploys the given HTML code to Vercel."""
|
1062 |
-
logger.info(f"[deploy_to_vercel] Attempting deployment. Code length: {len(code)}")
|
1063 |
-
vercel_token = os.getenv("VERCEL_API_TOKEN", "A8IFZmgW2cqA4yUNlLPnci0N") # Use env var first
|
1064 |
-
|
1065 |
-
if not vercel_token:
|
1066 |
-
logger.error("[deploy_to_vercel] Vercel API token (VERCEL_API_TOKEN or hardcoded) not found.")
|
1067 |
-
return "⚠️ **배포 실패:** Vercel API 토큰이 설정되지 않았습니다."
|
1068 |
-
|
1069 |
-
if not code or len(code.strip()) < 50: # Increased minimum length
|
1070 |
-
logger.warning("[deploy_to_vercel] Code is too short or empty for deployment.")
|
1071 |
-
return "⚠️ **배포 실패:** 배포할 코드가 너무 짧습니다 (최소 50자 필요)."
|
1072 |
-
|
1073 |
-
# Ensure code is wrapped in basic HTML if it isn't already
|
1074 |
-
if not re.search(r'<!DOCTYPE html>', code, re.IGNORECASE) and not re.search(r'<html.*?>', code, re.IGNORECASE):
|
1075 |
-
logger.debug("[deploy_to_vercel] Wrapping raw code in basic HTML structure for deployment.")
|
1076 |
-
code = f"""<!DOCTYPE html>
|
1077 |
-
<html lang="en">
|
1078 |
-
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Deployed Game</title></head>
|
1079 |
-
<body>{code}</body>
|
1080 |
-
</html>"""
|
1081 |
-
|
1082 |
-
project_name = 'gamecraft-' + ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
1083 |
-
logger.info(f"[deploy_to_vercel] Generated project name: {project_name}")
|
1084 |
-
|
1085 |
-
deploy_url = "https://api.vercel.com/v13/deployments"
|
1086 |
-
headers = {
|
1087 |
-
"Authorization": f"Bearer {vercel_token}",
|
1088 |
-
"Content-Type": "application/json"
|
1089 |
-
}
|
1090 |
-
|
1091 |
-
# Define files for deployment (only index.html needed for simple static deployment)
|
1092 |
-
files = [
|
1093 |
-
{"file": "index.html", "data": code}
|
1094 |
-
]
|
1095 |
-
|
1096 |
-
# Define deployment data (simpler structure for static deployment)
|
1097 |
-
deploy_data = {
|
1098 |
-
"name": project_name,
|
1099 |
-
"files": files,
|
1100 |
-
"target": "production",
|
1101 |
-
# Specify project settings to ensure it's treated as static
|
1102 |
-
"projectSettings": {
|
1103 |
-
"framework": None, # Explicitly set to null for static
|
1104 |
-
"outputDirectory": None # Not needed if deploying files directly
|
1105 |
-
}
|
1106 |
-
}
|
1107 |
-
|
1108 |
-
try:
|
1109 |
-
logger.debug("[deploy_to_vercel] Sending deployment request to Vercel API...")
|
1110 |
-
deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data, timeout=90) # Increased timeout
|
1111 |
-
logger.debug(f"[deploy_to_vercel] Vercel API response status: {deploy_response.status_code}")
|
1112 |
-
|
1113 |
-
response_data = {}
|
1114 |
-
try:
|
1115 |
-
response_data = deploy_response.json()
|
1116 |
-
except json.JSONDecodeError:
|
1117 |
-
logger.warning("[deploy_to_vercel] Vercel response was not valid JSON.")
|
1118 |
-
|
1119 |
-
if deploy_response.status_code not in [200, 201]: # Vercel might return 201 Created
|
1120 |
-
error_message = response_data.get('error', {}).get('message', deploy_response.text)
|
1121 |
-
logger.error(f"[deploy_to_vercel] Deployment failed: {deploy_response.status_code} - {error_message}")
|
1122 |
-
# Limit error message length shown to user
|
1123 |
-
return f"⚠️ **배포 실패:** Vercel API 오류 ({deploy_response.status_code}).\n```\n{html.escape(error_message[:500])}\n```"
|
1124 |
-
|
1125 |
-
# Extract the deployment URL from the response
|
1126 |
-
deployment_url = response_data.get('url')
|
1127 |
-
if not deployment_url:
|
1128 |
-
logger.error("[deploy_to_vercel] Deployment succeeded but URL not found in response.")
|
1129 |
-
return "⚠️ **배포 실패:** 배포는 성공했지만 URL을 가져올 수 없습니다."
|
1130 |
-
|
1131 |
-
# Vercel usually adds https:// automatically, but ensure it's there
|
1132 |
-
if not deployment_url.startswith(('http://', 'https://')):
|
1133 |
-
deployment_url = f"https://{deployment_url}"
|
1134 |
-
|
1135 |
-
logger.info(f"[deploy_to_vercel] Deployment successful! URL: {deployment_url}")
|
1136 |
-
|
1137 |
-
# Return Markdown link
|
1138 |
-
result_markdown = f"""
|
1139 |
-
✅ **배포 완료!**
|
1140 |
-
게임이 다음 주소에 배포되었습니다:
|
1141 |
-
[**{deployment_url}**]({deployment_url})
|
1142 |
-
|
1143 |
-
*참고: 배포된 사이트가 활성화되기까지 몇 분 정도 소요될 수 있습니다.*
|
1144 |
-
"""
|
1145 |
-
logger.debug("[deploy_to_vercel] Returning success Markdown.")
|
1146 |
-
return result_markdown.strip() # Strip leading/trailing whitespace
|
1147 |
-
|
1148 |
-
except requests.exceptions.Timeout:
|
1149 |
-
logger.error("[deploy_to_vercel] Request to Vercel API timed out.")
|
1150 |
-
return "⚠️ **배포 실패:** Vercel API 요청 시간 초과."
|
1151 |
-
except requests.exceptions.RequestException as e:
|
1152 |
-
logger.error(f"[deploy_to_vercel] Network or Request Error: {e}", exc_info=True)
|
1153 |
-
return f"⚠️ **배포 실패:** 네트워크 또는 요청 오류 발생.\n```\n{html.escape(str(e))}\n```"
|
1154 |
-
except Exception as e:
|
1155 |
-
logger.error(f"[deploy_to_vercel] Unexpected Error during deployment: {e}", exc_info=True)
|
1156 |
-
return f"⚠️ **배포 실패:** 예상치 못한 오류 발생.\n```\n{html.escape(str(e))}\n```"
|
1157 |
-
|
1158 |
-
|
1159 |
-
def handle_deploy_click(code_output_value: Optional[str]):
|
1160 |
-
"""Handles the deploy button click event."""
|
1161 |
-
logger.info("Deploy button clicked.")
|
1162 |
-
|
1163 |
-
# Add an immediate feedback message
|
1164 |
-
yield "⏳ 배포를 시작합니다... Vercel API 호출 중..."
|
1165 |
-
|
1166 |
-
if not code_output_value:
|
1167 |
-
logger.warning("[handle_deploy_click] No code available in code_output.")
|
1168 |
-
yield "⚠️ **배포 실패:** 생성된 코드가 없습니다."
|
1169 |
-
return
|
1170 |
-
|
1171 |
-
# Extract the actual code from the Markdown component's value
|
1172 |
-
# The value might contain ```html ... ```, so remove it.
|
1173 |
-
clean_code = remove_code_block(code_output_value)
|
1174 |
-
logger.debug(f"[handle_deploy_click] Extracted code for deployment (length: {len(clean_code)}).")
|
1175 |
-
|
1176 |
-
if not clean_code or len(clean_code.strip()) < 50:
|
1177 |
-
logger.warning("[handle_deploy_click] Cleaned code is too short for deployment.")
|
1178 |
-
yield "⚠️ **배포 실패:** 배포할 유효한 코드가 부족합니다 (최소 50자 필요)."
|
1179 |
-
return
|
1180 |
-
|
1181 |
-
# Call the deployment function
|
1182 |
-
deployment_result = deploy_to_vercel(clean_code)
|
1183 |
-
logger.info(f"[handle_deploy_click] Deployment result received.")
|
1184 |
-
|
1185 |
-
# Yield the final result to update the Markdown component
|
1186 |
-
yield deployment_result
|
1187 |
-
|
1188 |
-
|
1189 |
-
# ------------------------
|
1190 |
-
# 8) Gradio / Modelscope UI 빌드
|
1191 |
-
# ------------------------
|
1192 |
-
demo_instance = Demo()
|
1193 |
-
theme = gr.themes.Soft(
|
1194 |
-
primary_hue="blue",
|
1195 |
-
secondary_hue="purple",
|
1196 |
-
neutral_hue="slate",
|
1197 |
-
spacing_size=gr.themes.sizes.spacing_md,
|
1198 |
-
radius_size=gr.themes.sizes.radius_md,
|
1199 |
-
text_size=gr.themes.sizes.text_md,
|
1200 |
-
)
|
1201 |
-
|
1202 |
-
with gr.Blocks(css="app.css", theme=theme) as demo: # Use css parameter directly
|
1203 |
-
# Removed header_html and integrated title/description into gr.Blocks or Markdown
|
1204 |
-
gr.Markdown("""
|
1205 |
-
<div class="app-header">
|
1206 |
-
<h1>🎮 Vibe Game Craft</h1>
|
1207 |
-
<p>설명을 입력하면 웹 기반 HTML5, JavaScript, CSS 게임을 생성합니다. 실시간 미리보기와 원클릭 배포 기능을 지원합니다.</p>
|
1208 |
-
</div>
|
1209 |
-
""", elem_classes="header-markdown") # Added class for potential styling
|
1210 |
-
|
1211 |
-
# State variables
|
1212 |
-
history = gr.State([])
|
1213 |
-
setting = gr.State({"system": SystemPrompt})
|
1214 |
-
# Removed deploy_status state as we display directly in Markdown
|
1215 |
-
|
1216 |
-
with ms.Application() as app:
|
1217 |
-
with antd.ConfigProvider(): # Provides context for Ant Design components
|
1218 |
-
|
1219 |
-
# Drawers for Code and History
|
1220 |
-
with antd.Drawer(open=False, title="코드 보기", placement="left", width="50%") as code_drawer:
|
1221 |
-
# Use gr.Code for better syntax highlighting if legacy.Markdown causes issues
|
1222 |
-
code_output = gr.Code(language="html", label="Generated Code", interactive=False)
|
1223 |
-
# code_output = legacy.Markdown(elem_classes="code-output-markdown") # Keep if preferred
|
1224 |
-
|
1225 |
-
with antd.Drawer(open=False, title="히스토리", placement="left", width="60%") as history_drawer:
|
1226 |
-
history_output = legacy.Chatbot(
|
1227 |
-
show_label=False,
|
1228 |
-
flushing=False, # Keep flushing false for standard updates
|
1229 |
-
height=800, # Adjust height as needed
|
1230 |
-
elem_classes="history_chatbot",
|
1231 |
-
show_copy_button=True # Add copy button to chat messages
|
1232 |
-
)
|
1233 |
-
|
1234 |
-
# Drawer for Templates
|
1235 |
-
with antd.Drawer(
|
1236 |
-
open=False,
|
1237 |
-
title="🎮 게임 템플릿",
|
1238 |
-
placement="right",
|
1239 |
-
width="60%", # Adjust width
|
1240 |
-
elem_classes="session-drawer"
|
1241 |
-
) as session_drawer:
|
1242 |
-
|
1243 |
-
|
1244 |
-
with antd.Flex(vertical=True, gap="middle"):
|
1245 |
-
# Use gr.HTML for the template display
|
1246 |
-
session_history_html = gr.HTML(elem_classes="session-history-html") # Renamed variable
|
1247 |
-
# Button inside the drawer to close it
|
1248 |
-
close_template_btn = antd.Button("닫기", type="default", elem_classes="close-btn")
|
1249 |
-
|
1250 |
-
# Main Layout (Row with two Columns)
|
1251 |
-
with antd.Row(gutter=[16, 16], align="top", elem_classes="main-layout-row"): # Reduced gutter
|
1252 |
-
|
1253 |
-
# Left Column: Game Preview Area
|
1254 |
-
with antd.Col(xs=24, sm=24, md=14, lg=15, xl=16, elem_classes="preview-column"): # Responsive spans
|
1255 |
-
with ms.Div(elem_classes="preview-panel panel"): # Use ms.Div or standard gr.Column/Group
|
1256 |
-
gr.HTML(r"""
|
1257 |
-
<div class="render_header">
|
1258 |
-
<span class="header_btn red"></span><span class="header_btn yellow"></span><span class="header_btn green"></span>
|
1259 |
-
<span class="render-title">Game Preview</span>
|
1260 |
-
</div>
|
1261 |
-
""", elem_classes="preview-header-html")
|
1262 |
-
# Tabs for different preview states
|
1263 |
-
with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab:
|
1264 |
-
with antd.Tabs.Item(key="empty"):
|
1265 |
-
antd.Empty(description="게임을 만들려면 설명을 입력하거나 템플릿을 선택하세요.", elem_classes="right_content")
|
1266 |
-
with antd.Tabs.Item(key="loading"):
|
1267 |
-
antd.Spin(True, tip="게임 코드 생성 중...", size="large", elem_classes="right_content")
|
1268 |
-
with antd.Tabs.Item(key="render"):
|
1269 |
-
# Use gr.HTML for the sandbox iframe
|
1270 |
-
sandbox = gr.HTML(elem_classes="html_content")
|
1271 |
-
|
1272 |
-
|
1273 |
-
|
1274 |
-
# Right Column: Controls and Input
|
1275 |
-
with antd.Col(xs=24, sm=24, md=10, lg=9, xl=8, elem_classes="control-column"): # Responsive spans
|
1276 |
-
with antd.Flex(vertical=True, gap="middle", elem_classes="control-panel"): # Use Flex for vertical layout
|
1277 |
-
|
1278 |
-
# Top Buttons (Code, History, Templates)
|
1279 |
-
with antd.Flex(gap="small", justify="space-between", elem_classes="top-buttons"):
|
1280 |
-
codeBtn = antd.Button("🧑💻 코드", type="default", icon="<i class='fas fa-code'></i>") # Use icon font if available or text
|
1281 |
-
historyBtn = antd.Button("📜 히스토리", type="default", icon="<i class='fas fa-history'></i>")
|
1282 |
-
template_btn = antd.Button("🎮 템플릿", type="default", icon="<i class='fas fa-gamepad'></i>")
|
1283 |
-
|
1284 |
-
|
1285 |
-
|
1286 |
-
|
1287 |
-
input_text = antd.InputTextarea(
|
1288 |
-
size="large",
|
1289 |
-
allow_clear=True,
|
1290 |
-
placeholder="예: 벽돌깨기 게임 만들어줘 (간단하게)", # Shorter placeholder
|
1291 |
-
# rows=6, # 이 줄 제거
|
1292 |
-
max_length=2000, # Sensible max length for input
|
1293 |
-
show_count=True, # Show character count
|
1294 |
-
elem_classes="main-input-textarea"
|
1295 |
-
)
|
1296 |
-
# Input Textarea
|
1297 |
-
|
1298 |
-
gr.HTML('<div class="help-text">💡 원하는 게임을 설명하거나 템플릿을 사용하세요. 간결할수록 좋습니다!</div>')
|
1299 |
-
|
1300 |
-
# Action Buttons (Send, Boost, Execute, Deploy, Clear)
|
1301 |
-
with antd.Flex(gap="small", wrap='wrap', justify="space-between", elem_classes="action-buttons"): # Allow wrapping
|
1302 |
-
btn = antd.Button("🚀 생성", type="primary", size="middle") # Changed text, adjusted size
|
1303 |
-
boost_btn = antd.Button("✨ 증강", type="default", size="middle")
|
1304 |
-
execute_btn = antd.Button("▶️ 코드 실행", type="default", size="middle") # Changed text
|
1305 |
-
deploy_btn = antd.Button("☁️ 배포", type="default", size="middle")
|
1306 |
-
clear_btn = antd.Button("🧹 클리어", type="default", size="middle", danger=True) # Added danger style
|
1307 |
-
|
1308 |
-
# Deployment Result Area
|
1309 |
-
gr.Markdown("---") # Separator
|
1310 |
-
gr.Markdown("### ☁️ 배포 결과", elem_classes="deploy-header")
|
1311 |
-
deploy_result_container = gr.Markdown(
|
1312 |
-
value="아직 배포된 게임이 없습니다. '배포' 버튼을 클릭하세요.",
|
1313 |
-
label="Deployment Result", # Label might not be visible depending on theme/CSS
|
1314 |
-
elem_classes="deploy-result-markdown" # Add class for styling
|
1315 |
-
)
|
1316 |
-
|
1317 |
-
# --- Event Listeners ---
|
1318 |
-
|
1319 |
-
# Drawer Controls
|
1320 |
-
codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer])
|
1321 |
-
# code_drawer.change(lambda x: gr.update(open=x), inputs=[code_drawer], outputs=[code_drawer]) # Close via change event if needed
|
1322 |
-
|
1323 |
-
historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output])
|
1324 |
-
# history_drawer.change(lambda x: gr.update(open=x), inputs=[history_drawer], outputs=[history_drawer])
|
1325 |
-
|
1326 |
-
# Template Drawer Controls
|
1327 |
-
template_btn.click(
|
1328 |
-
fn=lambda: (gr.update(open=True), load_all_templates()),
|
1329 |
-
outputs=[session_drawer, session_history_html], # Target the gr.HTML component
|
1330 |
-
queue=False # No queue needed for simple UI update
|
1331 |
-
)
|
1332 |
-
# Close button inside the template drawer
|
1333 |
-
close_template_btn.click(lambda: gr.update(open=False), outputs=[session_drawer])
|
1334 |
-
# Also close when clicking outside (default drawer behavior) or via the 'X' icon
|
1335 |
-
|
1336 |
-
# Main Action: Generate Code
|
1337 |
-
btn.click(
|
1338 |
-
demo_instance.generation_code,
|
1339 |
-
inputs=[input_text, setting, history],
|
1340 |
-
outputs=[code_output, history, sandbox, state_tab, code_drawer], # Ensure code_drawer is opened
|
1341 |
-
concurrency_limit=5 # Limit concurrent requests
|
1342 |
-
)
|
1343 |
-
|
1344 |
-
# Clear Button
|
1345 |
-
clear_btn.click(
|
1346 |
-
demo_instance.clear_history,
|
1347 |
-
inputs=[],
|
1348 |
-
# Outputs: history state, code output, sandbox, state tab, code drawer, input text, deploy result
|
1349 |
-
outputs=[history, code_output, sandbox, state_tab, code_drawer, input_text, deploy_result_container]
|
1350 |
-
)
|
1351 |
-
|
1352 |
-
# Boost Button
|
1353 |
-
boost_btn.click(
|
1354 |
-
fn=handle_boost,
|
1355 |
-
inputs=[input_text],
|
1356 |
-
outputs=[input_text, state_tab], # Update input text, don't change tab
|
1357 |
-
queue=False
|
1358 |
-
)
|
1359 |
-
|
1360 |
-
# Execute Code Button
|
1361 |
-
execute_btn.click(
|
1362 |
-
fn=execute_code,
|
1363 |
-
inputs=[input_text], # Takes code directly from input for execution
|
1364 |
-
outputs=[sandbox, state_tab], # Updates sandbox and switches tab
|
1365 |
-
queue=False
|
1366 |
-
)
|
1367 |
-
|
1368 |
-
# Deploy Button
|
1369 |
-
# Use the new handle_deploy_click function which is a generator
|
1370 |
-
deploy_btn.click(
|
1371 |
-
fn=handle_deploy_click,
|
1372 |
-
inputs=[code_output], # Takes code from the code output drawer
|
1373 |
-
outputs=[deploy_result_container], # Updates the deployment result Markdown
|
1374 |
-
# queue=True # Enable queue as deployment can take time
|
1375 |
-
)
|
1376 |
-
|
1377 |
-
# ------------------------
|
1378 |
-
# 9) 실행
|
1379 |
-
# ------------------------
|
1380 |
-
if __name__ == "__main__":
|
1381 |
-
logger.info("Starting Gradio application...")
|
1382 |
-
try:
|
1383 |
-
# demo_instance is already created
|
1384 |
-
# Use share=True for public link if needed, debug=True for more logs
|
1385 |
-
demo.queue(default_concurrency_limit=10).launch(
|
1386 |
-
server_name="0.0.0.0", # Listen on all interfaces for docker/cloud
|
1387 |
-
# share=True, # Uncomment for public link
|
1388 |
-
debug=True, # Enable Gradio debug mode for more logs
|
1389 |
-
# prevent_thread_lock=True # May help in some environments
|
1390 |
-
)
|
1391 |
-
except Exception as e:
|
1392 |
-
logger.critical(f"Failed to launch Gradio demo: {e}", exc_info=True)
|
1393 |
-
raise
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|