Spaces:
Sleeping
Sleeping
feat: complete AutoApp Builder - AI-powered HF Space generator
Browse files- .gitignore +12 -0
- Dockerfile +20 -0
- README.md +27 -6
- app/__init__.py +0 -0
- app/codegen/__init__.py +0 -0
- app/codegen/docker_generator.py +367 -0
- app/codegen/gradio_generator.py +444 -0
- app/codegen/readme_generator.py +216 -0
- app/codegen/repo_generator.py +333 -0
- app/engine/__init__.py +0 -0
- app/engine/app_planner.py +279 -0
- app/engine/model_recommender.py +192 -0
- app/main.py +326 -0
- app/static/app.js +211 -0
- app/templates/base.html +187 -0
- app/templates/home.html +236 -0
- app/templates/result.html +217 -0
- app/validators/__init__.py +0 -0
- app/validators/code_checker.py +191 -0
- requirements.txt +6 -0
.gitignore
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.egg-info/
|
| 5 |
+
dist/
|
| 6 |
+
build/
|
| 7 |
+
.env
|
| 8 |
+
.venv/
|
| 9 |
+
venv/
|
| 10 |
+
generated/
|
| 11 |
+
*.zip
|
| 12 |
+
.DS_Store
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
build-essential \
|
| 7 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 8 |
+
|
| 9 |
+
COPY requirements.txt .
|
| 10 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
RUN useradd -m -u 1000 appuser
|
| 15 |
+
RUN mkdir -p /app/generated && chown -R appuser:appuser /app
|
| 16 |
+
USER appuser
|
| 17 |
+
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -1,10 +1,31 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: AutoApp Builder
|
| 3 |
+
emoji: "\U0001F3D7\uFE0F"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
+
license: mit
|
| 10 |
+
short_description: Generate HF Spaces from natural language
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# AutoApp Builder
|
| 14 |
+
|
| 15 |
+
Generate complete, working Hugging Face Space repositories from natural language descriptions.
|
| 16 |
+
|
| 17 |
+
## Features
|
| 18 |
+
|
| 19 |
+
- **Smart SDK Selection** - Automatically chooses Gradio, Docker, or Static based on your app description
|
| 20 |
+
- **LLM-Powered Code Generation** - Uses state-of-the-art models to generate real, working code
|
| 21 |
+
- **Model Recommendations** - Suggests the best HF models for your use case
|
| 22 |
+
- **Code Preview** - Syntax-highlighted preview of all generated files
|
| 23 |
+
- **ZIP Download** - Download the complete repo ready to upload to HF Spaces
|
| 24 |
+
- **Iterative Editing** - Refine your generated app with follow-up prompts
|
| 25 |
+
|
| 26 |
+
## How It Works
|
| 27 |
+
|
| 28 |
+
1. Describe your app idea in natural language
|
| 29 |
+
2. AutoApp Builder analyzes your prompt and plans the architecture
|
| 30 |
+
3. Working code is generated for every file in the repo
|
| 31 |
+
4. Preview, edit, and download your complete Space
|
app/__init__.py
ADDED
|
File without changes
|
app/codegen/__init__.py
ADDED
|
File without changes
|
app/codegen/docker_generator.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Generate Docker-based (FastAPI) Space code using LLM with template fallbacks.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
import re
|
| 7 |
+
from huggingface_hub import InferenceClient
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
LLM_MODEL = "Qwen/Qwen2.5-72B-Instruct"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
DOCKER_TEMPLATES = {
|
| 14 |
+
"rest_api": {
|
| 15 |
+
"app.py": '''from fastapi import FastAPI, HTTPException
|
| 16 |
+
from fastapi.responses import HTMLResponse
|
| 17 |
+
from pydantic import BaseModel
|
| 18 |
+
from huggingface_hub import InferenceClient
|
| 19 |
+
import os
|
| 20 |
+
|
| 21 |
+
app = FastAPI(
|
| 22 |
+
title="{title}",
|
| 23 |
+
description="{description}",
|
| 24 |
+
version="1.0.0",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
client = InferenceClient("{model_id}", token=os.environ.get("HF_TOKEN"))
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class TextRequest(BaseModel):
|
| 31 |
+
text: str
|
| 32 |
+
max_tokens: int = 512
|
| 33 |
+
temperature: float = 0.7
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class TextResponse(BaseModel):
|
| 37 |
+
result: str
|
| 38 |
+
model: str
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class SummarizeRequest(BaseModel):
|
| 42 |
+
text: str
|
| 43 |
+
max_length: int = 150
|
| 44 |
+
min_length: int = 30
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@app.get("/", response_class=HTMLResponse)
|
| 48 |
+
async def root():
|
| 49 |
+
return """
|
| 50 |
+
<html>
|
| 51 |
+
<head><title>{title}</title></head>
|
| 52 |
+
<body style="font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px;">
|
| 53 |
+
<h1>{title}</h1>
|
| 54 |
+
<p>{description}</p>
|
| 55 |
+
<h2>Endpoints</h2>
|
| 56 |
+
<ul>
|
| 57 |
+
<li><code>POST /generate</code> - Generate text</li>
|
| 58 |
+
<li><code>POST /summarize</code> - Summarize text</li>
|
| 59 |
+
<li><code>GET /health</code> - Health check</li>
|
| 60 |
+
<li><code>GET /docs</code> - API documentation</li>
|
| 61 |
+
</ul>
|
| 62 |
+
</body>
|
| 63 |
+
</html>
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
@app.post("/generate", response_model=TextResponse)
|
| 68 |
+
async def generate_text(request: TextRequest):
|
| 69 |
+
try:
|
| 70 |
+
messages = [{{"role": "user", "content": request.text}}]
|
| 71 |
+
response = client.chat_completion(
|
| 72 |
+
messages,
|
| 73 |
+
max_tokens=request.max_tokens,
|
| 74 |
+
temperature=request.temperature,
|
| 75 |
+
)
|
| 76 |
+
return TextResponse(
|
| 77 |
+
result=response.choices[0].message.content,
|
| 78 |
+
model="{model_id}",
|
| 79 |
+
)
|
| 80 |
+
except Exception as e:
|
| 81 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
@app.post("/summarize")
|
| 85 |
+
async def summarize_text(request: SummarizeRequest):
|
| 86 |
+
try:
|
| 87 |
+
prompt = f"Summarize the following text in a concise way:\\n\\n{{request.text}}"
|
| 88 |
+
messages = [{{"role": "user", "content": prompt}}]
|
| 89 |
+
response = client.chat_completion(messages, max_tokens=request.max_length)
|
| 90 |
+
return {{"summary": response.choices[0].message.content, "model": "{model_id}"}}
|
| 91 |
+
except Exception as e:
|
| 92 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
@app.get("/health")
|
| 96 |
+
async def health_check():
|
| 97 |
+
return {{"status": "healthy", "model": "{model_id}"}}
|
| 98 |
+
''',
|
| 99 |
+
"Dockerfile": '''FROM python:3.11-slim
|
| 100 |
+
|
| 101 |
+
WORKDIR /app
|
| 102 |
+
|
| 103 |
+
COPY requirements.txt .
|
| 104 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 105 |
+
|
| 106 |
+
COPY . .
|
| 107 |
+
|
| 108 |
+
RUN useradd -m -u 1000 appuser
|
| 109 |
+
USER appuser
|
| 110 |
+
|
| 111 |
+
EXPOSE 7860
|
| 112 |
+
|
| 113 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 114 |
+
''',
|
| 115 |
+
"requirements.txt": "fastapi==0.115.6\nuvicorn[standard]==0.34.0\nhuggingface-hub==0.27.1\n",
|
| 116 |
+
},
|
| 117 |
+
|
| 118 |
+
"generic_docker": {
|
| 119 |
+
"app.py": '''from fastapi import FastAPI, HTTPException
|
| 120 |
+
from fastapi.responses import HTMLResponse
|
| 121 |
+
from pydantic import BaseModel
|
| 122 |
+
from huggingface_hub import InferenceClient
|
| 123 |
+
import os
|
| 124 |
+
|
| 125 |
+
app = FastAPI(title="{title}", version="1.0.0")
|
| 126 |
+
|
| 127 |
+
client = InferenceClient("{model_id}", token=os.environ.get("HF_TOKEN"))
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
class QueryRequest(BaseModel):
|
| 131 |
+
query: str
|
| 132 |
+
max_tokens: int = 512
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
@app.get("/", response_class=HTMLResponse)
|
| 136 |
+
async def root():
|
| 137 |
+
return """
|
| 138 |
+
<html>
|
| 139 |
+
<head><title>{title}</title>
|
| 140 |
+
<style>
|
| 141 |
+
body {{ font-family: system-ui; max-width: 900px; margin: 50px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }}
|
| 142 |
+
h1 {{ color: #38bdf8; }}
|
| 143 |
+
code {{ background: #1e293b; padding: 2px 8px; border-radius: 4px; }}
|
| 144 |
+
a {{ color: #38bdf8; }}
|
| 145 |
+
</style>
|
| 146 |
+
</head>
|
| 147 |
+
<body>
|
| 148 |
+
<h1>{title}</h1>
|
| 149 |
+
<p>{description}</p>
|
| 150 |
+
<h2>API Documentation</h2>
|
| 151 |
+
<p>Visit <a href="/docs">/docs</a> for interactive API documentation.</p>
|
| 152 |
+
<h2>Endpoints</h2>
|
| 153 |
+
<ul>
|
| 154 |
+
<li><code>POST /query</code> - Process a query</li>
|
| 155 |
+
<li><code>GET /health</code> - Health check</li>
|
| 156 |
+
</ul>
|
| 157 |
+
</body>
|
| 158 |
+
</html>
|
| 159 |
+
"""
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.post("/query")
|
| 163 |
+
async def process_query(request: QueryRequest):
|
| 164 |
+
try:
|
| 165 |
+
messages = [{{"role": "user", "content": request.query}}]
|
| 166 |
+
response = client.chat_completion(messages, max_tokens=request.max_tokens)
|
| 167 |
+
return {{
|
| 168 |
+
"response": response.choices[0].message.content,
|
| 169 |
+
"model": "{model_id}",
|
| 170 |
+
}}
|
| 171 |
+
except Exception as e:
|
| 172 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
@app.get("/health")
|
| 176 |
+
async def health():
|
| 177 |
+
return {{"status": "healthy"}}
|
| 178 |
+
''',
|
| 179 |
+
"Dockerfile": '''FROM python:3.11-slim
|
| 180 |
+
|
| 181 |
+
WORKDIR /app
|
| 182 |
+
|
| 183 |
+
COPY requirements.txt .
|
| 184 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 185 |
+
|
| 186 |
+
COPY . .
|
| 187 |
+
|
| 188 |
+
RUN useradd -m -u 1000 appuser
|
| 189 |
+
USER appuser
|
| 190 |
+
|
| 191 |
+
EXPOSE 7860
|
| 192 |
+
|
| 193 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 194 |
+
''',
|
| 195 |
+
"requirements.txt": "fastapi==0.115.6\nuvicorn[standard]==0.34.0\nhuggingface-hub==0.27.1\n",
|
| 196 |
+
},
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
class DockerGenerator:
|
| 201 |
+
"""Generate Docker-based Space files."""
|
| 202 |
+
|
| 203 |
+
def __init__(self):
|
| 204 |
+
self._client = None
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def client(self) -> InferenceClient:
|
| 208 |
+
if self._client is None:
|
| 209 |
+
token = os.environ.get("HF_TOKEN", None)
|
| 210 |
+
self._client = InferenceClient(LLM_MODEL, token=token)
|
| 211 |
+
return self._client
|
| 212 |
+
|
| 213 |
+
def generate(self, plan: dict, prompt: str) -> dict:
|
| 214 |
+
"""
|
| 215 |
+
Generate all files for a Docker-based Space.
|
| 216 |
+
Returns dict of {filename: content}.
|
| 217 |
+
"""
|
| 218 |
+
model_id = self._get_model_id(plan)
|
| 219 |
+
title = plan.get("title", "My API")
|
| 220 |
+
description = plan.get("description", "")
|
| 221 |
+
template_key = plan.get("template_key", "generic_docker")
|
| 222 |
+
|
| 223 |
+
# Try LLM generation first
|
| 224 |
+
try:
|
| 225 |
+
files = self._generate_with_llm(plan, prompt, model_id, title, description)
|
| 226 |
+
if files and "app.py" in files and len(files["app.py"]) > 100:
|
| 227 |
+
# Ensure required files exist
|
| 228 |
+
if "Dockerfile" not in files:
|
| 229 |
+
files["Dockerfile"] = DOCKER_TEMPLATES["generic_docker"]["Dockerfile"]
|
| 230 |
+
if "requirements.txt" not in files:
|
| 231 |
+
files["requirements.txt"] = DOCKER_TEMPLATES["generic_docker"]["requirements.txt"]
|
| 232 |
+
return files
|
| 233 |
+
except Exception:
|
| 234 |
+
pass
|
| 235 |
+
|
| 236 |
+
# Fallback to templates
|
| 237 |
+
template = DOCKER_TEMPLATES.get(template_key, DOCKER_TEMPLATES["generic_docker"])
|
| 238 |
+
files = {}
|
| 239 |
+
for filename, content in template.items():
|
| 240 |
+
files[filename] = content.format(
|
| 241 |
+
model_id=model_id,
|
| 242 |
+
title=title,
|
| 243 |
+
description=description,
|
| 244 |
+
)
|
| 245 |
+
return files
|
| 246 |
+
|
| 247 |
+
def _generate_with_llm(
|
| 248 |
+
self, plan: dict, prompt: str, model_id: str, title: str, description: str
|
| 249 |
+
) -> dict:
|
| 250 |
+
"""Use LLM to generate custom Docker app files."""
|
| 251 |
+
system_prompt = """You are an expert Python developer specializing in FastAPI applications for Hugging Face Docker Spaces.
|
| 252 |
+
Generate complete, production-ready code files.
|
| 253 |
+
|
| 254 |
+
Rules:
|
| 255 |
+
1. Output files in this exact format - each file delimited by markers:
|
| 256 |
+
=== FILENAME: app.py ===
|
| 257 |
+
(file content)
|
| 258 |
+
=== FILENAME: requirements.txt ===
|
| 259 |
+
(file content)
|
| 260 |
+
=== FILENAME: Dockerfile ===
|
| 261 |
+
(file content)
|
| 262 |
+
2. Use FastAPI for the web framework.
|
| 263 |
+
3. Use `huggingface_hub.InferenceClient` for model inference.
|
| 264 |
+
4. Include a root GET endpoint that returns an HTML page describing the API.
|
| 265 |
+
5. Include a /health endpoint.
|
| 266 |
+
6. Include proper error handling.
|
| 267 |
+
7. The Dockerfile should use python:3.11-slim, create a non-root user, and expose port 7860.
|
| 268 |
+
8. Use uvicorn to serve the app on 0.0.0.0:7860.
|
| 269 |
+
9. Do NOT use transformers, torch, or tensorflow."""
|
| 270 |
+
|
| 271 |
+
user_prompt = f"""Generate a complete Docker-based HF Space for this request:
|
| 272 |
+
|
| 273 |
+
USER REQUEST: {prompt}
|
| 274 |
+
|
| 275 |
+
APP PLAN:
|
| 276 |
+
- Type: {plan.get('app_type', 'custom')}
|
| 277 |
+
- Model: {model_id}
|
| 278 |
+
- Model Task: {plan.get('model_task', 'text-generation')}
|
| 279 |
+
- Title: {title}
|
| 280 |
+
- Description: {description}
|
| 281 |
+
- Extra Features: {', '.join(plan.get('extra_features', []))}
|
| 282 |
+
|
| 283 |
+
Generate the files (app.py, requirements.txt, Dockerfile):"""
|
| 284 |
+
|
| 285 |
+
response = self.client.chat_completion(
|
| 286 |
+
messages=[
|
| 287 |
+
{"role": "system", "content": system_prompt},
|
| 288 |
+
{"role": "user", "content": user_prompt},
|
| 289 |
+
],
|
| 290 |
+
max_tokens=4096,
|
| 291 |
+
temperature=0.3,
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
raw = response.choices[0].message.content
|
| 295 |
+
return self._parse_files(raw)
|
| 296 |
+
|
| 297 |
+
def _parse_files(self, text: str) -> dict:
|
| 298 |
+
"""Parse LLM output into a dict of filename -> content."""
|
| 299 |
+
files = {}
|
| 300 |
+
|
| 301 |
+
# Try marker-based parsing first
|
| 302 |
+
parts = re.split(r"===\s*FILENAME:\s*(.+?)\s*===", text)
|
| 303 |
+
if len(parts) >= 3:
|
| 304 |
+
for i in range(1, len(parts), 2):
|
| 305 |
+
filename = parts[i].strip()
|
| 306 |
+
content = parts[i + 1].strip() if i + 1 < len(parts) else ""
|
| 307 |
+
# Remove markdown code fences if present
|
| 308 |
+
content = re.sub(r"^```\w*\n?", "", content)
|
| 309 |
+
content = re.sub(r"\n?```$", "", content)
|
| 310 |
+
files[filename] = content.strip() + "\n"
|
| 311 |
+
|
| 312 |
+
# Try code block parsing as fallback
|
| 313 |
+
if not files:
|
| 314 |
+
blocks = re.findall(
|
| 315 |
+
r"(?:#|//)\s*(\S+\.(?:py|txt|dockerfile))\s*\n```\w*\n(.*?)```",
|
| 316 |
+
text, re.DOTALL | re.IGNORECASE,
|
| 317 |
+
)
|
| 318 |
+
for name, content in blocks:
|
| 319 |
+
files[name.strip()] = content.strip() + "\n"
|
| 320 |
+
|
| 321 |
+
return files
|
| 322 |
+
|
| 323 |
+
def _get_model_id(self, plan: dict) -> str:
|
| 324 |
+
models = plan.get("recommended_models", [])
|
| 325 |
+
if models:
|
| 326 |
+
return models[0]["id"]
|
| 327 |
+
return "Qwen/Qwen2.5-7B-Instruct"
|
| 328 |
+
|
| 329 |
+
def edit(self, plan: dict, current_files: dict, edit_prompt: str) -> dict:
|
| 330 |
+
"""Edit existing Docker app files."""
|
| 331 |
+
try:
|
| 332 |
+
app_code = current_files.get("app.py", "")
|
| 333 |
+
system_prompt = """You are an expert Python developer. Modify the existing FastAPI app code according to the edit request.
|
| 334 |
+
Output ONLY the updated Python code for app.py, no explanations or markdown fences.
|
| 335 |
+
Keep all existing functionality unless explicitly asked to remove something.
|
| 336 |
+
Use huggingface_hub.InferenceClient for model inference."""
|
| 337 |
+
|
| 338 |
+
user_prompt = f"""EXISTING app.py:
|
| 339 |
+
```python
|
| 340 |
+
{app_code}
|
| 341 |
+
```
|
| 342 |
+
|
| 343 |
+
EDIT REQUEST: {edit_prompt}
|
| 344 |
+
|
| 345 |
+
Return the complete updated app.py:"""
|
| 346 |
+
|
| 347 |
+
response = self.client.chat_completion(
|
| 348 |
+
messages=[
|
| 349 |
+
{"role": "system", "content": system_prompt},
|
| 350 |
+
{"role": "user", "content": user_prompt},
|
| 351 |
+
],
|
| 352 |
+
max_tokens=4096,
|
| 353 |
+
temperature=0.2,
|
| 354 |
+
)
|
| 355 |
+
|
| 356 |
+
raw = response.choices[0].message.content
|
| 357 |
+
code = re.sub(r"^```\w*\n?", "", raw.strip())
|
| 358 |
+
code = re.sub(r"\n?```$", "", code)
|
| 359 |
+
|
| 360 |
+
if code and len(code) > 50:
|
| 361 |
+
updated = dict(current_files)
|
| 362 |
+
updated["app.py"] = code.strip() + "\n"
|
| 363 |
+
return updated
|
| 364 |
+
except Exception:
|
| 365 |
+
pass
|
| 366 |
+
|
| 367 |
+
return current_files
|
app/codegen/gradio_generator.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Generate Gradio app code using the HF Inference API (LLM-powered).
|
| 3 |
+
Falls back to template-based generation if the API is unavailable.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
from huggingface_hub import InferenceClient
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
LLM_MODEL = "Qwen/Qwen2.5-72B-Instruct"
|
| 12 |
+
|
| 13 |
+
# Pre-built templates for common app types (used as fallback and as LLM context)
|
| 14 |
+
GRADIO_TEMPLATES = {
|
| 15 |
+
"chatbot": '''import gradio as gr
|
| 16 |
+
from huggingface_hub import InferenceClient
|
| 17 |
+
|
| 18 |
+
client = InferenceClient("{model_id}")
|
| 19 |
+
|
| 20 |
+
def respond(message, history, system_message, max_tokens, temperature, top_p):
|
| 21 |
+
messages = [{"role": "system", "content": system_message}]
|
| 22 |
+
for user_msg, bot_msg in history:
|
| 23 |
+
if user_msg:
|
| 24 |
+
messages.append({"role": "user", "content": user_msg})
|
| 25 |
+
if bot_msg:
|
| 26 |
+
messages.append({"role": "assistant", "content": bot_msg})
|
| 27 |
+
messages.append({"role": "user", "content": message})
|
| 28 |
+
|
| 29 |
+
response = ""
|
| 30 |
+
for chunk in client.chat_completion(
|
| 31 |
+
messages,
|
| 32 |
+
max_tokens=max_tokens,
|
| 33 |
+
stream=True,
|
| 34 |
+
temperature=temperature,
|
| 35 |
+
top_p=top_p,
|
| 36 |
+
):
|
| 37 |
+
token = chunk.choices[0].delta.content or ""
|
| 38 |
+
response += token
|
| 39 |
+
yield response
|
| 40 |
+
|
| 41 |
+
demo = gr.ChatInterface(
|
| 42 |
+
respond,
|
| 43 |
+
additional_inputs=[
|
| 44 |
+
gr.Textbox(value="You are a helpful assistant.", label="System message"),
|
| 45 |
+
gr.Slider(minimum=1, maximum=2048, value=512, step=1, label="Max tokens"),
|
| 46 |
+
gr.Slider(minimum=0.1, maximum=2.0, value=0.7, step=0.1, label="Temperature"),
|
| 47 |
+
gr.Slider(minimum=0.1, maximum=1.0, value=0.95, step=0.05, label="Top-p"),
|
| 48 |
+
],
|
| 49 |
+
title="{title}",
|
| 50 |
+
description="{description}",
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
if __name__ == "__main__":
|
| 54 |
+
demo.launch()
|
| 55 |
+
''',
|
| 56 |
+
|
| 57 |
+
"image_classifier": '''import gradio as gr
|
| 58 |
+
from huggingface_hub import InferenceClient
|
| 59 |
+
import json
|
| 60 |
+
|
| 61 |
+
client = InferenceClient("{model_id}")
|
| 62 |
+
|
| 63 |
+
def classify_image(image):
|
| 64 |
+
if image is None:
|
| 65 |
+
return {{}}
|
| 66 |
+
result = client.image_classification(image)
|
| 67 |
+
return {{item["label"]: round(item["score"], 4) for item in result}}
|
| 68 |
+
|
| 69 |
+
demo = gr.Interface(
|
| 70 |
+
fn=classify_image,
|
| 71 |
+
inputs=gr.Image(type="filepath", label="Upload Image"),
|
| 72 |
+
outputs=gr.Label(num_top_classes=5, label="Predictions"),
|
| 73 |
+
title="{title}",
|
| 74 |
+
description="{description}",
|
| 75 |
+
examples=[],
|
| 76 |
+
allow_flagging="never",
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if __name__ == "__main__":
|
| 80 |
+
demo.launch()
|
| 81 |
+
''',
|
| 82 |
+
|
| 83 |
+
"text_summarizer": '''import gradio as gr
|
| 84 |
+
from huggingface_hub import InferenceClient
|
| 85 |
+
|
| 86 |
+
client = InferenceClient("{model_id}")
|
| 87 |
+
|
| 88 |
+
def summarize(text, max_length, min_length):
|
| 89 |
+
if not text.strip():
|
| 90 |
+
return "Please enter some text to summarize."
|
| 91 |
+
result = client.summarization(
|
| 92 |
+
text,
|
| 93 |
+
parameters={{"max_length": int(max_length), "min_length": int(min_length)}},
|
| 94 |
+
)
|
| 95 |
+
return result["summary_text"] if isinstance(result, dict) else result[0]["summary_text"]
|
| 96 |
+
|
| 97 |
+
demo = gr.Interface(
|
| 98 |
+
fn=summarize,
|
| 99 |
+
inputs=[
|
| 100 |
+
gr.Textbox(lines=10, placeholder="Paste your text here...", label="Input Text"),
|
| 101 |
+
gr.Slider(50, 500, value=150, step=10, label="Max Summary Length"),
|
| 102 |
+
gr.Slider(10, 100, value=30, step=5, label="Min Summary Length"),
|
| 103 |
+
],
|
| 104 |
+
outputs=gr.Textbox(lines=5, label="Summary"),
|
| 105 |
+
title="{title}",
|
| 106 |
+
description="{description}",
|
| 107 |
+
allow_flagging="never",
|
| 108 |
+
)
|
| 109 |
+
|
| 110 |
+
if __name__ == "__main__":
|
| 111 |
+
demo.launch()
|
| 112 |
+
''',
|
| 113 |
+
|
| 114 |
+
"sentiment_analyzer": '''import gradio as gr
|
| 115 |
+
from huggingface_hub import InferenceClient
|
| 116 |
+
|
| 117 |
+
client = InferenceClient("{model_id}")
|
| 118 |
+
|
| 119 |
+
def analyze_sentiment(text):
|
| 120 |
+
if not text.strip():
|
| 121 |
+
return {{}}, ""
|
| 122 |
+
result = client.text_classification(text)
|
| 123 |
+
scores = {{item["label"]: round(item["score"], 4) for item in result}}
|
| 124 |
+
top_label = max(scores, key=scores.get)
|
| 125 |
+
explanation = f"The text is predominantly **{{top_label}}** (confidence: {{scores[top_label]:.1%}})"
|
| 126 |
+
return scores, explanation
|
| 127 |
+
|
| 128 |
+
demo = gr.Interface(
|
| 129 |
+
fn=analyze_sentiment,
|
| 130 |
+
inputs=gr.Textbox(lines=5, placeholder="Enter text to analyze...", label="Input Text"),
|
| 131 |
+
outputs=[
|
| 132 |
+
gr.Label(label="Sentiment Scores"),
|
| 133 |
+
gr.Markdown(label="Analysis"),
|
| 134 |
+
],
|
| 135 |
+
title="{title}",
|
| 136 |
+
description="{description}",
|
| 137 |
+
allow_flagging="never",
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
if __name__ == "__main__":
|
| 141 |
+
demo.launch()
|
| 142 |
+
''',
|
| 143 |
+
|
| 144 |
+
"text_generator": '''import gradio as gr
|
| 145 |
+
from huggingface_hub import InferenceClient
|
| 146 |
+
|
| 147 |
+
client = InferenceClient("{model_id}")
|
| 148 |
+
|
| 149 |
+
def generate_text(prompt, max_tokens, temperature, top_p):
|
| 150 |
+
if not prompt.strip():
|
| 151 |
+
return "Please enter a prompt."
|
| 152 |
+
messages = [{{"role": "user", "content": prompt}}]
|
| 153 |
+
response = ""
|
| 154 |
+
for chunk in client.chat_completion(
|
| 155 |
+
messages, max_tokens=int(max_tokens), stream=True,
|
| 156 |
+
temperature=temperature, top_p=top_p,
|
| 157 |
+
):
|
| 158 |
+
token = chunk.choices[0].delta.content or ""
|
| 159 |
+
response += token
|
| 160 |
+
yield response
|
| 161 |
+
|
| 162 |
+
demo = gr.Interface(
|
| 163 |
+
fn=generate_text,
|
| 164 |
+
inputs=[
|
| 165 |
+
gr.Textbox(lines=3, placeholder="Enter your prompt...", label="Prompt"),
|
| 166 |
+
gr.Slider(50, 2048, value=512, step=50, label="Max Tokens"),
|
| 167 |
+
gr.Slider(0.1, 2.0, value=0.7, step=0.1, label="Temperature"),
|
| 168 |
+
gr.Slider(0.1, 1.0, value=0.95, step=0.05, label="Top-p"),
|
| 169 |
+
],
|
| 170 |
+
outputs=gr.Textbox(lines=10, label="Generated Text"),
|
| 171 |
+
title="{title}",
|
| 172 |
+
description="{description}",
|
| 173 |
+
allow_flagging="never",
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
if __name__ == "__main__":
|
| 177 |
+
demo.launch()
|
| 178 |
+
''',
|
| 179 |
+
|
| 180 |
+
"translator": '''import gradio as gr
|
| 181 |
+
from huggingface_hub import InferenceClient
|
| 182 |
+
|
| 183 |
+
client = InferenceClient("{model_id}")
|
| 184 |
+
|
| 185 |
+
def translate(text, target_language):
|
| 186 |
+
if not text.strip():
|
| 187 |
+
return "Please enter text to translate."
|
| 188 |
+
prompt = f"Translate the following text to {{target_language}}:\\n\\n{{text}}"
|
| 189 |
+
messages = [{{"role": "user", "content": prompt}}]
|
| 190 |
+
result = client.chat_completion(messages, max_tokens=1024)
|
| 191 |
+
return result.choices[0].message.content
|
| 192 |
+
|
| 193 |
+
LANGUAGES = ["French", "Spanish", "German", "Italian", "Portuguese",
|
| 194 |
+
"Chinese", "Japanese", "Korean", "Arabic", "Hindi", "Russian"]
|
| 195 |
+
|
| 196 |
+
demo = gr.Interface(
|
| 197 |
+
fn=translate,
|
| 198 |
+
inputs=[
|
| 199 |
+
gr.Textbox(lines=5, placeholder="Enter text to translate...", label="Input Text"),
|
| 200 |
+
gr.Dropdown(choices=LANGUAGES, value="French", label="Target Language"),
|
| 201 |
+
],
|
| 202 |
+
outputs=gr.Textbox(lines=5, label="Translation"),
|
| 203 |
+
title="{title}",
|
| 204 |
+
description="{description}",
|
| 205 |
+
allow_flagging="never",
|
| 206 |
+
)
|
| 207 |
+
|
| 208 |
+
if __name__ == "__main__":
|
| 209 |
+
demo.launch()
|
| 210 |
+
''',
|
| 211 |
+
|
| 212 |
+
"question_answering": '''import gradio as gr
|
| 213 |
+
from huggingface_hub import InferenceClient
|
| 214 |
+
|
| 215 |
+
client = InferenceClient("{model_id}")
|
| 216 |
+
|
| 217 |
+
def answer_question(context, question):
|
| 218 |
+
if not context.strip() or not question.strip():
|
| 219 |
+
return "Please provide both context and a question."
|
| 220 |
+
result = client.question_answering(question=question, context=context)
|
| 221 |
+
answer = result["answer"]
|
| 222 |
+
score = result["score"]
|
| 223 |
+
return f"**Answer:** {{answer}}\\n\\n**Confidence:** {{score:.1%}}"
|
| 224 |
+
|
| 225 |
+
demo = gr.Interface(
|
| 226 |
+
fn=answer_question,
|
| 227 |
+
inputs=[
|
| 228 |
+
gr.Textbox(lines=8, placeholder="Paste the context here...", label="Context"),
|
| 229 |
+
gr.Textbox(lines=2, placeholder="Ask a question...", label="Question"),
|
| 230 |
+
],
|
| 231 |
+
outputs=gr.Markdown(label="Answer"),
|
| 232 |
+
title="{title}",
|
| 233 |
+
description="{description}",
|
| 234 |
+
allow_flagging="never",
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if __name__ == "__main__":
|
| 238 |
+
demo.launch()
|
| 239 |
+
''',
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
class GradioGenerator:
|
| 244 |
+
"""Generate Gradio app.py code for a given plan."""
|
| 245 |
+
|
| 246 |
+
def __init__(self):
|
| 247 |
+
self._client = None
|
| 248 |
+
|
| 249 |
+
@property
|
| 250 |
+
def client(self) -> InferenceClient:
|
| 251 |
+
if self._client is None:
|
| 252 |
+
token = os.environ.get("HF_TOKEN", None)
|
| 253 |
+
self._client = InferenceClient(LLM_MODEL, token=token)
|
| 254 |
+
return self._client
|
| 255 |
+
|
| 256 |
+
def generate(self, plan: dict, prompt: str) -> str:
|
| 257 |
+
"""Generate app.py content for a Gradio Space."""
|
| 258 |
+
template_key = plan.get("template_key")
|
| 259 |
+
model_id = self._get_model_id(plan)
|
| 260 |
+
title = plan.get("title", "My App")
|
| 261 |
+
description = plan.get("description", "")
|
| 262 |
+
|
| 263 |
+
# First try LLM generation for best results
|
| 264 |
+
try:
|
| 265 |
+
code = self._generate_with_llm(plan, prompt, model_id, title, description)
|
| 266 |
+
if code and len(code) > 100:
|
| 267 |
+
return code
|
| 268 |
+
except Exception:
|
| 269 |
+
pass
|
| 270 |
+
|
| 271 |
+
# Fallback to template-based generation
|
| 272 |
+
if template_key and template_key in GRADIO_TEMPLATES:
|
| 273 |
+
return GRADIO_TEMPLATES[template_key].format(
|
| 274 |
+
model_id=model_id,
|
| 275 |
+
title=title,
|
| 276 |
+
description=description,
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
# Final fallback: generic Gradio app
|
| 280 |
+
return self._generate_generic(plan, model_id, title, description)
|
| 281 |
+
|
| 282 |
+
def _generate_with_llm(
|
| 283 |
+
self, plan: dict, prompt: str, model_id: str, title: str, description: str
|
| 284 |
+
) -> str:
|
| 285 |
+
"""Use the LLM to generate custom Gradio app code."""
|
| 286 |
+
template_key = plan.get("template_key")
|
| 287 |
+
reference_code = ""
|
| 288 |
+
if template_key and template_key in GRADIO_TEMPLATES:
|
| 289 |
+
reference_code = GRADIO_TEMPLATES[template_key].format(
|
| 290 |
+
model_id=model_id, title=title, description=description,
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
system_prompt = """You are an expert Python developer specializing in Gradio applications for Hugging Face Spaces.
|
| 294 |
+
You generate complete, working app.py files that are production-ready.
|
| 295 |
+
|
| 296 |
+
Rules:
|
| 297 |
+
1. Output ONLY the Python code, no explanations or markdown.
|
| 298 |
+
2. Use `huggingface_hub.InferenceClient` for model inference (NOT transformers or pipeline).
|
| 299 |
+
3. Always include proper error handling.
|
| 300 |
+
4. The app must use `demo.launch()` at the end.
|
| 301 |
+
5. Include descriptive title and description in the interface.
|
| 302 |
+
6. Use appropriate Gradio components for the use case.
|
| 303 |
+
7. Code must be complete and runnable as-is.
|
| 304 |
+
8. Do NOT use `transformers`, `torch`, or `tensorflow` imports - use only `huggingface_hub.InferenceClient`.
|
| 305 |
+
9. For streaming text generation, use `client.chat_completion(..., stream=True)`.
|
| 306 |
+
10. For image tasks, use `client.image_classification()`, `client.object_detection()`, etc.
|
| 307 |
+
11. For text tasks, use `client.text_classification()`, `client.summarization()`, etc."""
|
| 308 |
+
|
| 309 |
+
user_prompt = f"""Generate a complete Gradio app.py for this request:
|
| 310 |
+
|
| 311 |
+
USER REQUEST: {prompt}
|
| 312 |
+
|
| 313 |
+
APP PLAN:
|
| 314 |
+
- SDK: gradio
|
| 315 |
+
- Type: {plan.get('app_type', 'custom')}
|
| 316 |
+
- Model: {model_id}
|
| 317 |
+
- Model Task: {plan.get('model_task', 'text-generation')}
|
| 318 |
+
- Components: {', '.join(plan.get('components', []))}
|
| 319 |
+
- Title: {title}
|
| 320 |
+
- Description: {description}
|
| 321 |
+
- Extra Features: {', '.join(plan.get('extra_features', []))}
|
| 322 |
+
"""
|
| 323 |
+
|
| 324 |
+
if reference_code:
|
| 325 |
+
user_prompt += f"""
|
| 326 |
+
REFERENCE TEMPLATE (adapt and improve this based on the user's specific request):
|
| 327 |
+
```python
|
| 328 |
+
{reference_code}
|
| 329 |
+
```
|
| 330 |
+
"""
|
| 331 |
+
|
| 332 |
+
user_prompt += "\nGenerate the complete app.py code now:"
|
| 333 |
+
|
| 334 |
+
response = self.client.chat_completion(
|
| 335 |
+
messages=[
|
| 336 |
+
{"role": "system", "content": system_prompt},
|
| 337 |
+
{"role": "user", "content": user_prompt},
|
| 338 |
+
],
|
| 339 |
+
max_tokens=4096,
|
| 340 |
+
temperature=0.3,
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
raw = response.choices[0].message.content
|
| 344 |
+
return self._extract_code(raw)
|
| 345 |
+
|
| 346 |
+
def _extract_code(self, text: str) -> str:
|
| 347 |
+
"""Extract Python code from LLM response."""
|
| 348 |
+
# Try to find code blocks
|
| 349 |
+
code_blocks = re.findall(r"```(?:python)?\s*\n(.*?)```", text, re.DOTALL)
|
| 350 |
+
if code_blocks:
|
| 351 |
+
# Return the longest code block
|
| 352 |
+
return max(code_blocks, key=len).strip()
|
| 353 |
+
|
| 354 |
+
# If no code blocks, check if the whole response looks like Python code
|
| 355 |
+
lines = text.strip().split("\n")
|
| 356 |
+
if lines and (lines[0].startswith("import ") or lines[0].startswith("from ")):
|
| 357 |
+
return text.strip()
|
| 358 |
+
|
| 359 |
+
return text.strip()
|
| 360 |
+
|
| 361 |
+
def _get_model_id(self, plan: dict) -> str:
|
| 362 |
+
"""Get the best model ID from the plan."""
|
| 363 |
+
models = plan.get("recommended_models", [])
|
| 364 |
+
if models:
|
| 365 |
+
return models[0]["id"]
|
| 366 |
+
|
| 367 |
+
# Fallback defaults by task
|
| 368 |
+
task_defaults = {
|
| 369 |
+
"text-generation": "Qwen/Qwen2.5-7B-Instruct",
|
| 370 |
+
"text-classification": "cardiffnlp/twitter-roberta-base-sentiment-latest",
|
| 371 |
+
"summarization": "facebook/bart-large-cnn",
|
| 372 |
+
"translation": "Qwen/Qwen2.5-7B-Instruct",
|
| 373 |
+
"image-classification": "google/vit-base-patch16-224",
|
| 374 |
+
"object-detection": "facebook/detr-resnet-50",
|
| 375 |
+
"text-to-image": "stabilityai/stable-diffusion-xl-base-1.0",
|
| 376 |
+
"automatic-speech-recognition": "openai/whisper-base",
|
| 377 |
+
"question-answering": "deepset/roberta-base-squad2",
|
| 378 |
+
}
|
| 379 |
+
return task_defaults.get(plan.get("model_task", ""), "Qwen/Qwen2.5-7B-Instruct")
|
| 380 |
+
|
| 381 |
+
def _generate_generic(self, plan: dict, model_id: str, title: str, description: str) -> str:
|
| 382 |
+
"""Generate a generic Gradio app when no template matches."""
|
| 383 |
+
task = plan.get("model_task", "text-generation")
|
| 384 |
+
|
| 385 |
+
if task in ("text-generation",):
|
| 386 |
+
return GRADIO_TEMPLATES["text_generator"].format(
|
| 387 |
+
model_id=model_id, title=title, description=description,
|
| 388 |
+
)
|
| 389 |
+
elif task in ("text-classification",):
|
| 390 |
+
return GRADIO_TEMPLATES["sentiment_analyzer"].format(
|
| 391 |
+
model_id=model_id, title=title, description=description,
|
| 392 |
+
)
|
| 393 |
+
elif task in ("summarization",):
|
| 394 |
+
return GRADIO_TEMPLATES["text_summarizer"].format(
|
| 395 |
+
model_id=model_id, title=title, description=description,
|
| 396 |
+
)
|
| 397 |
+
elif task in ("image-classification",):
|
| 398 |
+
return GRADIO_TEMPLATES["image_classifier"].format(
|
| 399 |
+
model_id=model_id, title=title, description=description,
|
| 400 |
+
)
|
| 401 |
+
elif task in ("question-answering",):
|
| 402 |
+
return GRADIO_TEMPLATES["question_answering"].format(
|
| 403 |
+
model_id=model_id, title=title, description=description,
|
| 404 |
+
)
|
| 405 |
+
else:
|
| 406 |
+
return GRADIO_TEMPLATES["text_generator"].format(
|
| 407 |
+
model_id=model_id, title=title, description=description,
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
def edit(self, plan: dict, current_code: str, edit_prompt: str) -> str:
|
| 411 |
+
"""Edit existing Gradio code based on user instructions."""
|
| 412 |
+
try:
|
| 413 |
+
system_prompt = """You are an expert Python developer. You will receive existing Gradio app code and an edit request.
|
| 414 |
+
Modify the code according to the request and return the COMPLETE updated code.
|
| 415 |
+
Output ONLY the Python code, no explanations or markdown fences.
|
| 416 |
+
Keep all existing functionality unless the user explicitly asks to remove something.
|
| 417 |
+
Use huggingface_hub.InferenceClient for model inference."""
|
| 418 |
+
|
| 419 |
+
user_prompt = f"""EXISTING CODE:
|
| 420 |
+
```python
|
| 421 |
+
{current_code}
|
| 422 |
+
```
|
| 423 |
+
|
| 424 |
+
EDIT REQUEST: {edit_prompt}
|
| 425 |
+
|
| 426 |
+
Return the complete updated app.py code:"""
|
| 427 |
+
|
| 428 |
+
response = self.client.chat_completion(
|
| 429 |
+
messages=[
|
| 430 |
+
{"role": "system", "content": system_prompt},
|
| 431 |
+
{"role": "user", "content": user_prompt},
|
| 432 |
+
],
|
| 433 |
+
max_tokens=4096,
|
| 434 |
+
temperature=0.2,
|
| 435 |
+
)
|
| 436 |
+
|
| 437 |
+
raw = response.choices[0].message.content
|
| 438 |
+
code = self._extract_code(raw)
|
| 439 |
+
if code and len(code) > 50:
|
| 440 |
+
return code
|
| 441 |
+
except Exception:
|
| 442 |
+
pass
|
| 443 |
+
|
| 444 |
+
return current_code
|
app/codegen/readme_generator.py
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Generate beautiful README.md files for generated HF Spaces.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
EMOJI_MAP = {
|
| 9 |
+
"chatbot": "💬",
|
| 10 |
+
"image_classifier": "🖼️",
|
| 11 |
+
"text_summarizer": "📝",
|
| 12 |
+
"sentiment_analyzer": "😊",
|
| 13 |
+
"text_generator": "✍️",
|
| 14 |
+
"translator": "🌐",
|
| 15 |
+
"object_detector": "🔍",
|
| 16 |
+
"speech_to_text": "🎤",
|
| 17 |
+
"image_generator": "🎨",
|
| 18 |
+
"question_answering": "❓",
|
| 19 |
+
"rest_api": "🔌",
|
| 20 |
+
"portfolio": "💼",
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
COLOR_MAP = {
|
| 24 |
+
"chatbot": ("blue", "purple"),
|
| 25 |
+
"image_classifier": ("green", "yellow"),
|
| 26 |
+
"text_summarizer": ("indigo", "blue"),
|
| 27 |
+
"sentiment_analyzer": ("pink", "red"),
|
| 28 |
+
"text_generator": ("purple", "indigo"),
|
| 29 |
+
"translator": ("cyan", "blue"),
|
| 30 |
+
"object_detector": ("orange", "red"),
|
| 31 |
+
"speech_to_text": ("green", "cyan"),
|
| 32 |
+
"image_generator": ("purple", "pink"),
|
| 33 |
+
"question_answering": ("yellow", "orange"),
|
| 34 |
+
"rest_api": ("gray", "blue"),
|
| 35 |
+
"portfolio": ("blue", "cyan"),
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ReadmeGenerator:
|
| 40 |
+
"""Generate HF Space README.md with proper frontmatter."""
|
| 41 |
+
|
| 42 |
+
def generate(self, plan: dict, sdk: str) -> str:
|
| 43 |
+
"""Generate a complete README.md for the Space."""
|
| 44 |
+
app_type = plan.get("app_type", "custom")
|
| 45 |
+
title = plan.get("title", "My App")
|
| 46 |
+
description = plan.get("description", "A Hugging Face Space")
|
| 47 |
+
app_name = plan.get("app_name", "my-app")
|
| 48 |
+
models = plan.get("recommended_models", [])
|
| 49 |
+
components = plan.get("components", [])
|
| 50 |
+
|
| 51 |
+
emoji = EMOJI_MAP.get(app_type, "🚀")
|
| 52 |
+
color_from, color_to = COLOR_MAP.get(app_type, ("blue", "cyan"))
|
| 53 |
+
|
| 54 |
+
# Build frontmatter
|
| 55 |
+
frontmatter = self._build_frontmatter(
|
| 56 |
+
title=title,
|
| 57 |
+
emoji=emoji,
|
| 58 |
+
color_from=color_from,
|
| 59 |
+
color_to=color_to,
|
| 60 |
+
sdk=sdk,
|
| 61 |
+
models=models,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
# Build body
|
| 65 |
+
body = self._build_body(
|
| 66 |
+
title=title,
|
| 67 |
+
description=description,
|
| 68 |
+
app_type=app_type,
|
| 69 |
+
sdk=sdk,
|
| 70 |
+
models=models,
|
| 71 |
+
components=components,
|
| 72 |
+
plan=plan,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
return frontmatter + "\n" + body
|
| 76 |
+
|
| 77 |
+
def _build_frontmatter(
|
| 78 |
+
self,
|
| 79 |
+
title: str,
|
| 80 |
+
emoji: str,
|
| 81 |
+
color_from: str,
|
| 82 |
+
color_to: str,
|
| 83 |
+
sdk: str,
|
| 84 |
+
models: list,
|
| 85 |
+
) -> str:
|
| 86 |
+
lines = [
|
| 87 |
+
"---",
|
| 88 |
+
f'title: "{title}"',
|
| 89 |
+
f"emoji: {emoji}",
|
| 90 |
+
f"colorFrom: {color_from}",
|
| 91 |
+
f"colorTo: {color_to}",
|
| 92 |
+
]
|
| 93 |
+
|
| 94 |
+
if sdk == "gradio":
|
| 95 |
+
lines.append("sdk: gradio")
|
| 96 |
+
lines.append("sdk_version: 5.9.1")
|
| 97 |
+
elif sdk == "docker":
|
| 98 |
+
lines.append("sdk: docker")
|
| 99 |
+
lines.append("app_port: 7860")
|
| 100 |
+
else:
|
| 101 |
+
lines.append("sdk: static")
|
| 102 |
+
|
| 103 |
+
if models:
|
| 104 |
+
lines.append("models:")
|
| 105 |
+
for m in models[:3]:
|
| 106 |
+
lines.append(f' - "{m["id"]}"')
|
| 107 |
+
|
| 108 |
+
lines.append("pinned: false")
|
| 109 |
+
lines.append("license: mit")
|
| 110 |
+
lines.append("---")
|
| 111 |
+
|
| 112 |
+
return "\n".join(lines)
|
| 113 |
+
|
| 114 |
+
def _build_body(
|
| 115 |
+
self,
|
| 116 |
+
title: str,
|
| 117 |
+
description: str,
|
| 118 |
+
app_type: str,
|
| 119 |
+
sdk: str,
|
| 120 |
+
models: list,
|
| 121 |
+
components: list,
|
| 122 |
+
plan: dict,
|
| 123 |
+
) -> str:
|
| 124 |
+
sections = []
|
| 125 |
+
|
| 126 |
+
# Title and description
|
| 127 |
+
sections.append(f"# {title}\n")
|
| 128 |
+
sections.append(f"{description}\n")
|
| 129 |
+
|
| 130 |
+
# Features section
|
| 131 |
+
sections.append("## Features\n")
|
| 132 |
+
feature_descriptions = {
|
| 133 |
+
"chat_interface": "Interactive chat interface with streaming responses",
|
| 134 |
+
"system_prompt_config": "Configurable system prompt",
|
| 135 |
+
"clear_button": "Clear conversation history",
|
| 136 |
+
"history": "Persistent chat history",
|
| 137 |
+
"image_upload": "Image upload support",
|
| 138 |
+
"label_output": "Classification labels with confidence scores",
|
| 139 |
+
"confidence_bars": "Visual confidence bars",
|
| 140 |
+
"examples": "Pre-loaded example inputs",
|
| 141 |
+
"text_input": "Text input area",
|
| 142 |
+
"file_upload": "File upload support",
|
| 143 |
+
"length_selector": "Adjustable output length",
|
| 144 |
+
"text_output": "Text output display",
|
| 145 |
+
"chart_output": "Data visualization charts",
|
| 146 |
+
"parameter_controls": "Adjustable model parameters",
|
| 147 |
+
"language_selector": "Multi-language selection",
|
| 148 |
+
"audio_input": "Audio/microphone input",
|
| 149 |
+
"annotated_image_output": "Annotated image output with bounding boxes",
|
| 150 |
+
"json_output": "Structured JSON output",
|
| 151 |
+
"gallery": "Image gallery view",
|
| 152 |
+
"image_output": "Image output display",
|
| 153 |
+
"fastapi_app": "FastAPI web application",
|
| 154 |
+
"model_endpoint": "Model inference endpoint",
|
| 155 |
+
"docs": "Interactive API documentation",
|
| 156 |
+
"health_check": "Health check endpoint",
|
| 157 |
+
"html_page": "Responsive HTML pages",
|
| 158 |
+
"css_styles": "Custom CSS styling",
|
| 159 |
+
"js_scripts": "Interactive JavaScript",
|
| 160 |
+
"context_input": "Context paragraph input",
|
| 161 |
+
"question_input": "Question input field",
|
| 162 |
+
"answer_output": "Answer display with confidence",
|
| 163 |
+
"highlight": "Answer highlighting in context",
|
| 164 |
+
"file_component": "File handling component",
|
| 165 |
+
"video_input": "Video input support",
|
| 166 |
+
"hero_section": "Hero section with CTA",
|
| 167 |
+
"projects_section": "Projects showcase",
|
| 168 |
+
"skills_section": "Skills display",
|
| 169 |
+
"contact_form": "Contact form",
|
| 170 |
+
}
|
| 171 |
+
for comp in components:
|
| 172 |
+
desc = feature_descriptions.get(comp, comp.replace("_", " ").title())
|
| 173 |
+
sections.append(f"- {desc}")
|
| 174 |
+
sections.append("")
|
| 175 |
+
|
| 176 |
+
# Models section
|
| 177 |
+
if models:
|
| 178 |
+
sections.append("## Models Used\n")
|
| 179 |
+
for m in models:
|
| 180 |
+
sections.append(f"- **[{m['id']}](https://huggingface.co/{m['id']})** - {m.get('desc', '')}")
|
| 181 |
+
sections.append("")
|
| 182 |
+
|
| 183 |
+
# Tech stack
|
| 184 |
+
sections.append("## Tech Stack\n")
|
| 185 |
+
if sdk == "gradio":
|
| 186 |
+
sections.append("- [Gradio](https://gradio.app/) - UI framework")
|
| 187 |
+
sections.append("- [Hugging Face Hub](https://huggingface.co/) - Model inference")
|
| 188 |
+
elif sdk == "docker":
|
| 189 |
+
sections.append("- [FastAPI](https://fastapi.tiangolo.com/) - Web framework")
|
| 190 |
+
sections.append("- [Docker](https://docker.com/) - Containerization")
|
| 191 |
+
sections.append("- [Hugging Face Hub](https://huggingface.co/) - Model inference")
|
| 192 |
+
else:
|
| 193 |
+
sections.append("- HTML5 / CSS3 / JavaScript")
|
| 194 |
+
sections.append("")
|
| 195 |
+
|
| 196 |
+
# Usage
|
| 197 |
+
sections.append("## Usage\n")
|
| 198 |
+
if sdk == "gradio":
|
| 199 |
+
sections.append("1. Open the Space URL")
|
| 200 |
+
sections.append("2. Interact with the interface")
|
| 201 |
+
sections.append("3. Results will be displayed automatically")
|
| 202 |
+
elif sdk == "docker":
|
| 203 |
+
sections.append("### API Endpoints\n")
|
| 204 |
+
sections.append("Visit `/docs` for interactive API documentation.\n")
|
| 205 |
+
sections.append("```bash")
|
| 206 |
+
sections.append('curl -X POST "/query" -H "Content-Type: application/json" -d \'{"query": "Hello"}\'')
|
| 207 |
+
sections.append("```")
|
| 208 |
+
else:
|
| 209 |
+
sections.append("Simply visit the Space URL to view the site.")
|
| 210 |
+
sections.append("")
|
| 211 |
+
|
| 212 |
+
# Footer
|
| 213 |
+
sections.append("---\n")
|
| 214 |
+
sections.append("*Generated by [AutoApp Builder](https://huggingface.co/spaces/autoapp-builder)*")
|
| 215 |
+
|
| 216 |
+
return "\n".join(sections)
|
app/codegen/repo_generator.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Orchestrate full repository generation by delegating to SDK-specific generators.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from app.codegen.gradio_generator import GradioGenerator
|
| 6 |
+
from app.codegen.docker_generator import DockerGenerator
|
| 7 |
+
from app.codegen.readme_generator import ReadmeGenerator
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
STATIC_TEMPLATE = """<!DOCTYPE html>
|
| 11 |
+
<html lang="en">
|
| 12 |
+
<head>
|
| 13 |
+
<meta charset="UTF-8">
|
| 14 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 15 |
+
<title>{title}</title>
|
| 16 |
+
<style>
|
| 17 |
+
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
| 18 |
+
body {{
|
| 19 |
+
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
| 20 |
+
background: #0f172a;
|
| 21 |
+
color: #e2e8f0;
|
| 22 |
+
min-height: 100vh;
|
| 23 |
+
}}
|
| 24 |
+
.hero {{
|
| 25 |
+
min-height: 60vh;
|
| 26 |
+
display: flex;
|
| 27 |
+
flex-direction: column;
|
| 28 |
+
align-items: center;
|
| 29 |
+
justify-content: center;
|
| 30 |
+
text-align: center;
|
| 31 |
+
padding: 2rem;
|
| 32 |
+
background: linear-gradient(135deg, #0f172a 0%, #1e293b 50%, #0f172a 100%);
|
| 33 |
+
}}
|
| 34 |
+
.hero h1 {{
|
| 35 |
+
font-size: 3.5rem;
|
| 36 |
+
font-weight: 800;
|
| 37 |
+
background: linear-gradient(135deg, #38bdf8, #818cf8);
|
| 38 |
+
-webkit-background-clip: text;
|
| 39 |
+
-webkit-text-fill-color: transparent;
|
| 40 |
+
margin-bottom: 1rem;
|
| 41 |
+
}}
|
| 42 |
+
.hero p {{
|
| 43 |
+
font-size: 1.25rem;
|
| 44 |
+
color: #94a3b8;
|
| 45 |
+
max-width: 600px;
|
| 46 |
+
line-height: 1.8;
|
| 47 |
+
}}
|
| 48 |
+
.section {{
|
| 49 |
+
max-width: 1000px;
|
| 50 |
+
margin: 0 auto;
|
| 51 |
+
padding: 4rem 2rem;
|
| 52 |
+
}}
|
| 53 |
+
.section h2 {{
|
| 54 |
+
font-size: 2rem;
|
| 55 |
+
margin-bottom: 2rem;
|
| 56 |
+
color: #38bdf8;
|
| 57 |
+
}}
|
| 58 |
+
.grid {{
|
| 59 |
+
display: grid;
|
| 60 |
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
| 61 |
+
gap: 1.5rem;
|
| 62 |
+
}}
|
| 63 |
+
.card {{
|
| 64 |
+
background: rgba(30, 41, 59, 0.8);
|
| 65 |
+
border: 1px solid rgba(56, 189, 248, 0.1);
|
| 66 |
+
border-radius: 12px;
|
| 67 |
+
padding: 1.5rem;
|
| 68 |
+
transition: transform 0.2s, border-color 0.2s;
|
| 69 |
+
}}
|
| 70 |
+
.card:hover {{
|
| 71 |
+
transform: translateY(-4px);
|
| 72 |
+
border-color: rgba(56, 189, 248, 0.4);
|
| 73 |
+
}}
|
| 74 |
+
.card h3 {{ color: #f1f5f9; margin-bottom: 0.5rem; }}
|
| 75 |
+
.card p {{ color: #94a3b8; line-height: 1.6; }}
|
| 76 |
+
.skills {{
|
| 77 |
+
display: flex;
|
| 78 |
+
flex-wrap: wrap;
|
| 79 |
+
gap: 0.75rem;
|
| 80 |
+
}}
|
| 81 |
+
.skill-tag {{
|
| 82 |
+
background: rgba(56, 189, 248, 0.15);
|
| 83 |
+
color: #38bdf8;
|
| 84 |
+
padding: 0.5rem 1rem;
|
| 85 |
+
border-radius: 9999px;
|
| 86 |
+
font-size: 0.875rem;
|
| 87 |
+
}}
|
| 88 |
+
.contact {{
|
| 89 |
+
background: rgba(30, 41, 59, 0.6);
|
| 90 |
+
border-radius: 16px;
|
| 91 |
+
padding: 2rem;
|
| 92 |
+
text-align: center;
|
| 93 |
+
}}
|
| 94 |
+
.contact a {{
|
| 95 |
+
display: inline-block;
|
| 96 |
+
margin: 0.5rem;
|
| 97 |
+
padding: 0.75rem 1.5rem;
|
| 98 |
+
background: linear-gradient(135deg, #38bdf8, #818cf8);
|
| 99 |
+
color: white;
|
| 100 |
+
text-decoration: none;
|
| 101 |
+
border-radius: 8px;
|
| 102 |
+
font-weight: 600;
|
| 103 |
+
}}
|
| 104 |
+
footer {{
|
| 105 |
+
text-align: center;
|
| 106 |
+
padding: 2rem;
|
| 107 |
+
color: #475569;
|
| 108 |
+
font-size: 0.875rem;
|
| 109 |
+
}}
|
| 110 |
+
</style>
|
| 111 |
+
</head>
|
| 112 |
+
<body>
|
| 113 |
+
<div class="hero">
|
| 114 |
+
<h1>{title}</h1>
|
| 115 |
+
<p>{description}</p>
|
| 116 |
+
</div>
|
| 117 |
+
|
| 118 |
+
<div class="section">
|
| 119 |
+
<h2>Projects</h2>
|
| 120 |
+
<div class="grid">
|
| 121 |
+
<div class="card">
|
| 122 |
+
<h3>Project Alpha</h3>
|
| 123 |
+
<p>A cutting-edge machine learning project that pushes the boundaries of what's possible.</p>
|
| 124 |
+
</div>
|
| 125 |
+
<div class="card">
|
| 126 |
+
<h3>Project Beta</h3>
|
| 127 |
+
<p>An innovative data pipeline that processes millions of records in real-time.</p>
|
| 128 |
+
</div>
|
| 129 |
+
<div class="card">
|
| 130 |
+
<h3>Project Gamma</h3>
|
| 131 |
+
<p>A beautiful visualization dashboard that makes complex data accessible.</p>
|
| 132 |
+
</div>
|
| 133 |
+
</div>
|
| 134 |
+
</div>
|
| 135 |
+
|
| 136 |
+
<div class="section">
|
| 137 |
+
<h2>Skills</h2>
|
| 138 |
+
<div class="skills">
|
| 139 |
+
<span class="skill-tag">Python</span>
|
| 140 |
+
<span class="skill-tag">Machine Learning</span>
|
| 141 |
+
<span class="skill-tag">Deep Learning</span>
|
| 142 |
+
<span class="skill-tag">NLP</span>
|
| 143 |
+
<span class="skill-tag">Computer Vision</span>
|
| 144 |
+
<span class="skill-tag">Data Science</span>
|
| 145 |
+
<span class="skill-tag">FastAPI</span>
|
| 146 |
+
<span class="skill-tag">Docker</span>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
|
| 150 |
+
<div class="section">
|
| 151 |
+
<div class="contact">
|
| 152 |
+
<h2>Get in Touch</h2>
|
| 153 |
+
<p style="color: #94a3b8; margin: 1rem 0;">Interested in collaborating? Reach out!</p>
|
| 154 |
+
<a href="mailto:hello@example.com">Email</a>
|
| 155 |
+
<a href="https://github.com">GitHub</a>
|
| 156 |
+
<a href="https://linkedin.com">LinkedIn</a>
|
| 157 |
+
</div>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<footer>
|
| 161 |
+
<p>Built with care. Hosted on Hugging Face Spaces.</p>
|
| 162 |
+
</footer>
|
| 163 |
+
</body>
|
| 164 |
+
</html>
|
| 165 |
+
"""
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
class RepoGenerator:
|
| 169 |
+
"""Orchestrate full repo generation for any SDK type."""
|
| 170 |
+
|
| 171 |
+
def __init__(self):
|
| 172 |
+
self.gradio_gen = GradioGenerator()
|
| 173 |
+
self.docker_gen = DockerGenerator()
|
| 174 |
+
self.readme_gen = ReadmeGenerator()
|
| 175 |
+
|
| 176 |
+
def generate(self, plan: dict, prompt: str) -> dict:
|
| 177 |
+
"""
|
| 178 |
+
Generate all files for a complete HF Space repo.
|
| 179 |
+
Returns dict of {filepath: content}.
|
| 180 |
+
"""
|
| 181 |
+
sdk = plan.get("sdk", "gradio")
|
| 182 |
+
|
| 183 |
+
if sdk == "gradio":
|
| 184 |
+
return self._generate_gradio_repo(plan, prompt)
|
| 185 |
+
elif sdk == "docker":
|
| 186 |
+
return self._generate_docker_repo(plan, prompt)
|
| 187 |
+
else:
|
| 188 |
+
return self._generate_static_repo(plan, prompt)
|
| 189 |
+
|
| 190 |
+
def _generate_gradio_repo(self, plan: dict, prompt: str) -> dict:
|
| 191 |
+
"""Generate a complete Gradio Space repo."""
|
| 192 |
+
files = {}
|
| 193 |
+
|
| 194 |
+
# README.md
|
| 195 |
+
files["README.md"] = self.readme_gen.generate(plan, "gradio")
|
| 196 |
+
|
| 197 |
+
# app.py - the main Gradio application
|
| 198 |
+
files["app.py"] = self.gradio_gen.generate(plan, prompt)
|
| 199 |
+
|
| 200 |
+
# requirements.txt
|
| 201 |
+
files["requirements.txt"] = self._gradio_requirements(plan)
|
| 202 |
+
|
| 203 |
+
# .gitignore
|
| 204 |
+
files[".gitignore"] = self._gitignore()
|
| 205 |
+
|
| 206 |
+
return files
|
| 207 |
+
|
| 208 |
+
def _generate_docker_repo(self, plan: dict, prompt: str) -> dict:
|
| 209 |
+
"""Generate a complete Docker Space repo."""
|
| 210 |
+
# Get docker-specific files from the generator
|
| 211 |
+
docker_files = self.docker_gen.generate(plan, prompt)
|
| 212 |
+
|
| 213 |
+
files = {}
|
| 214 |
+
|
| 215 |
+
# README.md (always generate our own)
|
| 216 |
+
files["README.md"] = self.readme_gen.generate(plan, "docker")
|
| 217 |
+
|
| 218 |
+
# Merge docker-generated files
|
| 219 |
+
for name, content in docker_files.items():
|
| 220 |
+
files[name] = content
|
| 221 |
+
|
| 222 |
+
# .gitignore
|
| 223 |
+
files[".gitignore"] = self._gitignore()
|
| 224 |
+
|
| 225 |
+
return files
|
| 226 |
+
|
| 227 |
+
def _generate_static_repo(self, plan: dict, prompt: str) -> dict:
|
| 228 |
+
"""Generate a complete Static Space repo."""
|
| 229 |
+
title = plan.get("title", "My Site")
|
| 230 |
+
description = plan.get("description", "A static website")
|
| 231 |
+
|
| 232 |
+
files = {}
|
| 233 |
+
|
| 234 |
+
# README.md
|
| 235 |
+
files["README.md"] = self.readme_gen.generate(plan, "static")
|
| 236 |
+
|
| 237 |
+
# index.html
|
| 238 |
+
files["index.html"] = STATIC_TEMPLATE.format(
|
| 239 |
+
title=title,
|
| 240 |
+
description=description,
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
# style.css (additional styles)
|
| 244 |
+
files["style.css"] = """/* Additional custom styles */
|
| 245 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
| 246 |
+
|
| 247 |
+
body {
|
| 248 |
+
font-family: 'Inter', system-ui, sans-serif;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
/* Smooth scrolling */
|
| 252 |
+
html {
|
| 253 |
+
scroll-behavior: smooth;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
/* Animated gradient background */
|
| 257 |
+
@keyframes gradient-shift {
|
| 258 |
+
0% { background-position: 0% 50%; }
|
| 259 |
+
50% { background-position: 100% 50%; }
|
| 260 |
+
100% { background-position: 0% 50%; }
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
.hero {
|
| 264 |
+
background-size: 200% 200%;
|
| 265 |
+
animation: gradient-shift 8s ease infinite;
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* Fade-in animation */
|
| 269 |
+
@keyframes fadeInUp {
|
| 270 |
+
from {
|
| 271 |
+
opacity: 0;
|
| 272 |
+
transform: translateY(20px);
|
| 273 |
+
}
|
| 274 |
+
to {
|
| 275 |
+
opacity: 1;
|
| 276 |
+
transform: translateY(0);
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
.card {
|
| 281 |
+
animation: fadeInUp 0.5s ease forwards;
|
| 282 |
+
}
|
| 283 |
+
"""
|
| 284 |
+
|
| 285 |
+
files[".gitignore"] = self._gitignore()
|
| 286 |
+
|
| 287 |
+
return files
|
| 288 |
+
|
| 289 |
+
def _gradio_requirements(self, plan: dict) -> str:
|
| 290 |
+
"""Generate requirements.txt for Gradio apps."""
|
| 291 |
+
deps = [
|
| 292 |
+
"gradio>=5.9.1",
|
| 293 |
+
"huggingface-hub>=0.27.1",
|
| 294 |
+
]
|
| 295 |
+
|
| 296 |
+
# Add task-specific dependencies
|
| 297 |
+
task = plan.get("model_task", "")
|
| 298 |
+
if task in ("text-to-image", "image-classification", "object-detection"):
|
| 299 |
+
deps.append("Pillow>=10.0.0")
|
| 300 |
+
if "chart_output" in plan.get("components", []):
|
| 301 |
+
deps.append("matplotlib>=3.8.0")
|
| 302 |
+
deps.append("numpy>=1.26.0")
|
| 303 |
+
|
| 304 |
+
return "\n".join(deps) + "\n"
|
| 305 |
+
|
| 306 |
+
def _gitignore(self) -> str:
|
| 307 |
+
return """__pycache__/
|
| 308 |
+
*.py[cod]
|
| 309 |
+
*$py.class
|
| 310 |
+
*.egg-info/
|
| 311 |
+
dist/
|
| 312 |
+
build/
|
| 313 |
+
.env
|
| 314 |
+
.venv/
|
| 315 |
+
venv/
|
| 316 |
+
.DS_Store
|
| 317 |
+
*.log
|
| 318 |
+
"""
|
| 319 |
+
|
| 320 |
+
def edit(self, plan: dict, current_files: dict, edit_prompt: str) -> dict:
|
| 321 |
+
"""Edit an existing repo based on user instructions."""
|
| 322 |
+
sdk = plan.get("sdk", "gradio")
|
| 323 |
+
|
| 324 |
+
updated = dict(current_files)
|
| 325 |
+
|
| 326 |
+
if sdk == "gradio" and "app.py" in current_files:
|
| 327 |
+
updated["app.py"] = self.gradio_gen.edit(plan, current_files["app.py"], edit_prompt)
|
| 328 |
+
elif sdk == "docker":
|
| 329 |
+
docker_updated = self.docker_gen.edit(plan, current_files, edit_prompt)
|
| 330 |
+
updated.update(docker_updated)
|
| 331 |
+
# Static sites: for now return unchanged (could add LLM-based HTML editing)
|
| 332 |
+
|
| 333 |
+
return updated
|
app/engine/__init__.py
ADDED
|
File without changes
|
app/engine/app_planner.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Analyze user prompts, select the appropriate SDK, and plan app architecture.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from typing import Optional
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# Keywords that strongly indicate a particular SDK
|
| 10 |
+
GRADIO_KEYWORDS = [
|
| 11 |
+
"gradio", "demo", "interface", "chatbot", "chat", "image classifier",
|
| 12 |
+
"image classification", "object detection", "text generation", "summarizer",
|
| 13 |
+
"summarization", "sentiment", "translation", "translate", "speech",
|
| 14 |
+
"audio", "video", "upload", "webcam", "microphone", "slider", "dropdown",
|
| 15 |
+
"interactive", "ml model", "machine learning", "deep learning", "neural",
|
| 16 |
+
"predict", "inference", "classify", "segment", "detect", "recognize",
|
| 17 |
+
"generate image", "stable diffusion", "text-to-image", "image-to-text",
|
| 18 |
+
"question answering", "qa", "ner", "named entity", "token classification",
|
| 19 |
+
"zero-shot", "few-shot", "embedding", "similarity",
|
| 20 |
+
]
|
| 21 |
+
|
| 22 |
+
DOCKER_KEYWORDS = [
|
| 23 |
+
"docker", "api", "rest", "fastapi", "flask", "endpoint", "websocket",
|
| 24 |
+
"agent", "autonomous", "pipeline", "workflow", "backend", "server",
|
| 25 |
+
"database", "redis", "celery", "queue", "cron", "schedule", "scrape",
|
| 26 |
+
"crawl", "multi-step", "orchestrat", "microservice", "webhook",
|
| 27 |
+
"authentication", "auth", "oauth", "custom server", "complex",
|
| 28 |
+
"real-time", "streaming api",
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
STATIC_KEYWORDS = [
|
| 32 |
+
"static", "landing page", "portfolio", "documentation", "docs",
|
| 33 |
+
"blog", "website", "html", "css only", "showcase", "gallery",
|
| 34 |
+
"resume", "cv", "personal site", "no backend", "frontend only",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
# Templates for common app types
|
| 38 |
+
APP_TEMPLATES = {
|
| 39 |
+
"chatbot": {
|
| 40 |
+
"components": ["chat_interface", "system_prompt_config", "clear_button", "history"],
|
| 41 |
+
"model_task": "text-generation",
|
| 42 |
+
"description": "Conversational AI chatbot",
|
| 43 |
+
},
|
| 44 |
+
"image_classifier": {
|
| 45 |
+
"components": ["image_upload", "label_output", "confidence_bars", "examples"],
|
| 46 |
+
"model_task": "image-classification",
|
| 47 |
+
"description": "Image classification with confidence scores",
|
| 48 |
+
},
|
| 49 |
+
"text_summarizer": {
|
| 50 |
+
"components": ["text_input", "file_upload", "length_selector", "text_output"],
|
| 51 |
+
"model_task": "summarization",
|
| 52 |
+
"description": "Text summarization tool",
|
| 53 |
+
},
|
| 54 |
+
"sentiment_analyzer": {
|
| 55 |
+
"components": ["text_input", "label_output", "chart_output"],
|
| 56 |
+
"model_task": "text-classification",
|
| 57 |
+
"description": "Sentiment analysis with visualization",
|
| 58 |
+
},
|
| 59 |
+
"text_generator": {
|
| 60 |
+
"components": ["text_input", "parameter_controls", "text_output"],
|
| 61 |
+
"model_task": "text-generation",
|
| 62 |
+
"description": "Text generation with configurable parameters",
|
| 63 |
+
},
|
| 64 |
+
"translator": {
|
| 65 |
+
"components": ["text_input", "language_selector", "text_output"],
|
| 66 |
+
"model_task": "translation",
|
| 67 |
+
"description": "Multi-language translation tool",
|
| 68 |
+
},
|
| 69 |
+
"object_detector": {
|
| 70 |
+
"components": ["image_upload", "annotated_image_output", "json_output"],
|
| 71 |
+
"model_task": "object-detection",
|
| 72 |
+
"description": "Object detection with bounding boxes",
|
| 73 |
+
},
|
| 74 |
+
"speech_to_text": {
|
| 75 |
+
"components": ["audio_input", "text_output", "language_selector"],
|
| 76 |
+
"model_task": "automatic-speech-recognition",
|
| 77 |
+
"description": "Speech recognition / transcription",
|
| 78 |
+
},
|
| 79 |
+
"image_generator": {
|
| 80 |
+
"components": ["text_input", "parameter_controls", "image_output", "gallery"],
|
| 81 |
+
"model_task": "text-to-image",
|
| 82 |
+
"description": "AI image generation from text prompts",
|
| 83 |
+
},
|
| 84 |
+
"question_answering": {
|
| 85 |
+
"components": ["context_input", "question_input", "answer_output", "highlight"],
|
| 86 |
+
"model_task": "question-answering",
|
| 87 |
+
"description": "Extractive question answering",
|
| 88 |
+
},
|
| 89 |
+
"rest_api": {
|
| 90 |
+
"components": ["fastapi_app", "model_endpoint", "docs", "health_check"],
|
| 91 |
+
"model_task": "text-generation",
|
| 92 |
+
"description": "REST API serving ML models",
|
| 93 |
+
},
|
| 94 |
+
"portfolio": {
|
| 95 |
+
"components": ["hero_section", "projects_section", "skills_section", "contact_form"],
|
| 96 |
+
"model_task": None,
|
| 97 |
+
"description": "Personal portfolio website",
|
| 98 |
+
},
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
class AppPlanner:
|
| 103 |
+
"""Analyzes user prompts and produces a structured app plan."""
|
| 104 |
+
|
| 105 |
+
def analyze(self, prompt: str, sdk_preference: str = "auto") -> dict:
|
| 106 |
+
"""
|
| 107 |
+
Analyze the user prompt and return a structured plan.
|
| 108 |
+
|
| 109 |
+
Returns dict with keys:
|
| 110 |
+
sdk, app_name, app_type, description, components,
|
| 111 |
+
model_task, template_key, title
|
| 112 |
+
"""
|
| 113 |
+
prompt_lower = prompt.lower()
|
| 114 |
+
|
| 115 |
+
# Detect the best matching template
|
| 116 |
+
template_key = self._match_template(prompt_lower)
|
| 117 |
+
template = APP_TEMPLATES.get(template_key, {})
|
| 118 |
+
|
| 119 |
+
# Determine SDK
|
| 120 |
+
if sdk_preference != "auto":
|
| 121 |
+
sdk = sdk_preference
|
| 122 |
+
else:
|
| 123 |
+
sdk = self._select_sdk(prompt_lower, template_key)
|
| 124 |
+
|
| 125 |
+
# Generate an app name from the prompt
|
| 126 |
+
app_name = self._generate_app_name(prompt_lower)
|
| 127 |
+
|
| 128 |
+
# Determine a human-readable title
|
| 129 |
+
title = self._generate_title(prompt, template_key)
|
| 130 |
+
|
| 131 |
+
plan = {
|
| 132 |
+
"sdk": sdk,
|
| 133 |
+
"app_name": app_name,
|
| 134 |
+
"app_type": template_key or "custom",
|
| 135 |
+
"title": title,
|
| 136 |
+
"description": template.get("description", self._extract_description(prompt)),
|
| 137 |
+
"components": template.get("components", self._infer_components(prompt_lower, sdk)),
|
| 138 |
+
"model_task": template.get("model_task", self._infer_model_task(prompt_lower)),
|
| 139 |
+
"template_key": template_key,
|
| 140 |
+
"original_prompt": prompt,
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
return plan
|
| 144 |
+
|
| 145 |
+
def _select_sdk(self, prompt_lower: str, template_key: Optional[str]) -> str:
|
| 146 |
+
"""Auto-select the best SDK based on prompt analysis."""
|
| 147 |
+
# Check template-based hints
|
| 148 |
+
if template_key in ("rest_api",):
|
| 149 |
+
return "docker"
|
| 150 |
+
if template_key in ("portfolio",):
|
| 151 |
+
return "static"
|
| 152 |
+
|
| 153 |
+
gradio_score = sum(1 for kw in GRADIO_KEYWORDS if kw in prompt_lower)
|
| 154 |
+
docker_score = sum(1 for kw in DOCKER_KEYWORDS if kw in prompt_lower)
|
| 155 |
+
static_score = sum(1 for kw in STATIC_KEYWORDS if kw in prompt_lower)
|
| 156 |
+
|
| 157 |
+
scores = {"gradio": gradio_score, "docker": docker_score, "static": static_score}
|
| 158 |
+
best = max(scores, key=scores.get)
|
| 159 |
+
|
| 160 |
+
# Default to gradio if no strong signal
|
| 161 |
+
if scores[best] == 0:
|
| 162 |
+
return "gradio"
|
| 163 |
+
|
| 164 |
+
return best
|
| 165 |
+
|
| 166 |
+
def _match_template(self, prompt_lower: str) -> Optional[str]:
|
| 167 |
+
"""Find the best matching template for the prompt."""
|
| 168 |
+
scores = {}
|
| 169 |
+
keyword_map = {
|
| 170 |
+
"chatbot": ["chat", "chatbot", "conversation", "dialogue", "talk"],
|
| 171 |
+
"image_classifier": ["classify image", "image classif", "image recogni", "predict image"],
|
| 172 |
+
"text_summarizer": ["summar", "summarize", "summarization", "tldr", "shorten text"],
|
| 173 |
+
"sentiment_analyzer": ["sentiment", "emotion", "mood", "opinion"],
|
| 174 |
+
"text_generator": ["text generat", "write text", "generate text", "story generat", "complete text"],
|
| 175 |
+
"translator": ["translat", "language translation"],
|
| 176 |
+
"object_detector": ["object detect", "detect object", "bounding box", "find objects"],
|
| 177 |
+
"speech_to_text": ["speech", "transcri", "voice to text", "audio to text", "asr"],
|
| 178 |
+
"image_generator": ["generate image", "text to image", "stable diffusion", "create image", "dall-e", "image generat"],
|
| 179 |
+
"question_answering": ["question answer", "qa ", "answer question", "reading comprehension"],
|
| 180 |
+
"rest_api": ["rest api", "api service", "endpoint", "api server", "fastapi"],
|
| 181 |
+
"portfolio": ["portfolio", "personal site", "resume", "cv site", "showcase"],
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
for key, keywords in keyword_map.items():
|
| 185 |
+
score = sum(1 for kw in keywords if kw in prompt_lower)
|
| 186 |
+
if score > 0:
|
| 187 |
+
scores[key] = score
|
| 188 |
+
|
| 189 |
+
if not scores:
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
return max(scores, key=scores.get)
|
| 193 |
+
|
| 194 |
+
def _generate_app_name(self, prompt_lower: str) -> str:
|
| 195 |
+
"""Generate a slug-style app name."""
|
| 196 |
+
# Try to extract key nouns
|
| 197 |
+
words = re.sub(r"[^a-z0-9\s]", "", prompt_lower).split()
|
| 198 |
+
stop_words = {
|
| 199 |
+
"a", "an", "the", "is", "are", "was", "were", "be", "been",
|
| 200 |
+
"being", "have", "has", "had", "do", "does", "did", "will",
|
| 201 |
+
"would", "could", "should", "may", "might", "can", "shall",
|
| 202 |
+
"build", "create", "make", "develop", "design", "generate",
|
| 203 |
+
"that", "which", "who", "whom", "this", "these", "those",
|
| 204 |
+
"i", "me", "my", "we", "our", "you", "your", "it", "its",
|
| 205 |
+
"they", "them", "their", "what", "where", "when", "how",
|
| 206 |
+
"and", "or", "but", "if", "then", "else", "so", "for",
|
| 207 |
+
"with", "from", "to", "of", "in", "on", "at", "by", "about",
|
| 208 |
+
"into", "through", "during", "before", "after", "above",
|
| 209 |
+
"below", "between", "under", "using", "use", "uses",
|
| 210 |
+
"let", "users", "user", "app", "application",
|
| 211 |
+
}
|
| 212 |
+
meaningful = [w for w in words if w not in stop_words and len(w) > 2][:4]
|
| 213 |
+
if not meaningful:
|
| 214 |
+
meaningful = ["my", "app"]
|
| 215 |
+
return "-".join(meaningful)
|
| 216 |
+
|
| 217 |
+
def _generate_title(self, prompt: str, template_key: Optional[str]) -> str:
|
| 218 |
+
"""Generate a human-friendly title."""
|
| 219 |
+
if template_key and template_key in APP_TEMPLATES:
|
| 220 |
+
return APP_TEMPLATES[template_key]["description"]
|
| 221 |
+
# Capitalize first ~6 words of prompt
|
| 222 |
+
words = prompt.split()[:6]
|
| 223 |
+
title = " ".join(words)
|
| 224 |
+
if len(prompt.split()) > 6:
|
| 225 |
+
title += "..."
|
| 226 |
+
return title
|
| 227 |
+
|
| 228 |
+
def _extract_description(self, prompt: str) -> str:
|
| 229 |
+
"""Extract a short description from the prompt."""
|
| 230 |
+
sentences = prompt.split(".")
|
| 231 |
+
if sentences:
|
| 232 |
+
return sentences[0].strip()[:200]
|
| 233 |
+
return prompt[:200]
|
| 234 |
+
|
| 235 |
+
def _infer_components(self, prompt_lower: str, sdk: str) -> list:
|
| 236 |
+
"""Infer UI components from the prompt when no template matches."""
|
| 237 |
+
components = []
|
| 238 |
+
if sdk == "gradio":
|
| 239 |
+
if any(w in prompt_lower for w in ["image", "photo", "picture", "upload image"]):
|
| 240 |
+
components.append("image_upload")
|
| 241 |
+
if any(w in prompt_lower for w in ["text", "input", "enter", "paste", "type"]):
|
| 242 |
+
components.append("text_input")
|
| 243 |
+
if any(w in prompt_lower for w in ["audio", "sound", "voice", "microphone"]):
|
| 244 |
+
components.append("audio_input")
|
| 245 |
+
if any(w in prompt_lower for w in ["video", "webcam"]):
|
| 246 |
+
components.append("video_input")
|
| 247 |
+
if any(w in prompt_lower for w in ["chart", "plot", "graph", "visual"]):
|
| 248 |
+
components.append("chart_output")
|
| 249 |
+
if any(w in prompt_lower for w in ["file", "upload", "download"]):
|
| 250 |
+
components.append("file_component")
|
| 251 |
+
if not components:
|
| 252 |
+
components = ["text_input", "text_output"]
|
| 253 |
+
elif sdk == "docker":
|
| 254 |
+
components = ["fastapi_app", "model_endpoint", "health_check"]
|
| 255 |
+
else:
|
| 256 |
+
components = ["html_page", "css_styles", "js_scripts"]
|
| 257 |
+
return components
|
| 258 |
+
|
| 259 |
+
def _infer_model_task(self, prompt_lower: str) -> Optional[str]:
|
| 260 |
+
"""Infer the HF model task from prompt keywords."""
|
| 261 |
+
task_keywords = {
|
| 262 |
+
"text-generation": ["generat", "write", "complete", "chat", "story"],
|
| 263 |
+
"text-classification": ["classif", "sentiment", "emotion", "categor"],
|
| 264 |
+
"summarization": ["summar", "tldr", "shorten"],
|
| 265 |
+
"translation": ["translat"],
|
| 266 |
+
"image-classification": ["image classif", "recognize image", "identify image"],
|
| 267 |
+
"object-detection": ["object detect", "bounding box", "detect object"],
|
| 268 |
+
"text-to-image": ["generate image", "text to image", "create image"],
|
| 269 |
+
"automatic-speech-recognition": ["speech", "transcri", "asr", "voice to text"],
|
| 270 |
+
"question-answering": ["question answer", "qa"],
|
| 271 |
+
"token-classification": ["ner", "named entity", "token classif"],
|
| 272 |
+
"fill-mask": ["fill mask", "mask", "cloze"],
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
for task, keywords in task_keywords.items():
|
| 276 |
+
if any(kw in prompt_lower for kw in keywords):
|
| 277 |
+
return task
|
| 278 |
+
|
| 279 |
+
return "text-generation" # sensible default
|
app/engine/model_recommender.py
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Recommend Hugging Face models based on the app plan, size preference, and GPU needs.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
# Curated model recommendations by task and size
|
| 9 |
+
MODEL_CATALOG = {
|
| 10 |
+
"text-generation": {
|
| 11 |
+
"small": [
|
| 12 |
+
{"id": "HuggingFaceTB/SmolLM2-360M-Instruct", "desc": "Compact instruct model, very fast", "size": "360M"},
|
| 13 |
+
],
|
| 14 |
+
"medium": [
|
| 15 |
+
{"id": "Qwen/Qwen2.5-7B-Instruct", "desc": "Strong general-purpose 7B instruct model", "size": "7B"},
|
| 16 |
+
{"id": "mistralai/Mistral-7B-Instruct-v0.3", "desc": "Fast and capable instruct model", "size": "7B"},
|
| 17 |
+
],
|
| 18 |
+
"large": [
|
| 19 |
+
{"id": "meta-llama/Llama-3.1-70B-Instruct", "desc": "Top-tier large language model", "size": "70B"},
|
| 20 |
+
{"id": "Qwen/Qwen2.5-72B-Instruct", "desc": "Excellent multilingual reasoning", "size": "72B"},
|
| 21 |
+
],
|
| 22 |
+
},
|
| 23 |
+
"text-classification": {
|
| 24 |
+
"small": [
|
| 25 |
+
{"id": "distilbert-base-uncased-finetuned-sst-2-english", "desc": "Fast sentiment classifier", "size": "66M"},
|
| 26 |
+
],
|
| 27 |
+
"medium": [
|
| 28 |
+
{"id": "cardiffnlp/twitter-roberta-base-sentiment-latest", "desc": "Social media sentiment analysis", "size": "125M"},
|
| 29 |
+
{"id": "j-hartmann/emotion-english-distilroberta-base", "desc": "Multi-emotion classifier", "size": "82M"},
|
| 30 |
+
],
|
| 31 |
+
"large": [
|
| 32 |
+
{"id": "SamLowe/roberta-base-go_emotions", "desc": "28-class emotion detection", "size": "125M"},
|
| 33 |
+
],
|
| 34 |
+
},
|
| 35 |
+
"summarization": {
|
| 36 |
+
"small": [
|
| 37 |
+
{"id": "sshleifer/distilbart-cnn-12-6", "desc": "Compact summarization model", "size": "306M"},
|
| 38 |
+
],
|
| 39 |
+
"medium": [
|
| 40 |
+
{"id": "facebook/bart-large-cnn", "desc": "Strong CNN/DailyMail summarizer", "size": "406M"},
|
| 41 |
+
{"id": "google/pegasus-xsum", "desc": "Abstractive summarization", "size": "568M"},
|
| 42 |
+
],
|
| 43 |
+
"large": [
|
| 44 |
+
{"id": "google/pegasus-large", "desc": "High-quality abstractive summaries", "size": "568M"},
|
| 45 |
+
],
|
| 46 |
+
},
|
| 47 |
+
"translation": {
|
| 48 |
+
"small": [
|
| 49 |
+
{"id": "Helsinki-NLP/opus-mt-en-fr", "desc": "English to French translation", "size": "298M"},
|
| 50 |
+
],
|
| 51 |
+
"medium": [
|
| 52 |
+
{"id": "facebook/mbart-large-50-many-to-many-mmt", "desc": "50-language translation", "size": "611M"},
|
| 53 |
+
],
|
| 54 |
+
"large": [
|
| 55 |
+
{"id": "facebook/nllb-200-3.3B", "desc": "200-language translation model", "size": "3.3B"},
|
| 56 |
+
],
|
| 57 |
+
},
|
| 58 |
+
"image-classification": {
|
| 59 |
+
"small": [
|
| 60 |
+
{"id": "google/mobilenet_v2_1.0_224", "desc": "Mobile-optimized classifier", "size": "3.4M"},
|
| 61 |
+
],
|
| 62 |
+
"medium": [
|
| 63 |
+
{"id": "microsoft/resnet-50", "desc": "Classic ResNet-50 ImageNet classifier", "size": "25.6M"},
|
| 64 |
+
{"id": "google/vit-base-patch16-224", "desc": "Vision Transformer classifier", "size": "86M"},
|
| 65 |
+
],
|
| 66 |
+
"large": [
|
| 67 |
+
{"id": "google/vit-large-patch16-224", "desc": "Large Vision Transformer", "size": "304M"},
|
| 68 |
+
],
|
| 69 |
+
},
|
| 70 |
+
"object-detection": {
|
| 71 |
+
"small": [
|
| 72 |
+
{"id": "hustvl/yolos-tiny", "desc": "Tiny YOLO-style detector", "size": "6.5M"},
|
| 73 |
+
],
|
| 74 |
+
"medium": [
|
| 75 |
+
{"id": "facebook/detr-resnet-50", "desc": "DETR object detector", "size": "41M"},
|
| 76 |
+
],
|
| 77 |
+
"large": [
|
| 78 |
+
{"id": "facebook/detr-resnet-101", "desc": "Large DETR detector", "size": "60M"},
|
| 79 |
+
],
|
| 80 |
+
},
|
| 81 |
+
"text-to-image": {
|
| 82 |
+
"small": [
|
| 83 |
+
{"id": "segmind/SSD-1B", "desc": "Compact SD distilled model", "size": "1.3B"},
|
| 84 |
+
],
|
| 85 |
+
"medium": [
|
| 86 |
+
{"id": "stabilityai/stable-diffusion-xl-base-1.0", "desc": "SDXL base model", "size": "3.5B"},
|
| 87 |
+
],
|
| 88 |
+
"large": [
|
| 89 |
+
{"id": "black-forest-labs/FLUX.1-dev", "desc": "State-of-the-art image gen", "size": "12B"},
|
| 90 |
+
],
|
| 91 |
+
},
|
| 92 |
+
"automatic-speech-recognition": {
|
| 93 |
+
"small": [
|
| 94 |
+
{"id": "openai/whisper-tiny", "desc": "Tiny Whisper ASR", "size": "39M"},
|
| 95 |
+
],
|
| 96 |
+
"medium": [
|
| 97 |
+
{"id": "openai/whisper-base", "desc": "Whisper base ASR model", "size": "74M"},
|
| 98 |
+
{"id": "openai/whisper-medium", "desc": "Whisper medium ASR model", "size": "769M"},
|
| 99 |
+
],
|
| 100 |
+
"large": [
|
| 101 |
+
{"id": "openai/whisper-large-v3", "desc": "Best Whisper ASR model", "size": "1.5B"},
|
| 102 |
+
],
|
| 103 |
+
},
|
| 104 |
+
"question-answering": {
|
| 105 |
+
"small": [
|
| 106 |
+
{"id": "distilbert-base-cased-distilled-squad", "desc": "Fast QA model", "size": "66M"},
|
| 107 |
+
],
|
| 108 |
+
"medium": [
|
| 109 |
+
{"id": "deepset/roberta-base-squad2", "desc": "RoBERTa QA on SQuAD2", "size": "125M"},
|
| 110 |
+
],
|
| 111 |
+
"large": [
|
| 112 |
+
{"id": "deepset/deberta-v3-large-squad2", "desc": "DeBERTa large QA", "size": "304M"},
|
| 113 |
+
],
|
| 114 |
+
},
|
| 115 |
+
"token-classification": {
|
| 116 |
+
"small": [
|
| 117 |
+
{"id": "dslim/bert-base-NER", "desc": "BERT NER model", "size": "110M"},
|
| 118 |
+
],
|
| 119 |
+
"medium": [
|
| 120 |
+
{"id": "Jean-Baptiste/roberta-large-ner-english", "desc": "Large NER model", "size": "355M"},
|
| 121 |
+
],
|
| 122 |
+
"large": [
|
| 123 |
+
{"id": "Jean-Baptiste/roberta-large-ner-english", "desc": "Large NER model", "size": "355M"},
|
| 124 |
+
],
|
| 125 |
+
},
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
# GPU recommendation thresholds
|
| 129 |
+
GPU_THRESHOLDS = {
|
| 130 |
+
"small": False,
|
| 131 |
+
"medium": False, # most medium models run on CPU
|
| 132 |
+
"large": True,
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
class ModelRecommender:
|
| 137 |
+
"""Recommend HF models based on app plan and user preferences."""
|
| 138 |
+
|
| 139 |
+
def recommend(
|
| 140 |
+
self,
|
| 141 |
+
plan: dict,
|
| 142 |
+
model_size: str = "medium",
|
| 143 |
+
gpu_needed: bool = False,
|
| 144 |
+
) -> list:
|
| 145 |
+
"""
|
| 146 |
+
Return a list of recommended model dicts.
|
| 147 |
+
|
| 148 |
+
Each dict has: id, desc, size, gpu_recommended
|
| 149 |
+
"""
|
| 150 |
+
task = plan.get("model_task")
|
| 151 |
+
if not task:
|
| 152 |
+
return []
|
| 153 |
+
|
| 154 |
+
# Normalize size
|
| 155 |
+
if model_size not in ("small", "medium", "large"):
|
| 156 |
+
model_size = "medium"
|
| 157 |
+
|
| 158 |
+
task_models = MODEL_CATALOG.get(task, {})
|
| 159 |
+
candidates = task_models.get(model_size, [])
|
| 160 |
+
|
| 161 |
+
# Fallback: try adjacent sizes
|
| 162 |
+
if not candidates:
|
| 163 |
+
for fallback in ("medium", "small", "large"):
|
| 164 |
+
candidates = task_models.get(fallback, [])
|
| 165 |
+
if candidates:
|
| 166 |
+
break
|
| 167 |
+
|
| 168 |
+
# If still nothing, provide a generic suggestion
|
| 169 |
+
if not candidates:
|
| 170 |
+
candidates = [
|
| 171 |
+
{
|
| 172 |
+
"id": f"models?pipeline_tag={task}",
|
| 173 |
+
"desc": f"Search HF Hub for {task} models",
|
| 174 |
+
"size": "varies",
|
| 175 |
+
}
|
| 176 |
+
]
|
| 177 |
+
|
| 178 |
+
# Annotate with GPU recommendation
|
| 179 |
+
results = []
|
| 180 |
+
for model in candidates:
|
| 181 |
+
m = dict(model)
|
| 182 |
+
m["gpu_recommended"] = gpu_needed or GPU_THRESHOLDS.get(model_size, False)
|
| 183 |
+
results.append(m)
|
| 184 |
+
|
| 185 |
+
return results
|
| 186 |
+
|
| 187 |
+
def get_primary_model(self, plan: dict, model_size: str = "medium") -> Optional[str]:
|
| 188 |
+
"""Get the single best model ID for the plan."""
|
| 189 |
+
models = self.recommend(plan, model_size)
|
| 190 |
+
if models:
|
| 191 |
+
return models[0]["id"]
|
| 192 |
+
return None
|
app/main.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import io
|
| 3 |
+
import json
|
| 4 |
+
import uuid
|
| 5 |
+
import zipfile
|
| 6 |
+
import traceback
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Optional
|
| 9 |
+
|
| 10 |
+
from fastapi import FastAPI, Request, Form, HTTPException
|
| 11 |
+
from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse
|
| 12 |
+
from fastapi.staticfiles import StaticFiles
|
| 13 |
+
from fastapi.templating import Jinja2Templates
|
| 14 |
+
|
| 15 |
+
from app.engine.app_planner import AppPlanner
|
| 16 |
+
from app.engine.model_recommender import ModelRecommender
|
| 17 |
+
from app.codegen.repo_generator import RepoGenerator
|
| 18 |
+
from app.validators.code_checker import CodeChecker
|
| 19 |
+
|
| 20 |
+
app = FastAPI(title="AutoApp Builder", version="1.0.0")
|
| 21 |
+
|
| 22 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 23 |
+
GENERATED_DIR = Path("/tmp/autoapp_generated")
|
| 24 |
+
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
|
| 26 |
+
app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
|
| 27 |
+
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
|
| 28 |
+
|
| 29 |
+
# In-memory store for generated projects (keyed by session id)
|
| 30 |
+
projects: dict = {}
|
| 31 |
+
|
| 32 |
+
planner = AppPlanner()
|
| 33 |
+
recommender = ModelRecommender()
|
| 34 |
+
generator = RepoGenerator()
|
| 35 |
+
checker = CodeChecker()
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@app.get("/", response_class=HTMLResponse)
|
| 39 |
+
async def home(request: Request):
|
| 40 |
+
examples = [
|
| 41 |
+
{
|
| 42 |
+
"title": "Image Classifier",
|
| 43 |
+
"prompt": "Build a Gradio app that classifies images using a pretrained ResNet model. Users upload an image and get top-5 predictions with confidence bars.",
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"title": "AI Chatbot",
|
| 47 |
+
"prompt": "Create a chatbot that uses a Hugging Face language model to have conversations. Include chat history, system prompt configuration, and a clear button.",
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"title": "Text Summarizer",
|
| 51 |
+
"prompt": "Build an app that summarizes long text documents. Let users paste text or upload a .txt file, choose summary length (short/medium/long), and display the result.",
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"title": "Sentiment Dashboard",
|
| 55 |
+
"prompt": "Create an interactive sentiment analysis tool. Users enter text and see sentiment scores (positive/negative/neutral) visualized with charts.",
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"title": "REST API Service",
|
| 59 |
+
"prompt": "Build a Docker-based REST API that serves a text generation model with endpoints for completion, summarization, and translation. Include API docs.",
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"title": "Portfolio Site",
|
| 63 |
+
"prompt": "Create a beautiful static portfolio website for a data scientist. Include sections for projects, skills, publications, and contact info with a dark theme.",
|
| 64 |
+
},
|
| 65 |
+
]
|
| 66 |
+
return templates.TemplateResponse(
|
| 67 |
+
"home.html", {"request": request, "examples": examples}
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.post("/generate", response_class=HTMLResponse)
|
| 72 |
+
async def generate(
|
| 73 |
+
request: Request,
|
| 74 |
+
prompt: str = Form(...),
|
| 75 |
+
sdk_preference: str = Form("auto"),
|
| 76 |
+
model_size: str = Form("medium"),
|
| 77 |
+
gpu_needed: bool = Form(False),
|
| 78 |
+
features: str = Form(""),
|
| 79 |
+
):
|
| 80 |
+
try:
|
| 81 |
+
# Step 1: Plan the app
|
| 82 |
+
plan = planner.analyze(prompt, sdk_preference)
|
| 83 |
+
|
| 84 |
+
# Step 2: Recommend models
|
| 85 |
+
recommended_models = recommender.recommend(plan, model_size, gpu_needed)
|
| 86 |
+
plan["recommended_models"] = recommended_models
|
| 87 |
+
|
| 88 |
+
# Step 3: Parse additional features
|
| 89 |
+
feature_list = [f.strip() for f in features.split(",") if f.strip()]
|
| 90 |
+
plan["extra_features"] = feature_list
|
| 91 |
+
|
| 92 |
+
# Step 4: Generate repository files
|
| 93 |
+
repo_files = generator.generate(plan, prompt)
|
| 94 |
+
|
| 95 |
+
# Step 5: Validate the generated code
|
| 96 |
+
validation = checker.check(repo_files, plan["sdk"])
|
| 97 |
+
|
| 98 |
+
# Step 6: Store the project
|
| 99 |
+
project_id = str(uuid.uuid4())[:8]
|
| 100 |
+
projects[project_id] = {
|
| 101 |
+
"plan": plan,
|
| 102 |
+
"files": repo_files,
|
| 103 |
+
"validation": validation,
|
| 104 |
+
"prompt": prompt,
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
# Build file tree structure
|
| 108 |
+
file_tree = _build_file_tree(repo_files)
|
| 109 |
+
|
| 110 |
+
# Generate architecture diagram
|
| 111 |
+
arch_diagram = _generate_arch_diagram(plan)
|
| 112 |
+
|
| 113 |
+
return templates.TemplateResponse(
|
| 114 |
+
"result.html",
|
| 115 |
+
{
|
| 116 |
+
"request": request,
|
| 117 |
+
"project_id": project_id,
|
| 118 |
+
"plan": plan,
|
| 119 |
+
"files": repo_files,
|
| 120 |
+
"file_tree": file_tree,
|
| 121 |
+
"validation": validation,
|
| 122 |
+
"arch_diagram": arch_diagram,
|
| 123 |
+
"prompt": prompt,
|
| 124 |
+
},
|
| 125 |
+
)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
traceback.print_exc()
|
| 128 |
+
return templates.TemplateResponse(
|
| 129 |
+
"home.html",
|
| 130 |
+
{
|
| 131 |
+
"request": request,
|
| 132 |
+
"examples": [],
|
| 133 |
+
"error": f"Generation failed: {str(e)}. Please try again with a different prompt.",
|
| 134 |
+
},
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@app.post("/edit", response_class=HTMLResponse)
|
| 139 |
+
async def edit_project(
|
| 140 |
+
request: Request,
|
| 141 |
+
project_id: str = Form(...),
|
| 142 |
+
edit_prompt: str = Form(...),
|
| 143 |
+
):
|
| 144 |
+
if project_id not in projects:
|
| 145 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 146 |
+
|
| 147 |
+
project = projects[project_id]
|
| 148 |
+
original_plan = project["plan"]
|
| 149 |
+
original_files = project["files"]
|
| 150 |
+
|
| 151 |
+
try:
|
| 152 |
+
# Re-generate with edit instructions
|
| 153 |
+
updated_files = generator.edit(original_plan, original_files, edit_prompt)
|
| 154 |
+
|
| 155 |
+
validation = checker.check(updated_files, original_plan["sdk"])
|
| 156 |
+
|
| 157 |
+
project["files"] = updated_files
|
| 158 |
+
project["validation"] = validation
|
| 159 |
+
|
| 160 |
+
file_tree = _build_file_tree(updated_files)
|
| 161 |
+
arch_diagram = _generate_arch_diagram(original_plan)
|
| 162 |
+
|
| 163 |
+
return templates.TemplateResponse(
|
| 164 |
+
"result.html",
|
| 165 |
+
{
|
| 166 |
+
"request": request,
|
| 167 |
+
"project_id": project_id,
|
| 168 |
+
"plan": original_plan,
|
| 169 |
+
"files": updated_files,
|
| 170 |
+
"file_tree": file_tree,
|
| 171 |
+
"validation": validation,
|
| 172 |
+
"arch_diagram": arch_diagram,
|
| 173 |
+
"prompt": project["prompt"],
|
| 174 |
+
"edit_prompt": edit_prompt,
|
| 175 |
+
},
|
| 176 |
+
)
|
| 177 |
+
except Exception as e:
|
| 178 |
+
traceback.print_exc()
|
| 179 |
+
# Return original project with error
|
| 180 |
+
file_tree = _build_file_tree(original_files)
|
| 181 |
+
arch_diagram = _generate_arch_diagram(original_plan)
|
| 182 |
+
return templates.TemplateResponse(
|
| 183 |
+
"result.html",
|
| 184 |
+
{
|
| 185 |
+
"request": request,
|
| 186 |
+
"project_id": project_id,
|
| 187 |
+
"plan": original_plan,
|
| 188 |
+
"files": original_files,
|
| 189 |
+
"file_tree": file_tree,
|
| 190 |
+
"validation": project["validation"],
|
| 191 |
+
"arch_diagram": arch_diagram,
|
| 192 |
+
"prompt": project["prompt"],
|
| 193 |
+
"edit_error": f"Edit failed: {str(e)}",
|
| 194 |
+
},
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
@app.get("/download/{project_id}")
|
| 199 |
+
async def download_zip(project_id: str):
|
| 200 |
+
if project_id not in projects:
|
| 201 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 202 |
+
|
| 203 |
+
project = projects[project_id]
|
| 204 |
+
files = project["files"]
|
| 205 |
+
plan = project["plan"]
|
| 206 |
+
app_name = plan.get("app_name", "my-hf-space")
|
| 207 |
+
|
| 208 |
+
buf = io.BytesIO()
|
| 209 |
+
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 210 |
+
for filepath, content in files.items():
|
| 211 |
+
zf.writestr(f"{app_name}/{filepath}", content)
|
| 212 |
+
|
| 213 |
+
buf.seek(0)
|
| 214 |
+
return StreamingResponse(
|
| 215 |
+
buf,
|
| 216 |
+
media_type="application/zip",
|
| 217 |
+
headers={"Content-Disposition": f'attachment; filename="{app_name}.zip"'},
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
@app.get("/api/file/{project_id}/{filepath:path}")
|
| 222 |
+
async def get_file(project_id: str, filepath: str):
|
| 223 |
+
if project_id not in projects:
|
| 224 |
+
raise HTTPException(status_code=404, detail="Project not found")
|
| 225 |
+
|
| 226 |
+
files = projects[project_id]["files"]
|
| 227 |
+
if filepath not in files:
|
| 228 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 229 |
+
|
| 230 |
+
return JSONResponse({"filename": filepath, "content": files[filepath]})
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
def _build_file_tree(files: dict) -> list:
|
| 234 |
+
"""Build a nested file tree structure from flat file dict."""
|
| 235 |
+
tree = []
|
| 236 |
+
dirs_seen = set()
|
| 237 |
+
sorted_files = sorted(files.keys())
|
| 238 |
+
|
| 239 |
+
for filepath in sorted_files:
|
| 240 |
+
parts = filepath.split("/")
|
| 241 |
+
# Add directory entries
|
| 242 |
+
for i in range(len(parts) - 1):
|
| 243 |
+
dir_path = "/".join(parts[: i + 1])
|
| 244 |
+
if dir_path not in dirs_seen:
|
| 245 |
+
dirs_seen.add(dir_path)
|
| 246 |
+
tree.append(
|
| 247 |
+
{
|
| 248 |
+
"path": dir_path,
|
| 249 |
+
"name": parts[i],
|
| 250 |
+
"type": "dir",
|
| 251 |
+
"depth": i,
|
| 252 |
+
}
|
| 253 |
+
)
|
| 254 |
+
# Add file entry
|
| 255 |
+
tree.append(
|
| 256 |
+
{
|
| 257 |
+
"path": filepath,
|
| 258 |
+
"name": parts[-1],
|
| 259 |
+
"type": "file",
|
| 260 |
+
"depth": len(parts) - 1,
|
| 261 |
+
}
|
| 262 |
+
)
|
| 263 |
+
|
| 264 |
+
return tree
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _generate_arch_diagram(plan: dict) -> str:
|
| 268 |
+
"""Generate ASCII architecture diagram."""
|
| 269 |
+
sdk = plan.get("sdk", "gradio")
|
| 270 |
+
app_name = plan.get("app_name", "App")
|
| 271 |
+
components = plan.get("components", [])
|
| 272 |
+
|
| 273 |
+
if sdk == "gradio":
|
| 274 |
+
diagram = f"""
|
| 275 |
+
+------------------------------------------------------+
|
| 276 |
+
| Hugging Face Spaces |
|
| 277 |
+
| +------------------------------------------------+ |
|
| 278 |
+
| | {app_name:^30s} | |
|
| 279 |
+
| | +------------------------------------------+ | |
|
| 280 |
+
| | | Gradio Interface | | |
|
| 281 |
+
| | | +------------+ +------------------+ | | |
|
| 282 |
+
| | | | Inputs |--->| Processing | | | |
|
| 283 |
+
| | | +------------+ | +------------+ | | | |
|
| 284 |
+
| | | | | HF Model | | | | |
|
| 285 |
+
| | | +------------+ | +------------+ | | | |
|
| 286 |
+
| | | | Outputs |<---| | | | |
|
| 287 |
+
| | | +------------+ +------------------+ | | |
|
| 288 |
+
| | +------------------------------------------+ | |
|
| 289 |
+
| +------------------------------------------------+ |
|
| 290 |
+
+------------------------------------------------------+"""
|
| 291 |
+
elif sdk == "docker":
|
| 292 |
+
diagram = f"""
|
| 293 |
+
+------------------------------------------------------+
|
| 294 |
+
| Hugging Face Spaces |
|
| 295 |
+
| +------------------------------------------------+ |
|
| 296 |
+
| | Docker Container | |
|
| 297 |
+
| | +------------------------------------------+ | |
|
| 298 |
+
| | | {app_name:^30s} | | |
|
| 299 |
+
| | | +----------+ +----------+ +---------+ | | |
|
| 300 |
+
| | | | FastAPI | | Model | | Utils | | | |
|
| 301 |
+
| | | | Routes | | Service | | | | | |
|
| 302 |
+
| | | +-----+-----+ +----+-----+ +---------+ | | |
|
| 303 |
+
| | | | | | | |
|
| 304 |
+
| | | +-----v--------------v-----------------+ | | |
|
| 305 |
+
| | | | API Endpoints | | | |
|
| 306 |
+
| | | +---------------------------------------+ | | |
|
| 307 |
+
| | +------------------------------------------+ | |
|
| 308 |
+
| +------------------------------------------------+ |
|
| 309 |
+
+------------------------------------------------------+"""
|
| 310 |
+
else:
|
| 311 |
+
diagram = f"""
|
| 312 |
+
+------------------------------------------------------+
|
| 313 |
+
| Hugging Face Spaces |
|
| 314 |
+
| +------------------------------------------------+ |
|
| 315 |
+
| | Static Site | |
|
| 316 |
+
| | +------------------------------------------+ | |
|
| 317 |
+
| | | {app_name:^30s} | | |
|
| 318 |
+
| | | +----------+ +----------+ +---------+ | | |
|
| 319 |
+
| | | | HTML | | CSS | | JS | | | |
|
| 320 |
+
| | | | Pages | | Styles | | Scripts | | | |
|
| 321 |
+
| | | +----------+ +----------+ +---------+ | | |
|
| 322 |
+
| | +------------------------------------------+ | |
|
| 323 |
+
| +------------------------------------------------+ |
|
| 324 |
+
+------------------------------------------------------+"""
|
| 325 |
+
|
| 326 |
+
return diagram
|
app/static/app.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AutoApp Builder - Frontend JavaScript
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
// ─── Character Counter ──────────────────────────────────────────────
|
| 6 |
+
const promptInput = document.getElementById('promptInput');
|
| 7 |
+
const charCount = document.getElementById('charCount');
|
| 8 |
+
|
| 9 |
+
if (promptInput && charCount) {
|
| 10 |
+
promptInput.addEventListener('input', () => {
|
| 11 |
+
const len = promptInput.value.length;
|
| 12 |
+
charCount.textContent = `${len} / 2000`;
|
| 13 |
+
if (len > 1800) {
|
| 14 |
+
charCount.classList.add('text-yellow-500');
|
| 15 |
+
} else {
|
| 16 |
+
charCount.classList.remove('text-yellow-500');
|
| 17 |
+
}
|
| 18 |
+
});
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
// ─── Example Buttons ────────────────────────────────────────────────
|
| 22 |
+
document.querySelectorAll('.example-btn').forEach(btn => {
|
| 23 |
+
btn.addEventListener('click', () => {
|
| 24 |
+
const prompt = btn.getAttribute('data-prompt');
|
| 25 |
+
if (promptInput && prompt) {
|
| 26 |
+
promptInput.value = prompt;
|
| 27 |
+
promptInput.dispatchEvent(new Event('input'));
|
| 28 |
+
promptInput.focus();
|
| 29 |
+
// Scroll to textarea
|
| 30 |
+
promptInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
| 31 |
+
}
|
| 32 |
+
});
|
| 33 |
+
});
|
| 34 |
+
|
| 35 |
+
// ─── SDK Preference Toggle ──────────────────────────────────────────
|
| 36 |
+
document.querySelectorAll('.sdk-option').forEach(label => {
|
| 37 |
+
label.addEventListener('click', () => {
|
| 38 |
+
// Remove active styles from all
|
| 39 |
+
document.querySelectorAll('.sdk-option').forEach(l => {
|
| 40 |
+
l.classList.remove('bg-cyan-500/20', 'text-cyan-400');
|
| 41 |
+
l.classList.add('text-dark-400');
|
| 42 |
+
});
|
| 43 |
+
// Add active style to clicked
|
| 44 |
+
label.classList.add('bg-cyan-500/20', 'text-cyan-400');
|
| 45 |
+
label.classList.remove('text-dark-400');
|
| 46 |
+
});
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
// ─── Advanced Options Toggle ────────────────────────────────────────
|
| 50 |
+
const toggleAdvanced = document.getElementById('toggleAdvanced');
|
| 51 |
+
const advancedPanel = document.getElementById('advancedPanel');
|
| 52 |
+
const advancedArrow = document.getElementById('advancedArrow');
|
| 53 |
+
|
| 54 |
+
if (toggleAdvanced && advancedPanel) {
|
| 55 |
+
toggleAdvanced.addEventListener('click', () => {
|
| 56 |
+
advancedPanel.classList.toggle('hidden');
|
| 57 |
+
if (advancedArrow) {
|
| 58 |
+
advancedArrow.style.transform = advancedPanel.classList.contains('hidden')
|
| 59 |
+
? 'rotate(0deg)'
|
| 60 |
+
: 'rotate(90deg)';
|
| 61 |
+
}
|
| 62 |
+
});
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
// ─── Form Submission Loading State ──────────────────────────────────
|
| 66 |
+
const generateForm = document.getElementById('generateForm');
|
| 67 |
+
if (generateForm) {
|
| 68 |
+
generateForm.addEventListener('submit', () => {
|
| 69 |
+
const btn = document.getElementById('submitBtn');
|
| 70 |
+
const text = document.getElementById('submitText');
|
| 71 |
+
const spinner = document.getElementById('submitSpinner');
|
| 72 |
+
if (btn) btn.disabled = true;
|
| 73 |
+
if (text) text.textContent = 'Generating...';
|
| 74 |
+
if (spinner) spinner.classList.remove('hidden');
|
| 75 |
+
btn.classList.add('opacity-75', 'cursor-not-allowed');
|
| 76 |
+
});
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
const editForm = document.getElementById('editForm');
|
| 80 |
+
if (editForm) {
|
| 81 |
+
editForm.addEventListener('submit', () => {
|
| 82 |
+
const btn = document.getElementById('editBtn');
|
| 83 |
+
const text = document.getElementById('editText');
|
| 84 |
+
const spinner = document.getElementById('editSpinner');
|
| 85 |
+
if (btn) btn.disabled = true;
|
| 86 |
+
if (text) text.textContent = 'Editing...';
|
| 87 |
+
if (spinner) spinner.classList.remove('hidden');
|
| 88 |
+
btn.classList.add('opacity-75', 'cursor-not-allowed');
|
| 89 |
+
});
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
// ─── File Selection & Code Preview (Result Page) ────────────────────
|
| 93 |
+
function selectFile(filepath) {
|
| 94 |
+
if (typeof fileData === 'undefined') return;
|
| 95 |
+
|
| 96 |
+
const content = fileData[filepath];
|
| 97 |
+
if (content === undefined) return;
|
| 98 |
+
|
| 99 |
+
// Update active state in tree
|
| 100 |
+
document.querySelectorAll('.file-tree-item').forEach(item => {
|
| 101 |
+
item.classList.remove('active');
|
| 102 |
+
});
|
| 103 |
+
const activeItem = document.querySelector(`[data-filepath="${filepath}"]`);
|
| 104 |
+
if (activeItem) activeItem.classList.add('active');
|
| 105 |
+
|
| 106 |
+
// Update filename display
|
| 107 |
+
const fileNameEl = document.getElementById('currentFileName');
|
| 108 |
+
if (fileNameEl) fileNameEl.textContent = filepath;
|
| 109 |
+
|
| 110 |
+
// Determine language for syntax highlighting
|
| 111 |
+
const lang = getLanguage(filepath);
|
| 112 |
+
|
| 113 |
+
// Show code block, hide placeholder
|
| 114 |
+
const placeholder = document.getElementById('codePlaceholder');
|
| 115 |
+
const codeBlock = document.getElementById('codeBlock');
|
| 116 |
+
const codeContent = document.getElementById('codeContent');
|
| 117 |
+
|
| 118 |
+
if (placeholder) placeholder.classList.add('hidden');
|
| 119 |
+
if (codeBlock) codeBlock.classList.remove('hidden');
|
| 120 |
+
|
| 121 |
+
if (codeContent) {
|
| 122 |
+
// Escape HTML entities
|
| 123 |
+
codeContent.textContent = content;
|
| 124 |
+
codeContent.className = `language-${lang}`;
|
| 125 |
+
|
| 126 |
+
// Re-highlight
|
| 127 |
+
if (window.Prism) {
|
| 128 |
+
Prism.highlightElement(codeContent);
|
| 129 |
+
}
|
| 130 |
+
}
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
function getLanguage(filepath) {
|
| 134 |
+
const ext = filepath.split('.').pop().toLowerCase();
|
| 135 |
+
const langMap = {
|
| 136 |
+
'py': 'python',
|
| 137 |
+
'js': 'javascript',
|
| 138 |
+
'ts': 'javascript',
|
| 139 |
+
'html': 'markup',
|
| 140 |
+
'htm': 'markup',
|
| 141 |
+
'css': 'css',
|
| 142 |
+
'md': 'markdown',
|
| 143 |
+
'yaml': 'yaml',
|
| 144 |
+
'yml': 'yaml',
|
| 145 |
+
'json': 'javascript',
|
| 146 |
+
'sh': 'bash',
|
| 147 |
+
'bash': 'bash',
|
| 148 |
+
'dockerfile': 'docker',
|
| 149 |
+
'txt': 'text',
|
| 150 |
+
'toml': 'text',
|
| 151 |
+
'cfg': 'text',
|
| 152 |
+
'ini': 'text',
|
| 153 |
+
'gitignore': 'text',
|
| 154 |
+
};
|
| 155 |
+
|
| 156 |
+
// Handle Dockerfile specifically
|
| 157 |
+
if (filepath.toLowerCase() === 'dockerfile') return 'docker';
|
| 158 |
+
|
| 159 |
+
return langMap[ext] || 'text';
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// ─── Copy Code ──────────────────────────────────────────────────────
|
| 163 |
+
function copyCode() {
|
| 164 |
+
const codeContent = document.getElementById('codeContent');
|
| 165 |
+
if (!codeContent) return;
|
| 166 |
+
|
| 167 |
+
const text = codeContent.textContent;
|
| 168 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 169 |
+
const btn = document.getElementById('copyBtn');
|
| 170 |
+
if (btn) {
|
| 171 |
+
const originalHTML = btn.innerHTML;
|
| 172 |
+
btn.innerHTML = '<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg> Copied!';
|
| 173 |
+
btn.classList.add('text-emerald-400');
|
| 174 |
+
setTimeout(() => {
|
| 175 |
+
btn.innerHTML = originalHTML;
|
| 176 |
+
btn.classList.remove('text-emerald-400');
|
| 177 |
+
}, 2000);
|
| 178 |
+
}
|
| 179 |
+
}).catch(() => {
|
| 180 |
+
// Fallback: select text
|
| 181 |
+
const range = document.createRange();
|
| 182 |
+
range.selectNodeContents(codeContent);
|
| 183 |
+
const sel = window.getSelection();
|
| 184 |
+
sel.removeAllRanges();
|
| 185 |
+
sel.addRange(range);
|
| 186 |
+
});
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
// ─── Architecture Diagram Toggle ────────────────────────────────────
|
| 190 |
+
function toggleDiagram() {
|
| 191 |
+
const panel = document.getElementById('diagramPanel');
|
| 192 |
+
const toggle = document.getElementById('diagramToggle');
|
| 193 |
+
if (panel) {
|
| 194 |
+
panel.classList.toggle('hidden');
|
| 195 |
+
if (toggle) {
|
| 196 |
+
toggle.textContent = panel.classList.contains('hidden') ? 'Show' : 'Hide';
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
// ─── Keyboard Shortcuts ─────────────────────────────────────────────
|
| 202 |
+
document.addEventListener('keydown', (e) => {
|
| 203 |
+
// Ctrl/Cmd + Enter to submit form
|
| 204 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
| 205 |
+
const form = document.getElementById('generateForm') || document.getElementById('editForm');
|
| 206 |
+
if (form) {
|
| 207 |
+
form.dispatchEvent(new Event('submit', { bubbles: true }));
|
| 208 |
+
form.submit();
|
| 209 |
+
}
|
| 210 |
+
}
|
| 211 |
+
});
|
app/templates/base.html
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en" class="dark">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}AutoApp Builder{% endblock %}</title>
|
| 7 |
+
|
| 8 |
+
<!-- TailwindCSS -->
|
| 9 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<script>
|
| 11 |
+
tailwind.config = {
|
| 12 |
+
darkMode: 'class',
|
| 13 |
+
theme: {
|
| 14 |
+
extend: {
|
| 15 |
+
colors: {
|
| 16 |
+
dark: {
|
| 17 |
+
50: '#f8fafc',
|
| 18 |
+
100: '#f1f5f9',
|
| 19 |
+
200: '#e2e8f0',
|
| 20 |
+
300: '#cbd5e1',
|
| 21 |
+
400: '#94a3b8',
|
| 22 |
+
500: '#64748b',
|
| 23 |
+
600: '#475569',
|
| 24 |
+
700: '#334155',
|
| 25 |
+
800: '#1e293b',
|
| 26 |
+
900: '#0f172a',
|
| 27 |
+
950: '#020617',
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
animation: {
|
| 31 |
+
'gradient': 'gradient 6s ease infinite',
|
| 32 |
+
'float': 'float 6s ease-in-out infinite',
|
| 33 |
+
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 34 |
+
'fade-in': 'fadeIn 0.5s ease forwards',
|
| 35 |
+
'slide-up': 'slideUp 0.5s ease forwards',
|
| 36 |
+
},
|
| 37 |
+
keyframes: {
|
| 38 |
+
gradient: {
|
| 39 |
+
'0%, 100%': { backgroundPosition: '0% 50%' },
|
| 40 |
+
'50%': { backgroundPosition: '100% 50%' },
|
| 41 |
+
},
|
| 42 |
+
float: {
|
| 43 |
+
'0%, 100%': { transform: 'translateY(0px)' },
|
| 44 |
+
'50%': { transform: 'translateY(-10px)' },
|
| 45 |
+
},
|
| 46 |
+
fadeIn: {
|
| 47 |
+
from: { opacity: '0' },
|
| 48 |
+
to: { opacity: '1' },
|
| 49 |
+
},
|
| 50 |
+
slideUp: {
|
| 51 |
+
from: { opacity: '0', transform: 'translateY(20px)' },
|
| 52 |
+
to: { opacity: '1', transform: 'translateY(0)' },
|
| 53 |
+
},
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
</script>
|
| 59 |
+
|
| 60 |
+
<!-- Prism.js for code highlighting -->
|
| 61 |
+
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">
|
| 62 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>
|
| 63 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-python.min.js"></script>
|
| 64 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-bash.min.js"></script>
|
| 65 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-docker.min.js"></script>
|
| 66 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-yaml.min.js"></script>
|
| 67 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markdown.min.js"></script>
|
| 68 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-markup.min.js"></script>
|
| 69 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-css.min.js"></script>
|
| 70 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-javascript.min.js"></script>
|
| 71 |
+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.js"></script>
|
| 72 |
+
<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/line-numbers/prism-line-numbers.min.css" rel="stylesheet">
|
| 73 |
+
|
| 74 |
+
<style>
|
| 75 |
+
/* Custom scrollbar */
|
| 76 |
+
::-webkit-scrollbar { width: 8px; height: 8px; }
|
| 77 |
+
::-webkit-scrollbar-track { background: #0f172a; }
|
| 78 |
+
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
|
| 79 |
+
::-webkit-scrollbar-thumb:hover { background: #475569; }
|
| 80 |
+
|
| 81 |
+
/* Glassmorphism */
|
| 82 |
+
.glass {
|
| 83 |
+
background: rgba(15, 23, 42, 0.7);
|
| 84 |
+
backdrop-filter: blur(16px);
|
| 85 |
+
-webkit-backdrop-filter: blur(16px);
|
| 86 |
+
border: 1px solid rgba(56, 189, 248, 0.1);
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.glass-light {
|
| 90 |
+
background: rgba(30, 41, 59, 0.5);
|
| 91 |
+
backdrop-filter: blur(12px);
|
| 92 |
+
-webkit-backdrop-filter: blur(12px);
|
| 93 |
+
border: 1px solid rgba(56, 189, 248, 0.08);
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
/* Gradient text */
|
| 97 |
+
.gradient-text {
|
| 98 |
+
background: linear-gradient(135deg, #38bdf8, #818cf8, #c084fc);
|
| 99 |
+
-webkit-background-clip: text;
|
| 100 |
+
-webkit-text-fill-color: transparent;
|
| 101 |
+
background-clip: text;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
/* Animated gradient background */
|
| 105 |
+
.animated-gradient {
|
| 106 |
+
background: linear-gradient(-45deg, #0f172a, #1e1b4b, #172554, #0f172a);
|
| 107 |
+
background-size: 400% 400%;
|
| 108 |
+
animation: gradient 6s ease infinite;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* Glow effects */
|
| 112 |
+
.glow-blue { box-shadow: 0 0 20px rgba(56, 189, 248, 0.15); }
|
| 113 |
+
.glow-purple { box-shadow: 0 0 20px rgba(139, 92, 246, 0.15); }
|
| 114 |
+
|
| 115 |
+
/* Code preview overrides */
|
| 116 |
+
pre[class*="language-"] {
|
| 117 |
+
background: #0d1117 !important;
|
| 118 |
+
border-radius: 8px;
|
| 119 |
+
font-size: 0.85rem;
|
| 120 |
+
margin: 0;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
code[class*="language-"] {
|
| 124 |
+
font-family: 'JetBrains Mono', 'Fira Code', monospace;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* Loading spinner */
|
| 128 |
+
.spinner {
|
| 129 |
+
border: 3px solid rgba(56, 189, 248, 0.2);
|
| 130 |
+
border-top: 3px solid #38bdf8;
|
| 131 |
+
border-radius: 50%;
|
| 132 |
+
width: 24px;
|
| 133 |
+
height: 24px;
|
| 134 |
+
animation: spin 0.8s linear infinite;
|
| 135 |
+
}
|
| 136 |
+
@keyframes spin {
|
| 137 |
+
to { transform: rotate(360deg); }
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* File tree styling */
|
| 141 |
+
.file-tree-item { transition: all 0.15s ease; }
|
| 142 |
+
.file-tree-item:hover { background: rgba(56, 189, 248, 0.08); }
|
| 143 |
+
.file-tree-item.active { background: rgba(56, 189, 248, 0.15); border-left: 2px solid #38bdf8; }
|
| 144 |
+
|
| 145 |
+
/* Textarea styling */
|
| 146 |
+
textarea:focus { outline: none; }
|
| 147 |
+
|
| 148 |
+
/* Smooth transitions */
|
| 149 |
+
.transition-smooth { transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
|
| 150 |
+
</style>
|
| 151 |
+
|
| 152 |
+
{% block extra_head %}{% endblock %}
|
| 153 |
+
</head>
|
| 154 |
+
<body class="bg-dark-950 text-dark-200 min-h-screen">
|
| 155 |
+
<!-- Navigation -->
|
| 156 |
+
<nav class="glass sticky top-0 z-50 px-6 py-3">
|
| 157 |
+
<div class="max-w-7xl mx-auto flex items-center justify-between">
|
| 158 |
+
<a href="/" class="flex items-center gap-3 group">
|
| 159 |
+
<div class="w-9 h-9 rounded-lg bg-gradient-to-br from-cyan-500 to-blue-600 flex items-center justify-center text-white font-bold text-sm group-hover:scale-110 transition-transform">
|
| 160 |
+
AB
|
| 161 |
+
</div>
|
| 162 |
+
<span class="text-lg font-bold gradient-text">AutoApp Builder</span>
|
| 163 |
+
</a>
|
| 164 |
+
<div class="flex items-center gap-4">
|
| 165 |
+
<a href="/" class="text-dark-400 hover:text-cyan-400 transition-colors text-sm font-medium">New Project</a>
|
| 166 |
+
<a href="https://huggingface.co/spaces" target="_blank" class="text-dark-400 hover:text-cyan-400 transition-colors text-sm font-medium">HF Spaces</a>
|
| 167 |
+
</div>
|
| 168 |
+
</div>
|
| 169 |
+
</nav>
|
| 170 |
+
|
| 171 |
+
<!-- Main Content -->
|
| 172 |
+
<main>
|
| 173 |
+
{% block content %}{% endblock %}
|
| 174 |
+
</main>
|
| 175 |
+
|
| 176 |
+
<!-- Footer -->
|
| 177 |
+
<footer class="border-t border-dark-800 mt-16 py-8 px-6">
|
| 178 |
+
<div class="max-w-7xl mx-auto text-center text-dark-500 text-sm">
|
| 179 |
+
<p>AutoApp Builder — Generate complete HF Space repos from natural language.</p>
|
| 180 |
+
<p class="mt-1">Powered by Hugging Face Inference API</p>
|
| 181 |
+
</div>
|
| 182 |
+
</footer>
|
| 183 |
+
|
| 184 |
+
<script src="/static/app.js"></script>
|
| 185 |
+
{% block extra_scripts %}{% endblock %}
|
| 186 |
+
</body>
|
| 187 |
+
</html>
|
app/templates/home.html
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}AutoApp Builder - Generate HF Spaces from Prompts{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<!-- Hero Section -->
|
| 7 |
+
<section class="animated-gradient py-20 px-6 relative overflow-hidden">
|
| 8 |
+
<!-- Background decorations -->
|
| 9 |
+
<div class="absolute inset-0 overflow-hidden pointer-events-none">
|
| 10 |
+
<div class="absolute top-20 left-10 w-72 h-72 bg-cyan-500/10 rounded-full blur-3xl animate-float"></div>
|
| 11 |
+
<div class="absolute bottom-10 right-10 w-96 h-96 bg-purple-500/10 rounded-full blur-3xl animate-float" style="animation-delay: -3s;"></div>
|
| 12 |
+
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[600px] bg-blue-500/5 rounded-full blur-3xl"></div>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
<div class="max-w-4xl mx-auto text-center relative z-10">
|
| 16 |
+
<div class="inline-flex items-center gap-2 px-4 py-1.5 rounded-full glass-light text-cyan-400 text-sm font-medium mb-6">
|
| 17 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
| 18 |
+
LLM-Powered Code Generation
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<h1 class="text-5xl md:text-6xl font-extrabold mb-6 leading-tight">
|
| 22 |
+
<span class="gradient-text">Build HF Spaces</span><br>
|
| 23 |
+
<span class="text-dark-200">From a Simple Prompt</span>
|
| 24 |
+
</h1>
|
| 25 |
+
|
| 26 |
+
<p class="text-xl text-dark-400 max-w-2xl mx-auto mb-10 leading-relaxed">
|
| 27 |
+
Describe your app idea in plain English. AutoApp Builder generates a complete,
|
| 28 |
+
working Hugging Face Space repository — ready to deploy.
|
| 29 |
+
</p>
|
| 30 |
+
|
| 31 |
+
<!-- Stats -->
|
| 32 |
+
<div class="flex items-center justify-center gap-8 mb-10 text-sm">
|
| 33 |
+
<div class="flex items-center gap-2 text-dark-400">
|
| 34 |
+
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 35 |
+
Gradio Apps
|
| 36 |
+
</div>
|
| 37 |
+
<div class="flex items-center gap-2 text-dark-400">
|
| 38 |
+
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 39 |
+
Docker APIs
|
| 40 |
+
</div>
|
| 41 |
+
<div class="flex items-center gap-2 text-dark-400">
|
| 42 |
+
<svg class="w-5 h-5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 43 |
+
Static Sites
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</section>
|
| 48 |
+
|
| 49 |
+
<!-- Main Form -->
|
| 50 |
+
<section class="max-w-4xl mx-auto px-6 -mt-8 relative z-20">
|
| 51 |
+
{% if error %}
|
| 52 |
+
<div class="mb-6 p-4 rounded-xl bg-red-900/30 border border-red-500/30 text-red-300 animate-fade-in">
|
| 53 |
+
<div class="flex items-center gap-3">
|
| 54 |
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 55 |
+
<span>{{ error }}</span>
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
{% endif %}
|
| 59 |
+
|
| 60 |
+
<form id="generateForm" action="/generate" method="POST" class="glass rounded-2xl p-8 glow-blue">
|
| 61 |
+
<!-- Prompt Input -->
|
| 62 |
+
<div class="mb-6">
|
| 63 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">
|
| 64 |
+
Describe Your App
|
| 65 |
+
</label>
|
| 66 |
+
<div class="relative">
|
| 67 |
+
<textarea
|
| 68 |
+
name="prompt"
|
| 69 |
+
id="promptInput"
|
| 70 |
+
rows="4"
|
| 71 |
+
required
|
| 72 |
+
class="w-full bg-dark-900/80 border border-dark-700 rounded-xl px-5 py-4 text-dark-200 placeholder-dark-500 focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20 resize-none transition-all"
|
| 73 |
+
placeholder="e.g., Build a Gradio app that classifies images using a pretrained ResNet model..."
|
| 74 |
+
></textarea>
|
| 75 |
+
<!-- Character count -->
|
| 76 |
+
<div class="absolute bottom-3 right-3 text-xs text-dark-600" id="charCount">0 / 2000</div>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
|
| 80 |
+
<!-- Examples Dropdown -->
|
| 81 |
+
<div class="mb-6">
|
| 82 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">Quick Examples</label>
|
| 83 |
+
<div class="grid grid-cols-2 md:grid-cols-3 gap-2">
|
| 84 |
+
{% for example in examples %}
|
| 85 |
+
<button
|
| 86 |
+
type="button"
|
| 87 |
+
class="example-btn text-left p-3 rounded-lg glass-light hover:border-cyan-500/30 transition-all text-sm group"
|
| 88 |
+
data-prompt="{{ example.prompt }}"
|
| 89 |
+
>
|
| 90 |
+
<span class="text-dark-300 group-hover:text-cyan-400 font-medium transition-colors">{{ example.title }}</span>
|
| 91 |
+
</button>
|
| 92 |
+
{% endfor %}
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
|
| 96 |
+
<!-- Options Row -->
|
| 97 |
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
| 98 |
+
<!-- SDK Preference -->
|
| 99 |
+
<div>
|
| 100 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">SDK Preference</label>
|
| 101 |
+
<div class="flex rounded-lg overflow-hidden border border-dark-700">
|
| 102 |
+
<label class="sdk-option flex-1 text-center py-2.5 cursor-pointer transition-all text-sm font-medium bg-cyan-500/20 text-cyan-400 border-r border-dark-700">
|
| 103 |
+
<input type="radio" name="sdk_preference" value="auto" checked class="hidden"> Auto
|
| 104 |
+
</label>
|
| 105 |
+
<label class="sdk-option flex-1 text-center py-2.5 cursor-pointer transition-all text-sm font-medium text-dark-400 hover:text-dark-200 border-r border-dark-700">
|
| 106 |
+
<input type="radio" name="sdk_preference" value="gradio" class="hidden"> Gradio
|
| 107 |
+
</label>
|
| 108 |
+
<label class="sdk-option flex-1 text-center py-2.5 cursor-pointer transition-all text-sm font-medium text-dark-400 hover:text-dark-200 border-r border-dark-700">
|
| 109 |
+
<input type="radio" name="sdk_preference" value="docker" class="hidden"> Docker
|
| 110 |
+
</label>
|
| 111 |
+
<label class="sdk-option flex-1 text-center py-2.5 cursor-pointer transition-all text-sm font-medium text-dark-400 hover:text-dark-200">
|
| 112 |
+
<input type="radio" name="sdk_preference" value="static" class="hidden"> Static
|
| 113 |
+
</label>
|
| 114 |
+
</div>
|
| 115 |
+
</div>
|
| 116 |
+
|
| 117 |
+
<!-- Model Size -->
|
| 118 |
+
<div>
|
| 119 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">Model Size</label>
|
| 120 |
+
<select name="model_size" class="w-full bg-dark-900/80 border border-dark-700 rounded-lg px-4 py-2.5 text-dark-300 text-sm focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20">
|
| 121 |
+
<option value="small">Small (fastest)</option>
|
| 122 |
+
<option value="medium" selected>Medium (balanced)</option>
|
| 123 |
+
<option value="large">Large (best quality)</option>
|
| 124 |
+
</select>
|
| 125 |
+
</div>
|
| 126 |
+
|
| 127 |
+
<!-- GPU Toggle -->
|
| 128 |
+
<div>
|
| 129 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">GPU Required</label>
|
| 130 |
+
<label class="flex items-center gap-3 bg-dark-900/80 border border-dark-700 rounded-lg px-4 py-2.5 cursor-pointer">
|
| 131 |
+
<input type="checkbox" name="gpu_needed" value="true" class="w-4 h-4 rounded border-dark-600 text-cyan-500 focus:ring-cyan-500/30 bg-dark-800">
|
| 132 |
+
<span class="text-dark-400 text-sm">Enable GPU hardware</span>
|
| 133 |
+
</label>
|
| 134 |
+
</div>
|
| 135 |
+
</div>
|
| 136 |
+
|
| 137 |
+
<!-- Advanced: Extra Features -->
|
| 138 |
+
<div class="mb-6">
|
| 139 |
+
<button type="button" id="toggleAdvanced" class="text-sm text-dark-500 hover:text-cyan-400 transition-colors flex items-center gap-1">
|
| 140 |
+
<svg id="advancedArrow" class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
|
| 141 |
+
Advanced Options
|
| 142 |
+
</button>
|
| 143 |
+
<div id="advancedPanel" class="hidden mt-3">
|
| 144 |
+
<label class="block text-sm font-semibold text-dark-300 mb-2">Extra Features (comma-separated)</label>
|
| 145 |
+
<input
|
| 146 |
+
type="text"
|
| 147 |
+
name="features"
|
| 148 |
+
class="w-full bg-dark-900/80 border border-dark-700 rounded-lg px-4 py-2.5 text-dark-300 placeholder-dark-600 text-sm focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20"
|
| 149 |
+
placeholder="e.g., dark theme, error handling, caching, rate limiting"
|
| 150 |
+
>
|
| 151 |
+
</div>
|
| 152 |
+
</div>
|
| 153 |
+
|
| 154 |
+
<!-- Submit Button -->
|
| 155 |
+
<button
|
| 156 |
+
type="submit"
|
| 157 |
+
id="submitBtn"
|
| 158 |
+
class="w-full py-4 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-bold text-lg transition-all hover:shadow-lg hover:shadow-cyan-500/20 flex items-center justify-center gap-3"
|
| 159 |
+
>
|
| 160 |
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
| 161 |
+
<span id="submitText">Generate Space</span>
|
| 162 |
+
<div id="submitSpinner" class="spinner hidden"></div>
|
| 163 |
+
</button>
|
| 164 |
+
</form>
|
| 165 |
+
</section>
|
| 166 |
+
|
| 167 |
+
<!-- How It Works -->
|
| 168 |
+
<section class="max-w-5xl mx-auto px-6 py-20">
|
| 169 |
+
<h2 class="text-3xl font-bold text-center mb-12 gradient-text">How It Works</h2>
|
| 170 |
+
<div class="grid md:grid-cols-4 gap-6">
|
| 171 |
+
<div class="text-center animate-slide-up" style="animation-delay: 0.1s;">
|
| 172 |
+
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-cyan-500/20 to-blue-500/20 flex items-center justify-center mx-auto mb-4 text-2xl">1</div>
|
| 173 |
+
<h3 class="font-semibold text-dark-200 mb-2">Describe</h3>
|
| 174 |
+
<p class="text-dark-500 text-sm">Write a plain-English description of your app idea</p>
|
| 175 |
+
</div>
|
| 176 |
+
<div class="text-center animate-slide-up" style="animation-delay: 0.2s;">
|
| 177 |
+
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-500/20 to-purple-500/20 flex items-center justify-center mx-auto mb-4 text-2xl">2</div>
|
| 178 |
+
<h3 class="font-semibold text-dark-200 mb-2">Analyze</h3>
|
| 179 |
+
<p class="text-dark-500 text-sm">AI selects the best SDK, models, and architecture</p>
|
| 180 |
+
</div>
|
| 181 |
+
<div class="text-center animate-slide-up" style="animation-delay: 0.3s;">
|
| 182 |
+
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-purple-500/20 to-pink-500/20 flex items-center justify-center mx-auto mb-4 text-2xl">3</div>
|
| 183 |
+
<h3 class="font-semibold text-dark-200 mb-2">Generate</h3>
|
| 184 |
+
<p class="text-dark-500 text-sm">LLM generates complete, working code for every file</p>
|
| 185 |
+
</div>
|
| 186 |
+
<div class="text-center animate-slide-up" style="animation-delay: 0.4s;">
|
| 187 |
+
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-pink-500/20 to-red-500/20 flex items-center justify-center mx-auto mb-4 text-2xl">4</div>
|
| 188 |
+
<h3 class="font-semibold text-dark-200 mb-2">Deploy</h3>
|
| 189 |
+
<p class="text-dark-500 text-sm">Download the ZIP and push to Hugging Face Spaces</p>
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
</section>
|
| 193 |
+
|
| 194 |
+
<!-- Supported SDK cards -->
|
| 195 |
+
<section class="max-w-5xl mx-auto px-6 pb-20">
|
| 196 |
+
<h2 class="text-3xl font-bold text-center mb-12 gradient-text">Supported SDKs</h2>
|
| 197 |
+
<div class="grid md:grid-cols-3 gap-6">
|
| 198 |
+
<div class="glass rounded-xl p-6 hover:glow-blue transition-all group">
|
| 199 |
+
<div class="flex items-center gap-3 mb-4">
|
| 200 |
+
<div class="w-10 h-10 rounded-lg bg-orange-500/20 flex items-center justify-center text-orange-400 text-lg font-bold">G</div>
|
| 201 |
+
<h3 class="text-lg font-bold text-dark-200">Gradio</h3>
|
| 202 |
+
</div>
|
| 203 |
+
<p class="text-dark-400 text-sm leading-relaxed mb-4">Interactive ML demos, chatbots, image classifiers, audio/video processing, and more.</p>
|
| 204 |
+
<div class="flex flex-wrap gap-1.5">
|
| 205 |
+
<span class="text-xs px-2 py-1 rounded-full bg-orange-500/10 text-orange-400">ML Demos</span>
|
| 206 |
+
<span class="text-xs px-2 py-1 rounded-full bg-orange-500/10 text-orange-400">Chatbots</span>
|
| 207 |
+
<span class="text-xs px-2 py-1 rounded-full bg-orange-500/10 text-orange-400">Vision</span>
|
| 208 |
+
</div>
|
| 209 |
+
</div>
|
| 210 |
+
<div class="glass rounded-xl p-6 hover:glow-blue transition-all group">
|
| 211 |
+
<div class="flex items-center gap-3 mb-4">
|
| 212 |
+
<div class="w-10 h-10 rounded-lg bg-blue-500/20 flex items-center justify-center text-blue-400 text-lg font-bold">D</div>
|
| 213 |
+
<h3 class="text-lg font-bold text-dark-200">Docker</h3>
|
| 214 |
+
</div>
|
| 215 |
+
<p class="text-dark-400 text-sm leading-relaxed mb-4">REST APIs, custom backends, agents, websocket apps, complex multi-service workflows.</p>
|
| 216 |
+
<div class="flex flex-wrap gap-1.5">
|
| 217 |
+
<span class="text-xs px-2 py-1 rounded-full bg-blue-500/10 text-blue-400">APIs</span>
|
| 218 |
+
<span class="text-xs px-2 py-1 rounded-full bg-blue-500/10 text-blue-400">Agents</span>
|
| 219 |
+
<span class="text-xs px-2 py-1 rounded-full bg-blue-500/10 text-blue-400">Backends</span>
|
| 220 |
+
</div>
|
| 221 |
+
</div>
|
| 222 |
+
<div class="glass rounded-xl p-6 hover:glow-blue transition-all group">
|
| 223 |
+
<div class="flex items-center gap-3 mb-4">
|
| 224 |
+
<div class="w-10 h-10 rounded-lg bg-emerald-500/20 flex items-center justify-center text-emerald-400 text-lg font-bold">S</div>
|
| 225 |
+
<h3 class="text-lg font-bold text-dark-200">Static</h3>
|
| 226 |
+
</div>
|
| 227 |
+
<p class="text-dark-400 text-sm leading-relaxed mb-4">Landing pages, portfolios, documentation sites, galleries, and pure frontend apps.</p>
|
| 228 |
+
<div class="flex flex-wrap gap-1.5">
|
| 229 |
+
<span class="text-xs px-2 py-1 rounded-full bg-emerald-500/10 text-emerald-400">Portfolio</span>
|
| 230 |
+
<span class="text-xs px-2 py-1 rounded-full bg-emerald-500/10 text-emerald-400">Docs</span>
|
| 231 |
+
<span class="text-xs px-2 py-1 rounded-full bg-emerald-500/10 text-emerald-400">Showcase</span>
|
| 232 |
+
</div>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</section>
|
| 236 |
+
{% endblock %}
|
app/templates/result.html
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ plan.title }} - AutoApp Builder{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="max-w-7xl mx-auto px-6 py-8">
|
| 7 |
+
|
| 8 |
+
<!-- Header -->
|
| 9 |
+
<div class="flex flex-col md:flex-row items-start md:items-center justify-between gap-4 mb-8 animate-fade-in">
|
| 10 |
+
<div>
|
| 11 |
+
<div class="flex items-center gap-3 mb-2">
|
| 12 |
+
<span class="px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider
|
| 13 |
+
{% if plan.sdk == 'gradio' %}bg-orange-500/20 text-orange-400
|
| 14 |
+
{% elif plan.sdk == 'docker' %}bg-blue-500/20 text-blue-400
|
| 15 |
+
{% else %}bg-emerald-500/20 text-emerald-400{% endif %}">
|
| 16 |
+
{{ plan.sdk }}
|
| 17 |
+
</span>
|
| 18 |
+
<span class="px-3 py-1 rounded-full text-xs font-medium bg-dark-800 text-dark-400">
|
| 19 |
+
{{ plan.app_type }}
|
| 20 |
+
</span>
|
| 21 |
+
{% if validation.valid %}
|
| 22 |
+
<span class="px-3 py-1 rounded-full text-xs font-medium bg-emerald-500/20 text-emerald-400 flex items-center gap-1">
|
| 23 |
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
|
| 24 |
+
Valid
|
| 25 |
+
</span>
|
| 26 |
+
{% else %}
|
| 27 |
+
<span class="px-3 py-1 rounded-full text-xs font-medium bg-red-500/20 text-red-400 flex items-center gap-1">
|
| 28 |
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
| 29 |
+
Issues Found
|
| 30 |
+
</span>
|
| 31 |
+
{% endif %}
|
| 32 |
+
</div>
|
| 33 |
+
<h1 class="text-3xl font-bold gradient-text">{{ plan.title }}</h1>
|
| 34 |
+
<p class="text-dark-400 mt-1 text-sm">{{ plan.description }}</p>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="flex items-center gap-3">
|
| 37 |
+
<a href="/download/{{ project_id }}" class="flex items-center gap-2 px-5 py-2.5 rounded-xl bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500 text-white font-semibold text-sm transition-all hover:shadow-lg hover:shadow-cyan-500/20">
|
| 38 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
| 39 |
+
Download ZIP
|
| 40 |
+
</a>
|
| 41 |
+
<a href="/" class="flex items-center gap-2 px-5 py-2.5 rounded-xl glass hover:border-cyan-500/30 text-dark-300 hover:text-cyan-400 font-semibold text-sm transition-all">
|
| 42 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
| 43 |
+
Generate Another
|
| 44 |
+
</a>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
{% if edit_error %}
|
| 49 |
+
<div class="mb-6 p-4 rounded-xl bg-red-900/30 border border-red-500/30 text-red-300 animate-fade-in">
|
| 50 |
+
<div class="flex items-center gap-3">
|
| 51 |
+
<svg class="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
| 52 |
+
<span>{{ edit_error }}</span>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
{% endif %}
|
| 56 |
+
|
| 57 |
+
<!-- Validation Warnings/Errors -->
|
| 58 |
+
{% if validation.warnings or validation.errors %}
|
| 59 |
+
<div class="mb-6 animate-fade-in">
|
| 60 |
+
{% for error in validation.errors %}
|
| 61 |
+
<div class="mb-2 p-3 rounded-lg bg-red-900/20 border border-red-500/20 text-red-300 text-sm flex items-center gap-2">
|
| 62 |
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
| 63 |
+
{{ error }}
|
| 64 |
+
</div>
|
| 65 |
+
{% endfor %}
|
| 66 |
+
{% for warning in validation.warnings %}
|
| 67 |
+
<div class="mb-2 p-3 rounded-lg bg-yellow-900/20 border border-yellow-500/20 text-yellow-300 text-sm flex items-center gap-2">
|
| 68 |
+
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
| 69 |
+
{{ warning }}
|
| 70 |
+
</div>
|
| 71 |
+
{% endfor %}
|
| 72 |
+
</div>
|
| 73 |
+
{% endif %}
|
| 74 |
+
|
| 75 |
+
<!-- Recommended Models -->
|
| 76 |
+
{% if plan.recommended_models %}
|
| 77 |
+
<div class="mb-6 glass rounded-xl p-5 animate-fade-in">
|
| 78 |
+
<h3 class="text-sm font-semibold text-dark-300 mb-3 flex items-center gap-2">
|
| 79 |
+
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
|
| 80 |
+
Recommended Models
|
| 81 |
+
</h3>
|
| 82 |
+
<div class="flex flex-wrap gap-3">
|
| 83 |
+
{% for model in plan.recommended_models %}
|
| 84 |
+
<a href="https://huggingface.co/{{ model.id }}" target="_blank" class="flex items-center gap-2 px-3 py-2 rounded-lg glass-light hover:border-purple-500/30 transition-all group">
|
| 85 |
+
<span class="text-sm font-mono text-purple-400 group-hover:text-purple-300">{{ model.id }}</span>
|
| 86 |
+
<span class="text-xs text-dark-500">{{ model.size }}</span>
|
| 87 |
+
{% if model.gpu_recommended %}
|
| 88 |
+
<span class="text-xs px-1.5 py-0.5 rounded bg-yellow-500/20 text-yellow-400">GPU</span>
|
| 89 |
+
{% endif %}
|
| 90 |
+
</a>
|
| 91 |
+
{% endfor %}
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
{% endif %}
|
| 95 |
+
|
| 96 |
+
<!-- Main Content: File Tree + Code Preview -->
|
| 97 |
+
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 animate-slide-up">
|
| 98 |
+
|
| 99 |
+
<!-- File Tree Sidebar -->
|
| 100 |
+
<div class="lg:col-span-3">
|
| 101 |
+
<div class="glass rounded-xl overflow-hidden sticky top-20">
|
| 102 |
+
<div class="px-4 py-3 border-b border-dark-800">
|
| 103 |
+
<h3 class="text-sm font-semibold text-dark-300 flex items-center gap-2">
|
| 104 |
+
<svg class="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
| 105 |
+
File Tree
|
| 106 |
+
</h3>
|
| 107 |
+
<p class="text-xs text-dark-500 mt-1">{{ files|length }} files</p>
|
| 108 |
+
</div>
|
| 109 |
+
<div class="p-2 max-h-[60vh] overflow-y-auto" id="fileTree">
|
| 110 |
+
{% for item in file_tree %}
|
| 111 |
+
{% if item.type == 'dir' %}
|
| 112 |
+
<div class="file-tree-item flex items-center gap-2 px-3 py-1.5 rounded-lg" style="padding-left: {{ (item.depth * 16) + 12 }}px;">
|
| 113 |
+
<svg class="w-4 h-4 text-cyan-500/60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/></svg>
|
| 114 |
+
<span class="text-sm text-dark-400">{{ item.name }}</span>
|
| 115 |
+
</div>
|
| 116 |
+
{% else %}
|
| 117 |
+
<button
|
| 118 |
+
type="button"
|
| 119 |
+
class="file-tree-item w-full flex items-center gap-2 px-3 py-1.5 rounded-lg cursor-pointer text-left"
|
| 120 |
+
style="padding-left: {{ (item.depth * 16) + 12 }}px;"
|
| 121 |
+
data-filepath="{{ item.path }}"
|
| 122 |
+
onclick="selectFile('{{ item.path }}')"
|
| 123 |
+
>
|
| 124 |
+
<svg class="w-4 h-4 text-dark-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
| 125 |
+
<span class="text-sm text-dark-300 truncate">{{ item.name }}</span>
|
| 126 |
+
</button>
|
| 127 |
+
{% endif %}
|
| 128 |
+
{% endfor %}
|
| 129 |
+
</div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
|
| 133 |
+
<!-- Code Preview -->
|
| 134 |
+
<div class="lg:col-span-9">
|
| 135 |
+
<div class="glass rounded-xl overflow-hidden">
|
| 136 |
+
<!-- Tab Bar -->
|
| 137 |
+
<div class="flex items-center justify-between px-4 py-2 border-b border-dark-800 bg-dark-900/50">
|
| 138 |
+
<div class="flex items-center gap-2">
|
| 139 |
+
<div class="flex items-center gap-1.5">
|
| 140 |
+
<div class="w-3 h-3 rounded-full bg-red-500/60"></div>
|
| 141 |
+
<div class="w-3 h-3 rounded-full bg-yellow-500/60"></div>
|
| 142 |
+
<div class="w-3 h-3 rounded-full bg-green-500/60"></div>
|
| 143 |
+
</div>
|
| 144 |
+
<span class="text-sm font-mono text-dark-400 ml-3" id="currentFileName">Select a file</span>
|
| 145 |
+
</div>
|
| 146 |
+
<button onclick="copyCode()" class="text-xs text-dark-500 hover:text-cyan-400 transition-colors flex items-center gap-1 px-2 py-1 rounded hover:bg-dark-800" id="copyBtn">
|
| 147 |
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
|
| 148 |
+
Copy
|
| 149 |
+
</button>
|
| 150 |
+
</div>
|
| 151 |
+
<!-- Code Content -->
|
| 152 |
+
<div class="overflow-auto max-h-[70vh] p-0" id="codeContainer">
|
| 153 |
+
<div class="p-8 text-center text-dark-500" id="codePlaceholder">
|
| 154 |
+
<svg class="w-12 h-12 mx-auto mb-3 text-dark-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
| 155 |
+
<p>Click a file in the tree to view its code</p>
|
| 156 |
+
</div>
|
| 157 |
+
<pre class="hidden line-numbers" id="codeBlock"><code id="codeContent"></code></pre>
|
| 158 |
+
</div>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<!-- Architecture Diagram -->
|
| 162 |
+
<div class="glass rounded-xl mt-6 overflow-hidden">
|
| 163 |
+
<div class="px-4 py-3 border-b border-dark-800 flex items-center justify-between">
|
| 164 |
+
<h3 class="text-sm font-semibold text-dark-300 flex items-center gap-2">
|
| 165 |
+
<svg class="w-4 h-4 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/></svg>
|
| 166 |
+
Architecture
|
| 167 |
+
</h3>
|
| 168 |
+
<button onclick="toggleDiagram()" class="text-xs text-dark-500 hover:text-cyan-400 transition-colors" id="diagramToggle">Hide</button>
|
| 169 |
+
</div>
|
| 170 |
+
<div id="diagramPanel" class="p-4">
|
| 171 |
+
<pre class="text-xs font-mono text-cyan-400/70 overflow-x-auto leading-relaxed">{{ arch_diagram }}</pre>
|
| 172 |
+
</div>
|
| 173 |
+
</div>
|
| 174 |
+
|
| 175 |
+
<!-- Edit Section -->
|
| 176 |
+
<div class="glass rounded-xl mt-6 p-6">
|
| 177 |
+
<h3 class="text-sm font-semibold text-dark-300 mb-3 flex items-center gap-2">
|
| 178 |
+
<svg class="w-4 h-4 text-cyan-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
| 179 |
+
Iterate on Your App
|
| 180 |
+
</h3>
|
| 181 |
+
<form action="/edit" method="POST" id="editForm" class="flex gap-3">
|
| 182 |
+
<input type="hidden" name="project_id" value="{{ project_id }}">
|
| 183 |
+
<input
|
| 184 |
+
type="text"
|
| 185 |
+
name="edit_prompt"
|
| 186 |
+
required
|
| 187 |
+
class="flex-1 bg-dark-900/80 border border-dark-700 rounded-lg px-4 py-2.5 text-dark-300 placeholder-dark-600 text-sm focus:border-cyan-500/50 focus:ring-2 focus:ring-cyan-500/20"
|
| 188 |
+
placeholder="e.g., Add a dark theme toggle, increase max tokens to 4096, add error messages..."
|
| 189 |
+
>
|
| 190 |
+
<button type="submit" id="editBtn" class="px-5 py-2.5 rounded-lg bg-gradient-to-r from-purple-500 to-pink-600 hover:from-purple-400 hover:to-pink-500 text-white font-semibold text-sm transition-all flex items-center gap-2">
|
| 191 |
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
|
| 192 |
+
<span id="editText">Edit</span>
|
| 193 |
+
<div id="editSpinner" class="spinner hidden"></div>
|
| 194 |
+
</button>
|
| 195 |
+
</form>
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
</div>
|
| 199 |
+
</div>
|
| 200 |
+
|
| 201 |
+
<!-- Hidden data for JS -->
|
| 202 |
+
<script id="fileData" type="application/json">{{ files | tojson }}</script>
|
| 203 |
+
|
| 204 |
+
{% endblock %}
|
| 205 |
+
|
| 206 |
+
{% block extra_scripts %}
|
| 207 |
+
<script>
|
| 208 |
+
// Load file data
|
| 209 |
+
const fileData = JSON.parse(document.getElementById('fileData').textContent);
|
| 210 |
+
|
| 211 |
+
// Auto-select the first file
|
| 212 |
+
const firstFile = Object.keys(fileData)[0];
|
| 213 |
+
if (firstFile) {
|
| 214 |
+
selectFile(firstFile);
|
| 215 |
+
}
|
| 216 |
+
</script>
|
| 217 |
+
{% endblock %}
|
app/validators/__init__.py
ADDED
|
File without changes
|
app/validators/code_checker.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Validate generated code for common issues.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import ast
|
| 6 |
+
import re
|
| 7 |
+
from typing import Optional
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class CodeChecker:
|
| 11 |
+
"""Validate generated Space code for correctness and common issues."""
|
| 12 |
+
|
| 13 |
+
def check(self, files: dict, sdk: str) -> dict:
|
| 14 |
+
"""
|
| 15 |
+
Validate all files in the repo.
|
| 16 |
+
|
| 17 |
+
Returns dict with:
|
| 18 |
+
valid: bool
|
| 19 |
+
errors: list of error strings
|
| 20 |
+
warnings: list of warning strings
|
| 21 |
+
file_checks: dict of filename -> {valid, issues}
|
| 22 |
+
"""
|
| 23 |
+
errors = []
|
| 24 |
+
warnings = []
|
| 25 |
+
file_checks = {}
|
| 26 |
+
|
| 27 |
+
for filename, content in files.items():
|
| 28 |
+
check = self._check_file(filename, content, sdk)
|
| 29 |
+
file_checks[filename] = check
|
| 30 |
+
errors.extend(check.get("errors", []))
|
| 31 |
+
warnings.extend(check.get("warnings", []))
|
| 32 |
+
|
| 33 |
+
# Cross-file checks
|
| 34 |
+
cross_issues = self._cross_file_checks(files, sdk)
|
| 35 |
+
errors.extend(cross_issues.get("errors", []))
|
| 36 |
+
warnings.extend(cross_issues.get("warnings", []))
|
| 37 |
+
|
| 38 |
+
return {
|
| 39 |
+
"valid": len(errors) == 0,
|
| 40 |
+
"errors": errors,
|
| 41 |
+
"warnings": warnings,
|
| 42 |
+
"file_checks": file_checks,
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
def _check_file(self, filename: str, content: str, sdk: str) -> dict:
|
| 46 |
+
"""Check a single file."""
|
| 47 |
+
errors = []
|
| 48 |
+
warnings = []
|
| 49 |
+
|
| 50 |
+
if not content or not content.strip():
|
| 51 |
+
errors.append(f"{filename}: File is empty")
|
| 52 |
+
return {"valid": False, "errors": errors, "warnings": warnings}
|
| 53 |
+
|
| 54 |
+
if filename.endswith(".py"):
|
| 55 |
+
py_result = self._check_python(filename, content)
|
| 56 |
+
errors.extend(py_result["errors"])
|
| 57 |
+
warnings.extend(py_result["warnings"])
|
| 58 |
+
elif filename == "Dockerfile":
|
| 59 |
+
docker_result = self._check_dockerfile(content)
|
| 60 |
+
errors.extend(docker_result["errors"])
|
| 61 |
+
warnings.extend(docker_result["warnings"])
|
| 62 |
+
elif filename == "requirements.txt":
|
| 63 |
+
req_result = self._check_requirements(content)
|
| 64 |
+
errors.extend(req_result["errors"])
|
| 65 |
+
warnings.extend(req_result["warnings"])
|
| 66 |
+
elif filename == "README.md":
|
| 67 |
+
if "---" not in content:
|
| 68 |
+
warnings.append("README.md: Missing YAML frontmatter")
|
| 69 |
+
elif filename.endswith(".html"):
|
| 70 |
+
html_result = self._check_html(filename, content)
|
| 71 |
+
errors.extend(html_result["errors"])
|
| 72 |
+
warnings.extend(html_result["warnings"])
|
| 73 |
+
|
| 74 |
+
valid = len(errors) == 0
|
| 75 |
+
return {"valid": valid, "errors": errors, "warnings": warnings}
|
| 76 |
+
|
| 77 |
+
def _check_python(self, filename: str, content: str) -> dict:
|
| 78 |
+
"""Validate Python code syntax and common patterns."""
|
| 79 |
+
errors = []
|
| 80 |
+
warnings = []
|
| 81 |
+
|
| 82 |
+
# Syntax check
|
| 83 |
+
try:
|
| 84 |
+
ast.parse(content)
|
| 85 |
+
except SyntaxError as e:
|
| 86 |
+
errors.append(f"{filename}: Python syntax error at line {e.lineno}: {e.msg}")
|
| 87 |
+
return {"errors": errors, "warnings": warnings}
|
| 88 |
+
|
| 89 |
+
# Check for common issues
|
| 90 |
+
if "import " not in content and "from " not in content:
|
| 91 |
+
warnings.append(f"{filename}: No imports found")
|
| 92 |
+
|
| 93 |
+
# Check for dangerous patterns
|
| 94 |
+
dangerous_patterns = [
|
| 95 |
+
(r"os\.system\(", "os.system() call found - potential security risk"),
|
| 96 |
+
(r"eval\(", "eval() call found - potential security risk"),
|
| 97 |
+
(r"exec\(", "exec() call found - potential security risk"),
|
| 98 |
+
(r"__import__\(", "__import__() call found - potential security risk"),
|
| 99 |
+
]
|
| 100 |
+
for pattern, msg in dangerous_patterns:
|
| 101 |
+
if re.search(pattern, content):
|
| 102 |
+
warnings.append(f"{filename}: {msg}")
|
| 103 |
+
|
| 104 |
+
# Check for hardcoded tokens/secrets
|
| 105 |
+
secret_patterns = [
|
| 106 |
+
(r'(?:token|key|secret|password)\s*=\s*["\'][^"\']{10,}["\']', "Possible hardcoded secret"),
|
| 107 |
+
]
|
| 108 |
+
for pattern, msg in secret_patterns:
|
| 109 |
+
if re.search(pattern, content, re.IGNORECASE):
|
| 110 |
+
warnings.append(f"{filename}: {msg}")
|
| 111 |
+
|
| 112 |
+
return {"errors": errors, "warnings": warnings}
|
| 113 |
+
|
| 114 |
+
def _check_dockerfile(self, content: str) -> dict:
|
| 115 |
+
"""Validate Dockerfile content."""
|
| 116 |
+
errors = []
|
| 117 |
+
warnings = []
|
| 118 |
+
|
| 119 |
+
if "FROM" not in content:
|
| 120 |
+
errors.append("Dockerfile: Missing FROM instruction")
|
| 121 |
+
if "EXPOSE" not in content:
|
| 122 |
+
warnings.append("Dockerfile: Missing EXPOSE instruction")
|
| 123 |
+
if "7860" not in content:
|
| 124 |
+
warnings.append("Dockerfile: Port 7860 not found (required for HF Spaces)")
|
| 125 |
+
if "CMD" not in content and "ENTRYPOINT" not in content:
|
| 126 |
+
errors.append("Dockerfile: Missing CMD or ENTRYPOINT")
|
| 127 |
+
|
| 128 |
+
return {"errors": errors, "warnings": warnings}
|
| 129 |
+
|
| 130 |
+
def _check_requirements(self, content: str) -> dict:
|
| 131 |
+
"""Validate requirements.txt."""
|
| 132 |
+
errors = []
|
| 133 |
+
warnings = []
|
| 134 |
+
|
| 135 |
+
lines = [l.strip() for l in content.strip().split("\n") if l.strip() and not l.strip().startswith("#")]
|
| 136 |
+
if not lines:
|
| 137 |
+
warnings.append("requirements.txt: No dependencies listed")
|
| 138 |
+
|
| 139 |
+
for line in lines:
|
| 140 |
+
# Basic format check
|
| 141 |
+
if " " in line and ";" not in line and "#" not in line:
|
| 142 |
+
warnings.append(f"requirements.txt: Suspicious line: '{line}'")
|
| 143 |
+
|
| 144 |
+
return {"errors": errors, "warnings": warnings}
|
| 145 |
+
|
| 146 |
+
def _check_html(self, filename: str, content: str) -> dict:
|
| 147 |
+
"""Basic HTML validation."""
|
| 148 |
+
errors = []
|
| 149 |
+
warnings = []
|
| 150 |
+
|
| 151 |
+
if "<html" not in content.lower() and "<!doctype" not in content.lower():
|
| 152 |
+
warnings.append(f"{filename}: Missing <html> tag or DOCTYPE")
|
| 153 |
+
|
| 154 |
+
# Check for unclosed tags (very basic)
|
| 155 |
+
for tag in ["html", "head", "body"]:
|
| 156 |
+
open_count = len(re.findall(f"<{tag}[\\s>]", content, re.IGNORECASE))
|
| 157 |
+
close_count = len(re.findall(f"</{tag}>", content, re.IGNORECASE))
|
| 158 |
+
if open_count > close_count:
|
| 159 |
+
warnings.append(f"{filename}: Unclosed <{tag}> tag")
|
| 160 |
+
|
| 161 |
+
return {"errors": errors, "warnings": warnings}
|
| 162 |
+
|
| 163 |
+
def _cross_file_checks(self, files: dict, sdk: str) -> dict:
|
| 164 |
+
"""Perform checks across multiple files."""
|
| 165 |
+
errors = []
|
| 166 |
+
warnings = []
|
| 167 |
+
|
| 168 |
+
if sdk == "gradio":
|
| 169 |
+
if "app.py" not in files:
|
| 170 |
+
errors.append("Missing app.py (required for Gradio Spaces)")
|
| 171 |
+
if "requirements.txt" not in files:
|
| 172 |
+
warnings.append("Missing requirements.txt")
|
| 173 |
+
if "README.md" not in files:
|
| 174 |
+
warnings.append("Missing README.md")
|
| 175 |
+
|
| 176 |
+
# Check that requirements includes gradio
|
| 177 |
+
req = files.get("requirements.txt", "")
|
| 178 |
+
if "gradio" not in req.lower():
|
| 179 |
+
warnings.append("requirements.txt: 'gradio' not listed as dependency")
|
| 180 |
+
|
| 181 |
+
elif sdk == "docker":
|
| 182 |
+
if "Dockerfile" not in files:
|
| 183 |
+
errors.append("Missing Dockerfile (required for Docker Spaces)")
|
| 184 |
+
if "README.md" not in files:
|
| 185 |
+
warnings.append("Missing README.md")
|
| 186 |
+
|
| 187 |
+
elif sdk == "static":
|
| 188 |
+
if "index.html" not in files:
|
| 189 |
+
errors.append("Missing index.html (required for Static Spaces)")
|
| 190 |
+
|
| 191 |
+
return {"errors": errors, "warnings": warnings}
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.115.6
|
| 2 |
+
uvicorn[standard]==0.34.0
|
| 3 |
+
jinja2==3.1.5
|
| 4 |
+
python-multipart==0.0.20
|
| 5 |
+
huggingface-hub==0.27.1
|
| 6 |
+
aiofiles==24.1.0
|