ruslanmv commited on
Commit
2c304fc
·
verified ·
1 Parent(s): 83ccd6c

feat: complete AutoApp Builder - AI-powered HF Space generator

Browse files
.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: Autoapp Builder
3
- emoji: 🐨
4
- colorFrom: indigo
5
- colorTo: yellow
6
  sdk: docker
7
- pinned: false
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 &mdash; 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 &mdash; 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