Spaces:
Paused
Paused
import os | |
import random | |
import time | |
import html | |
import base64 | |
import string | |
import json | |
import asyncio | |
import requests | |
import anthropic | |
import io | |
import logging | |
from http import HTTPStatus | |
from typing import Dict, List, Optional, Tuple | |
from functools import partial | |
import gradio as gr | |
import modelscope_studio.components.base as ms | |
import modelscope_studio.components.legacy as legacy | |
import modelscope_studio.components.antd as antd | |
# === [1] Logger Setup === | |
log_stream = io.StringIO() | |
handler = logging.StreamHandler(log_stream) | |
logger = logging.getLogger() | |
logger.setLevel(logging.DEBUG) # Set desired level | |
logger.addHandler(handler) | |
def get_logs(): | |
"""Return the logs in the StringIO buffer as a string.""" | |
return log_stream.getvalue() | |
import re | |
deploying_flag = False # Global flag for deployment | |
def get_deployment_update(code_md: str): | |
"""Return a Gradio Markdown update with the Vercel deployment status.""" | |
clean = remove_code_block(code_md) | |
result = deploy_to_vercel(clean) | |
m = re.search(r"https?://[\w\.-]+\.vercel\.app", result) | |
if m: | |
url = m.group(0) | |
md = ( | |
"✅ **Deployment Complete!**\n\n" | |
f"➡️ [Open Deployed App]({url})" | |
) | |
else: | |
md = ( | |
"❌ **Deployment Failed**\n\n" | |
f"```\n{result}\n```" | |
) | |
return gr.update(value=md, visible=True) | |
# ------------------------ | |
# 1) DEMO_LIST and SystemPrompt | |
# ------------------------ | |
DEMO_LIST = [ | |
{ | |
"description": ( | |
"Please build a classic Tetris game with blocks falling from the top. " | |
"Use arrow keys to move and rotate blocks. A completed horizontal line clears " | |
"and increases the score. Speed up over time, include a game over condition, and display score." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a two-player chess game where players take turns. Implement basic chess rules " | |
"(King, Queen, Rook, Bishop, Knight, Pawn moves) with check/checkmate detection. " | |
"Enable drag-and-drop for moving pieces and record each move." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a memory matching card game. Flip a card to reveal an image; " | |
"if two match, the player scores. Include flip animation, track attempts, and implement difficulty levels (easy/medium/hard)." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a space shooter game. Control a spaceship with arrow keys and fire with the spacebar. " | |
"Waves of enemies attack, collisions are detected, and power-ups (shield, multi-fire, speed boost) appear. Difficulty increases gradually." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a sliding puzzle (3x3 or 4x4). Shuffle tiles, then slide them into place using the empty space. " | |
"Include a shuffle function, move counter, completion message, and optional size selection for difficulty." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a classic Snake game. Control the snake with arrow keys to eat randomly placed food. " | |
"Eating food grows the snake. Colliding with itself or a wall ends the game. Score equals the number of food pieces eaten. " | |
"Increase speed over time." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a Breakout game. Move a paddle at the bottom to bounce a ball and break bricks at the top. " | |
"Clearing all bricks finishes a level, losing the ball reduces a life. The ball speeds up over time, " | |
"and special bricks can grant power-ups." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a tower defense game. Place various towers (basic, splash, slow) to stop enemies traveling along a path. " | |
"Each wave is stronger, and destroying enemies grants resources to build or upgrade towers." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build an endless runner. The player jumps over obstacles (rocks, pits, etc.) with spacebar or mouse click. " | |
"Distance is the score. Include collectible coins, power-ups, and increase speed over time." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a 2D platformer. Move with arrow keys, jump with spacebar, and collect items. " | |
"Avoid enemies/traps and reach the goal. Implement multiple levels with a simple health system and checkpoints." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a maze game that generates a new maze each time. " | |
"Use arrow keys to move from the start to the exit. Generate mazes using an algorithm (like DFS or Prim's). " | |
"Add a timer and optionally show a shortest path hint." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a simple turn-based RPG. The player moves on a tile-based map, encountering monsters triggers turn-based combat. " | |
"Include basic attacks, special skills, items, and experience/level-up. Add a shop to buy equipment after winning battles." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a match-3 puzzle game. Swap adjacent items to match 3 or more. " | |
"Matched items disappear, scoring points. Larger matches create special items; chain combos yield extra points. " | |
"Include a target score or limited moves/time mode." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a Flappy Bird-style game. The bird jumps with spacebar or mouse click, avoiding top/bottom pipes. " | |
"Each pipe pair passed scores 1 point, and hitting a pipe or the top/bottom ends the game. Store high scores locally." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a spot-the-difference game using pairs of similar images with 5-10 differences. " | |
"Click differences to mark them. There's a time limit, with penalty for wrong clicks. " | |
"Include a hint system and easy/hard difficulty modes." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a typing game where words fall from the top. Type them correctly before they reach the bottom. " | |
"Words vary in length and speed based on difficulty. Special words provide bonuses or extra time. " | |
"Difficulty increases gradually with the score." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a mini golf game with a basic physics engine. Drag to set shot direction and power, then shoot to reach the hole. " | |
"Multiple courses with obstacles (sand, water, ramps). Keep track of strokes and total score. Optional wind and shot preview." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a fishing simulator. Click to cast, and if a fish bites, a timing mini-game determines success. " | |
"Different fish have different rarity and scores. Earn gold for upgrades (rods, bait). " | |
"Time/weather can affect which fish appear." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a single-player or AI-versus bingo game. A 5x5 grid with numbers 1-25 is randomized. " | |
"Players mark chosen numbers in turns. A line (horizontal, vertical, diagonal) is a 'bingo'. " | |
"Get 3 bingos first to win. Include a timer and record wins/losses." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a rhythm game with notes rising from the bottom. Press keys (D,F,J,K) at the right time. " | |
"Judge timing as Perfect, Good, or Miss. Show combos and final results (accuracy, combos, score). Include difficulty (note speed)." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a top-down 2D racing game. The player drives a car with arrow keys on a track. " | |
"Leaving the track slows the car. Add AI opponents, a 3-lap race mode, and a time attack mode. " | |
"Offer multiple cars with different speeds/handling." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a trivia quiz game with various categories. 4 multiple-choice answers, 30s limit per question. " | |
"Correct answers score points, wrong answers cost lives. Provide difficulty levels and hint items. " | |
"Show a summary of results at the end." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a shooting gallery with moving targets. Click to shoot. Targets move at various speeds/patterns. " | |
"Limited time and ammo. Special targets grant bonuses. Consecutive hits form combos. Increase target speed for difficulty." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a board game with a virtual dice (1-6) to move a piece around. Different board spaces trigger events: " | |
"move forward/back, skip turns, mini-games, etc. Players collect items to use. Up to 4 players or AI. " | |
"First to reach the goal or highest points wins." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a top-down zombie survival game. Move with WASD, aim and shoot with the mouse. " | |
"Zombies come in waves, increasing in number and speed. Implement ammo, health packs, bombs, and special zombie types. " | |
"Survive as long as possible." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a soccer penalty shootout game. The player sets direction and power to shoot, or chooses a direction for the goalkeeper. " | |
"5 rounds each, plus sudden death if tied. The AI goalkeeper can learn player tendencies. Support single-player and 2-player local mode." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a classic Minesweeper game. NxN grid, M mines. Left-click reveals a cell, right-click flags a mine. " | |
"Numbers show how many mines are adjacent. Reveal all safe cells to win; hitting a mine loses. " | |
"Offer beginner/intermediate/expert sizes and ensure the first click is safe." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a Connect Four game on a 7x6 grid. Players drop colored discs to form a line of 4 horizontally, vertically, or diagonally. " | |
"Alternate turns. First to connect 4 wins, or it's a draw if the board fills. Include AI and local 2-player modes." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a Scrabble-style word game. Each player has 7 letter tiles. Place them on the board to form words. " | |
"All new words must connect to existing ones. Each tile has a point value. Include bonus spaces (double letter, triple word). " | |
"Validate words against a dictionary, support 1-4 players or AI." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a 2D tank battle game. Move with WASD, aim and fire with the mouse. " | |
"Destructible terrain (brick, wood) and indestructible blocks (steel, water). Various weapons (shell, spread, laser) and power-ups. " | |
"Add multiple stages with AI enemies and increasing difficulty." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a gem-matching puzzle game. Swap adjacent gems to match 3. Matched gems vanish and new gems fall. " | |
"4+ matches create special gems with bigger explosions. Chain combos increase score. " | |
"Use limited moves or time, plus special objectives like clearing obstacles." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a single-tower defense game. The tower is in the center and auto-attacks incoming enemies. " | |
"Between waves, use earned resources to upgrade damage, speed, or range. " | |
"Waves get harder with different enemy types (fast, armored, splitting). Survive as long as possible." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a side-scrolling runner with zombies and obstacles. Press space to jump and S to slide. " | |
"Collect coins and power-ups (invincibility, magnet, slow). Occasionally fight a mini-boss zombie. " | |
"Earn points for distance, spend coins on upgrades like double-jump or extra health." | |
) | |
}, | |
{ | |
"description": ( | |
"Please build a top-down action RPG. Move with WASD, basic attack with the mouse, and use skills (keys 1-4). " | |
"Defeat monsters to gain XP and items, level up to improve stats. Equip weapons/armor, use a skill tree, and fight bosses. " | |
"Add simple quests and multiple zones." | |
) | |
}, | |
] | |
SystemPrompt = """ | |
# GameCraft System Prompt | |
## 1. Basic Info & Role | |
Your name is 'GameCraft'. You are a web game developer specialized in gameplay mechanics, interactive design, and performance optimization. | |
You build concise, efficient HTML/JS/CSS web-based games. | |
## 2. Core Tech Stack | |
- **Frontend**: HTML5, CSS3, JavaScript (ES6+) | |
- **Rendering**: Directly in browser | |
- **Code Style**: Vanilla JS first, minimal external libraries | |
## 3. Game Type Guidelines | |
### 3.1 Arcade/Action | |
- Simple collision detection | |
- Keyboard/touch optimization | |
- Basic scoring | |
### 3.2 Puzzle | |
- Clear rules & win conditions | |
- Basic difficulty levels | |
- Focus on core mechanics | |
### 3.3 Card/Board | |
- Simplified turn-based system | |
- Automate main rules | |
- Focus on core logic | |
### 3.4 Simulation | |
- Efficient state management | |
- Implement essential interactions | |
- Only critical elements | |
## 4. Emoji Usage 🎮 | |
- Use emojis for key UI elements (HP ❤️, coin 💰, timer ⏱️) | |
- Minimal, essential usage | |
## 5. Technical Implementation | |
### 5.1 Code Structure | |
- **Conciseness**: Keep code as short as possible, minimal comments | |
- **Modularity**: Split by function, avoid unnecessary abstraction | |
- **Optimization**: Focus on game loop & rendering | |
- **Size Limit**: Entire code under 200 lines | |
### 5.2 Performance | |
- Minimize DOM manipulation | |
- Remove unused variables | |
- Watch memory usage | |
### 5.3 Responsive | |
- Basic responsive layout | |
- Simple UI focusing on core features | |
## 6. External Libraries | |
- Use minimal libraries, only if necessary | |
- If used, load via CDN | |
## 7. Accessibility & Inclusion | |
- Only essential accessibility features | |
## 8. Constraints & Notes | |
- No external API calls | |
- Keep code minimal (<200 lines) | |
- Minimal comments (only what's necessary) | |
- Avoid unnecessary features | |
## 9. Output Format | |
- Provide code in an HTML code block only | |
- No extra explanations, just a single-file ready code | |
- Must be fully self-contained | |
## 10. Code Quality | |
- Efficiency & brevity are top priorities | |
- Focus on core gameplay mechanics | |
- Prefer basic functionality over complexity | |
- No redundant comments | |
- Single file, <200 lines total | |
## 11. Important: Code Generation Limit | |
- Must NOT exceed 200 lines | |
- Omit non-essential details | |
- If code grows too long, simplify or remove features | |
""" | |
# ------------------------ | |
# 2) Constants, Functions, Classes | |
# ------------------------ | |
class Role: | |
SYSTEM = "system" | |
USER = "user" | |
ASSISTANT = "assistant" | |
History = List[Tuple[str, str]] | |
Messages = List[Dict[str, str]] | |
IMAGE_CACHE = {} | |
def get_image_base64(image_path): | |
"""Read an image file into base64 and cache it.""" | |
if image_path in IMAGE_CACHE: | |
return IMAGE_CACHE[image_path] | |
try: | |
with open(image_path, "rb") as image_file: | |
encoded_string = base64.b64encode(image_file.read()).decode() | |
IMAGE_CACHE[image_path] = encoded_string | |
return encoded_string | |
except: | |
return IMAGE_CACHE.get('default.png', '') | |
def history_to_messages(history: History, system: str) -> Messages: | |
messages = [{'role': Role.SYSTEM, 'content': system}] | |
for h in history: | |
messages.append({'role': Role.USER, 'content': h[0]}) | |
messages.append({'role': Role.ASSISTANT, 'content': h[1]}) | |
return messages | |
def messages_to_history(messages: Messages) -> History: | |
assert messages[0]['role'] == Role.SYSTEM | |
history = [] | |
for q, r in zip(messages[1::2], messages[2::2]): | |
history.append([q['content'], r['content']]) | |
return history | |
# ------------------------ | |
# 3) API Setup | |
# ------------------------ | |
YOUR_ANTHROPIC_TOKEN = os.getenv('ANTHROPIC_API_KEY', '').strip() | |
claude_client = anthropic.Anthropic(api_key=YOUR_ANTHROPIC_TOKEN) | |
async def try_claude_api(system_message, claude_messages, timeout=15): | |
""" | |
Claude 3.7 Sonnet API call (streaming). | |
""" | |
try: | |
system_message_with_limit = ( | |
system_message | |
+ "\n\nAdditional note: The generated code must NOT exceed 200 lines. Keep it concise, minimal comments, essential only." | |
) | |
start_time = time.time() | |
with claude_client.messages.stream( | |
model="claude-3-7-sonnet-20250219", | |
max_tokens=19800, | |
system=system_message_with_limit, | |
messages=claude_messages, | |
temperature=0.3 | |
) as stream: | |
collected_content = "" | |
for chunk in stream: | |
current_time = time.time() | |
if current_time - start_time > timeout: | |
raise TimeoutError("Claude API timeout") | |
if chunk.type == "content_block_delta": | |
collected_content += chunk.delta.text | |
yield collected_content | |
await asyncio.sleep(0) | |
start_time = current_time | |
except Exception as e: | |
raise e | |
# ------------------------ | |
# 4) Templates | |
# ------------------------ | |
def load_json_data(): | |
data_list = [] | |
for item in DEMO_LIST: | |
data_list.append({ | |
"name": f"[Game] {item['description'][:25]}...", | |
"prompt": item['description'] | |
}) | |
return data_list | |
def create_template_html(title, items): | |
""" | |
Create simple HTML for game templates (no images). | |
""" | |
html_content = r""" | |
<style> | |
.prompt-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
gap: 16px; | |
padding: 12px; | |
} | |
.prompt-card { | |
background: white; | |
border: 1px solid #eee; | |
border-radius: 12px; | |
padding: 12px; | |
cursor: pointer; | |
box-shadow: 0 4px 8px rgba(0,0,0,0.05); | |
transition: all 0.3s ease; | |
} | |
.prompt-card:hover { | |
transform: translateY(-4px); | |
box-shadow: 0 6px 12px rgba(0,0,0,0.1); | |
} | |
.card-name { | |
font-weight: bold; | |
margin-bottom: 8px; | |
font-size: 13px; | |
color: #444; | |
} | |
.card-prompt { | |
font-size: 11px; | |
line-height: 1.4; | |
color: #666; | |
display: -webkit-box; | |
-webkit-line-clamp: 7; | |
-webkit-box-orient: vertical; | |
overflow: hidden; | |
height: 84px; | |
background-color: #f8f9fa; | |
padding: 8px; | |
border-radius: 6px; | |
} | |
</style> | |
<div class="prompt-grid"> | |
""" | |
import html as html_lib | |
for item in items: | |
card_html = f""" | |
<div class="prompt-card" onclick="copyToInput(this)" data-prompt="{html_lib.escape(item.get('prompt', ''))}"> | |
<div class="card-name">{html_lib.escape(item.get('name', ''))}</div> | |
<div class="card-prompt">{html_lib.escape(item.get('prompt', ''))}</div> | |
</div> | |
""" | |
html_content += card_html | |
html_content += r""" | |
</div> | |
<script> | |
function copyToInput(card) { | |
const prompt = card.dataset.prompt; | |
const textarea = document.querySelector('.ant-input-textarea-large textarea'); | |
if (textarea) { | |
textarea.value = prompt; | |
textarea.dispatchEvent(new Event('input', { bubbles: true })); | |
document.querySelector('.session-drawer .close-btn').click(); | |
} | |
} | |
</script> | |
""" | |
return gr.HTML(value=html_content) | |
def load_all_templates(): | |
return create_template_html("Available Game Templates", load_json_data()) | |
# ------------------------ | |
# 5) Deploy/Boost/Utils | |
# ------------------------ | |
def remove_code_block(text): | |
pattern = r'```html\s*([\s\S]+?)\s*```' | |
match = re.search(pattern, text, re.DOTALL) | |
if match: | |
return match.group(1).strip() | |
pattern = r'```(?:\w+)?\s*([\s\S]+?)\s*```' | |
match = re.search(pattern, text, re.DOTALL) | |
if match: | |
return match.group(1).strip() | |
text = re.sub(r'```html\s*', '', text) | |
text = re.sub(r'\s*```', '', text) | |
return text.strip() | |
def optimize_code(code: str) -> str: | |
"""Remove comments/whitespace if code exceeds 200 lines.""" | |
if not code or len(code.strip()) == 0: | |
return code | |
lines = code.split('\n') | |
if len(lines) <= 200: | |
return code | |
# Remove comments | |
comment_patterns = [ | |
r'/\*[\s\S]*?\*/', | |
r'//.*?$', | |
r'<!--[\s\S]*?-->' | |
] | |
cleaned_code = code | |
for pattern in comment_patterns: | |
cleaned_code = re.sub(pattern, '', cleaned_code, flags=re.MULTILINE) | |
# Remove extra blank lines | |
cleaned_lines = [] | |
empty_line_count = 0 | |
for line in cleaned_code.split('\n'): | |
if line.strip() == '': | |
empty_line_count += 1 | |
if empty_line_count <= 1: | |
cleaned_lines.append('') | |
else: | |
empty_line_count = 0 | |
cleaned_lines.append(line) | |
cleaned_code = '\n'.join(cleaned_lines) | |
# Remove console logs | |
cleaned_code = re.sub(r'console\.log\(.*?\);', '', cleaned_code, flags=re.MULTILINE) | |
# Remove double spaces | |
cleaned_code = re.sub(r' {2,}', ' ', cleaned_code) | |
return cleaned_code | |
def send_to_sandbox(code): | |
clean_code = remove_code_block(code) | |
clean_code = optimize_code(clean_code) | |
if clean_code.startswith('```html'): | |
clean_code = clean_code[7:].strip() | |
if clean_code.endswith('```'): | |
clean_code = clean_code[:-3].strip() | |
if not clean_code.strip().startswith('<!DOCTYPE') and not clean_code.strip().startswith('<html'): | |
clean_code = f"""<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Game Preview</title> | |
</head> | |
<body> | |
{clean_code} | |
</body> | |
</html>""" | |
encoded_html = base64.b64encode(clean_code.encode('utf-8')).decode('utf-8') | |
data_uri = f"data:text/html;charset=utf-8;base64,{encoded_html}" | |
return f'<iframe src="{data_uri}" width="100%" height="920px" style="border:none;"></iframe>' | |
def boost_prompt(prompt: str) -> str: | |
""" | |
Ask Claude to refine the prompt for clarity and conciseness, | |
ensuring minimal extraneous requirements. | |
""" | |
if not prompt: | |
return "" | |
boost_system_prompt = ( | |
"You are an expert prompt engineer for web game development. " | |
"Please analyze the given prompt and refine it for clarity and brevity, " | |
"while preserving the original intent. Focus on: \n" | |
"1) Clarifying core gameplay mechanics\n" | |
"2) Essential interactions only\n" | |
"3) Simple UI elements\n" | |
"4) Minimizing features to ensure code under 600 lines\n" | |
"5) Basic rules and win/lose conditions\n\n" | |
"Important: Exclude unneeded details, keep it under 600 lines of code overall, and be concise." | |
) | |
try: | |
# Single direct request to Claude | |
response = claude_client.messages.create( | |
model="claude-3-7-sonnet-20250219", | |
max_tokens=10000, | |
temperature=0.3, | |
messages=[ | |
{"role": "user", "content": f"Refine this game prompt while keeping it concise: {prompt}"} | |
], | |
system=boost_system_prompt | |
) | |
if hasattr(response, 'content') and len(response.content) > 0: | |
return response.content[0].text | |
return prompt | |
except Exception: | |
return prompt | |
def handle_boost(prompt: str): | |
try: | |
boosted_prompt = boost_prompt(prompt) | |
return boosted_prompt, gr.update(active_key="empty") | |
except Exception: | |
return prompt, gr.update(active_key="empty") | |
def history_render(history: History): | |
return gr.update(open=True), history | |
def execute_code(query: str): | |
"""Try to interpret the content as code and display it.""" | |
if not query or query.strip() == '': | |
return None, gr.update(active_key="empty") | |
try: | |
clean_code = remove_code_block(query) | |
if clean_code.startswith('```html'): | |
clean_code = clean_code[7:].strip() | |
if clean_code.endswith('```'): | |
clean_code = clean_code[:-3].strip() | |
if not (clean_code.strip().startswith('<!DOCTYPE') or clean_code.strip().startswith('<html')): | |
if not ('<body' in clean_code and '</body>' in clean_code): | |
clean_code = ( | |
"<!DOCTYPE html>\n<html>\n<head>\n" | |
' <meta charset="UTF-8">\n' | |
' <meta name="viewport" content="width=device-width, initial-scale=1.0">\n' | |
' <title>Game Preview</title>\n' | |
"</head>\n<body>\n" | |
+ clean_code | |
+ "\n</body>\n</html>" | |
) | |
return send_to_sandbox(clean_code), gr.update(active_key="render") | |
except Exception as e: | |
print(f"Execute code error: {str(e)}") | |
return None, gr.update(active_key="empty") | |
# ------------------------ | |
# 6) Demo Class | |
# ------------------------ | |
def deploy_and_show(code_md: str): | |
""" | |
Deploy button logic. | |
""" | |
global deploying_flag | |
# Prevent repeated clicks | |
if deploying_flag: | |
return ( | |
gr.update(value="⏳ Already deploying...", visible=True), | |
None, | |
gr.update(active_key="empty") | |
) | |
deploying_flag = True | |
clean = remove_code_block(code_md) | |
result = deploy_to_vercel(clean) | |
m = re.search(r"https?://[\w\.-]+\.vercel\.app", result) | |
if m: | |
url = m.group(0) | |
md_out = f"✅ **Deployment Complete!**\n\n➡️ [Open]({url})" | |
iframe = ( | |
f"<iframe src='{url}' width='100%' height='920px' style='border:none;'></iframe>" | |
) | |
deploying_flag = False | |
return ( | |
gr.update(value=md_out, visible=True), | |
iframe, | |
gr.update(active_key="render") | |
) | |
md_err = f"❌ **Deployment Failed**\n\n```\n{result}\n```" | |
deploying_flag = False | |
return ( | |
gr.update(value=md_err, visible=True), | |
None, | |
gr.update(active_key="empty") | |
) | |
class Demo: | |
def __init__(self): | |
pass | |
async def generation_code( | |
self, | |
query: Optional[str], | |
_setting: Dict[str, str], | |
_history: Optional[History] | |
): | |
""" | |
Create the game code from user prompt using only Claude. | |
""" | |
if not query or query.strip() == '': | |
query = random.choice(DEMO_LIST)['description'] | |
if _history is None: | |
_history = [] | |
# Additional constraints for the request | |
query = ( | |
"Please create the following game. Important requirements: \n" | |
"1. Keep code as concise as possible.\n" | |
"2. Omit unnecessary comments.\n" | |
"3. The code must NOT exceed 600 lines.\n" | |
"4. All code in one HTML file.\n" | |
"5. Implement core features only.\n\n" | |
f"Game request: {query}" | |
) | |
messages = history_to_messages(_history, _setting['system']) | |
system_message = messages[0]['content'] | |
# For Claude | |
claude_messages = [ | |
{ | |
"role": (msg["role"] if msg["role"] != "system" else "user"), | |
"content": msg["content"] | |
} | |
for msg in messages[1:] + [{"role": Role.USER, "content": query}] | |
if msg["content"].strip() != '' | |
] | |
try: | |
# Start streaming response | |
yield [ | |
"Generating code...", | |
_history, | |
None, | |
gr.update(active_key="loading"), | |
gr.update(open=True) | |
] | |
await asyncio.sleep(0) | |
collected_content = None | |
async for content in try_claude_api(system_message, claude_messages): | |
yield [ | |
content, | |
_history, | |
None, | |
gr.update(active_key="loading"), | |
gr.update(open=True) | |
] | |
await asyncio.sleep(0) | |
collected_content = content | |
if collected_content: | |
clean_code = remove_code_block(collected_content) | |
code_lines = clean_code.count('\n') + 1 | |
if code_lines > 700: | |
warning_msg = ( | |
f"⚠️ **Warning: Generated code is too long ({code_lines} lines).**\n" | |
"This may cause issues. Try simplifying the request:\n" | |
"1) Request a simpler game\n" | |
"2) Omit advanced features\n" | |
"```html\n" | |
+ clean_code[:2000] | |
+ "\n... (truncated) ..." | |
) | |
yield [ | |
warning_msg, | |
_history, | |
None, | |
gr.update(active_key="empty"), | |
gr.update(open=True) | |
] | |
else: | |
# Add final message to chat history | |
final_msg = { | |
"role": Role.ASSISTANT, | |
"content": collected_content | |
} | |
updated_messages = messages + [final_msg] | |
_history = messages_to_history(updated_messages) | |
yield [ | |
collected_content, | |
_history, | |
send_to_sandbox(clean_code), | |
gr.update(active_key="render"), | |
gr.update(open=True) | |
] | |
else: | |
raise ValueError("No content generated from Claude.") | |
except Exception as e: | |
raise ValueError(f"Error calling Claude: {str(e)}") | |
def clear_history(self): | |
return [] | |
#################################################### | |
# 1) deploy_to_vercel | |
#################################################### | |
def deploy_to_vercel(code: str): | |
"""Deploy code to Vercel and return the response.""" | |
print(f"[DEBUG] deploy_to_vercel() start. code length: {len(code) if code else 0}") | |
try: | |
if not code or len(code.strip()) < 10: | |
print("[DEBUG] No code to deploy (too short).") | |
return "No code to deploy." | |
# Hard-coded token for demonstration | |
token = "A8IFZmgW2cqA4yUNlLPnci0N" | |
if not token: | |
print("[DEBUG] Vercel token is not set.") | |
return "Vercel token is not set." | |
project_name = ''.join(random.choice(string.ascii_lowercase) for _ in range(6)) | |
print(f"[DEBUG] Generated project_name: {project_name}") | |
deploy_url = "https://api.vercel.com/v13/deployments" | |
headers = { | |
"Authorization": f"Bearer {token}", | |
"Content-Type": "application/json" | |
} | |
package_json = { | |
"name": project_name, | |
"version": "1.0.0", | |
"private": True, | |
"dependencies": {"vite": "^5.0.0"}, | |
"scripts": { | |
"dev": "vite", | |
"build": "echo 'No build needed' && mkdir -p dist && cp index.html dist/", | |
"preview": "vite preview" | |
} | |
} | |
files = [ | |
{"file": "index.html", "data": code}, | |
{"file": "package.json", "data": json.dumps(package_json, indent=2)} | |
] | |
project_settings = { | |
"buildCommand": "npm run build", | |
"outputDirectory": "dist", | |
"installCommand": "npm install", | |
"framework": None | |
} | |
deploy_data = { | |
"name": project_name, | |
"files": files, | |
"target": "production", | |
"projectSettings": project_settings | |
} | |
print("[DEBUG] Sending request to Vercel...") | |
deploy_response = requests.post(deploy_url, headers=headers, json=deploy_data) | |
print("[DEBUG] Response status_code:", deploy_response.status_code) | |
if deploy_response.status_code != 200: | |
print("[DEBUG] Deployment failed:", deploy_response.text) | |
return f"Deployment failed: {deploy_response.text}" | |
deployment_url = f"https://{project_name}.vercel.app" | |
print(f"[DEBUG] Deployment success -> URL: {deployment_url}") | |
time.sleep(5) | |
return ( | |
"✅ **Deployment complete!** \n" | |
"Your app is live at: \n" | |
f"[**{deployment_url}**]({deployment_url})" | |
) | |
except Exception as e: | |
print("[ERROR] deploy_to_vercel() exception:", e) | |
return f"Error during deployment: {str(e)}" | |
# ------------------------ | |
# (Legacy Deploy Handler) | |
# ------------------------ | |
def handle_deploy_legacy(code): | |
logger.debug(f"[handle_deploy_legacy] code length: {len(code) if code else 0}") | |
if not code or len(code.strip()) < 10: | |
logger.info("[handle_deploy_legacy] Not enough code.") | |
return "<div style='color:red;'>No code to deploy.</div>" | |
clean_code = remove_code_block(code) | |
result = deploy_to_vercel(clean_code) | |
logger.debug(f"[handle_deploy_legacy] deploy_to_vercel result: {result}") | |
match = re.search(r'https?://[\w.-]+\.vercel\.app', result) | |
if match: | |
deployment_url = match.group(0) | |
iframe_html = ( | |
f'<iframe src="{deployment_url}" ' | |
'width="100%" height="600px" style="border:none;" ' | |
'sandbox="allow-scripts allow-same-origin allow-popups"></iframe>' | |
) | |
logger.debug("[handle_deploy_legacy] returning iframe_html") | |
return iframe_html | |
logger.warning("[handle_deploy_legacy] No deployment URL found.") | |
safe_result = html.escape(result) | |
return f"<div style='color:red;'>Deployment URL not found.<br>Result: {safe_result}</div>" | |
# ------------------------ | |
# 8) Gradio / Modelscope UI | |
# ------------------------ | |
demo_instance = Demo() | |
theme = gr.themes.Soft( | |
primary_hue="blue", | |
secondary_hue="purple", | |
neutral_hue="slate", | |
spacing_size=gr.themes.sizes.spacing_md, | |
radius_size=gr.themes.sizes.radius_md, | |
text_size=gr.themes.sizes.text_md, | |
) | |
with gr.Blocks(css_paths=["app.css"], theme=theme) as demo: | |
gr.HTML(""" | |
<style> | |
.app-header{ text-align:center; margin-bottom:24px; } | |
.badge-row{ display:inline-flex; gap:8px; margin:8px 0; } | |
</style> | |
<div class="app-header"> | |
<h1>🎮 Vibe Game Craft</h1> | |
<div class="badge-row"> | |
<a href="https://huggingface.co/spaces/openfree/Vibe-Game" target="_blank"> | |
<img src="https://img.shields.io/static/v1?label=huggingface&message=Vibe%20Game%20Craft&color=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&style=for-the-badge" alt="HF Vibe badge"> | |
</a> | |
<a href="https://huggingface.co/spaces/openfree/Game-Gallery" target="_blank"> | |
<img src="https://img.shields.io/static/v1?label=huggingface&message=Game%20Gallery&color=%23800080&labelColor=%23ffa500&logo=huggingface&logoColor=%23ffff00&style=for-the-badge" alt="HF Gallery badge"> | |
</a> | |
<a href="https://discord.gg/openfreeai" target="_blank"> | |
<img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="Discord badge"> | |
</a> | |
</div> | |
<p>Enter a game description, and this tool will generate an HTML5/JS/CSS game via Claude. Preview or deploy it easily.</p> | |
</div> | |
""") | |
history = gr.State([]) | |
setting = gr.State({"system": SystemPrompt}) | |
deploy_status = gr.State({"is_deployed": False, "status": "", "url": "", "message": ""}) | |
with ms.Application() as app: | |
with antd.ConfigProvider(): | |
with antd.Drawer(open=False, title="View Code", placement="left", width="750px") as code_drawer: | |
code_output = legacy.Markdown() | |
with antd.Drawer(open=False, title="History", placement="left", width="900px") as history_drawer: | |
history_output = legacy.Chatbot(show_label=False, flushing=False, height=960, elem_classes="history_chatbot") | |
with antd.Drawer( | |
open=False, | |
title="Game Templates", | |
placement="right", | |
width="900px", | |
elem_classes="session-drawer" | |
) as session_drawer: | |
with antd.Flex(vertical=True, gap="middle"): | |
gr.Markdown("### Available Game Templates") | |
session_history = gr.HTML(elem_classes="session-history") | |
close_btn = antd.Button("Close", type="default", elem_classes="close-btn") | |
with antd.Row(gutter=[32, 12], align="top", elem_classes="equal-height-container"): | |
# Left Column | |
with antd.Col(span=24, md=16, elem_classes="equal-height-col"): | |
with ms.Div(elem_classes="right_panel panel"): | |
gr.HTML(""" | |
<div class="render_header"> | |
<span class="header_btn"></span><span class="header_btn"></span><span class="header_btn"></span> | |
</div> | |
""") | |
with antd.Tabs(active_key="empty", render_tab_bar="() => null") as state_tab: | |
with antd.Tabs.Item(key="empty"): | |
antd.Empty(description="Enter a description to generate a game", elem_classes="right_content") | |
with antd.Tabs.Item(key="loading"): | |
antd.Spin(True, tip="Generating game code...", size="large", elem_classes="right_content") | |
with antd.Tabs.Item(key="render"): | |
sandbox = gr.HTML(elem_classes="html_content") | |
# Right Column | |
with antd.Col(span=24, md=8, elem_classes="equal-height-col"): | |
with antd.Flex(vertical=True, gap="small", elem_classes="right-top-buttons"): | |
with antd.Flex(gap="small", elem_classes="setting-buttons", justify="space-between"): | |
codeBtn = antd.Button("View Code", type="default", elem_classes="code-btn") | |
historyBtn = antd.Button("History", type="default", elem_classes="history-btn") | |
template_btn = antd.Button("🎮 Templates", type="default", elem_classes="template-btn") | |
with antd.Flex(gap="small", justify="space-between", elem_classes="action-buttons"): | |
send_btn = antd.Button("Send", type="primary", size="large", elem_classes="send-btn") | |
enhance_btn = antd.Button("Enhance", type="default", size="large", elem_classes="boost-btn") | |
code_exec_btn = antd.Button("Code", type="default", size="large", elem_classes="execute-btn") | |
deploy_btn = antd.Button("Deploy", type="default", size="large", elem_classes="deploy-btn") | |
clear_btn = antd.Button("Clear", type="default", size="large", elem_classes="clear-btn") | |
with antd.Flex(vertical=True, gap="middle", wrap=True, elem_classes="input-panel"): | |
deploy_result_container = gr.Markdown(value="", visible=False) | |
input_text = antd.InputTextarea( | |
size="large", | |
allow_clear=True, | |
placeholder=random.choice(DEMO_LIST)['description'], | |
max_length=100000 | |
) | |
gr.HTML('<div class="help-text">💡 Describe your game here, e.g. "Please build a simple Tetris game."</div>') | |
# Drawer Toggles | |
codeBtn.click(lambda: gr.update(open=True), inputs=[], outputs=[code_drawer]) | |
code_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[code_drawer]) | |
historyBtn.click(history_render, inputs=[history], outputs=[history_drawer, history_output]) | |
history_drawer.close(lambda: gr.update(open=False), inputs=[], outputs=[history_drawer]) | |
template_btn.click( | |
fn=lambda: (gr.update(open=True), load_all_templates()), | |
outputs=[session_drawer, session_history], | |
queue=False | |
) | |
session_drawer.close(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history]) | |
close_btn.click(lambda: (gr.update(open=False), gr.HTML("")), outputs=[session_drawer, session_history]) | |
# Buttons | |
send_btn.click( | |
demo_instance.generation_code, | |
inputs=[input_text, setting, history], | |
outputs=[code_output, history, sandbox, state_tab, code_drawer] | |
) | |
clear_btn.click(demo_instance.clear_history, inputs=[], outputs=[history]) | |
enhance_btn.click(handle_boost, inputs=[input_text], outputs=[input_text, state_tab]) | |
code_exec_btn.click(execute_code, inputs=[input_text], outputs=[sandbox, state_tab]) | |
deploy_btn.click( | |
fn=deploy_and_show, | |
inputs=[code_output], | |
outputs=[ | |
deploy_result_container, | |
sandbox, | |
state_tab | |
] | |
) | |
# 9) Launch | |
if __name__ == "__main__": | |
try: | |
demo_instance = Demo() | |
demo.queue(default_concurrency_limit=20).launch(ssr_mode=False) | |
except Exception as e: | |
print(f"Initialization error: {e}") | |
raise | |