R-Kentaren commited on
Commit
4194df4
·
verified ·
1 Parent(s): e286e3c

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +56 -6
  2. app.py +1044 -0
  3. index.html +1852 -0
  4. requirements.txt +6 -0
README.md CHANGED
@@ -1,13 +1,63 @@
1
  ---
2
  title: Fullstack Code Builder
3
- emoji: 😻
4
- colorFrom: indigo
5
- colorTo: purple
6
  sdk: gradio
7
- sdk_version: 6.18.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Fullstack Code Builder
3
+ emoji: 🚀
4
+ colorFrom: purple
5
+ colorTo: green
6
  sdk: gradio
7
+ sdk_version: 6.14.0
8
+ python_version: '3.11'
9
  app_file: app.py
10
  pinned: false
11
  ---
12
 
13
+ ## Fullstack Code Builder
14
+
15
+ An AI-powered fullstack application generator running **entirely locally** with no external API dependencies. Powered by [MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B) (2.17 GB).
16
+
17
+ ### Features
18
+
19
+ - **Local Inference**: Uses MiniCPM5-1B running locally via `transformers` — no API keys needed
20
+ - **Multi-Language Support**: Generate apps in Python, JavaScript, TypeScript, Java, Go, Rust, PHP, Ruby, C#, Swift, Kotlin, and more
21
+ - **Framework Support**: Choose from popular frameworks like React, Vue, Flask, Django, Express, Spring Boot, and others
22
+ - **Live Preview**: See generated web apps in a sandboxed iframe preview
23
+ - **Code Execution**: Run generated Python code and see output
24
+ - **Project Download**: Download generated projects as ZIP files
25
+ - **HuggingFace Deploy**: Push generated projects directly to HuggingFace Spaces
26
+
27
+ ### Supported Languages & Frameworks
28
+
29
+ | Language | Frameworks |
30
+ |----------|-----------|
31
+ | Python | Flask, Django, FastAPI, Streamlit, Plain Python |
32
+ | JavaScript | React, Vue.js, Next.js, Express.js, Node.js, Vanilla JS |
33
+ | TypeScript | React, Next.js, Express.js, NestJS |
34
+ | HTML/CSS/JS | Tailwind CSS, Bootstrap, Vanilla |
35
+ | Java | Spring Boot, Maven, Gradle |
36
+ | Go | Gin, Fiber, Echo, Plain Go |
37
+ | Rust | Actix, Axum, Rocket |
38
+ | PHP | Laravel, Symfony, Plain PHP |
39
+ | Ruby | Rails, Sinatra |
40
+ | C# | ASP.NET, Blazor |
41
+ | Swift | Vapor, SwiftUI |
42
+ | Kotlin | Ktor, Spring Boot |
43
+
44
+ ### Local Run
45
+
46
+ ```bash
47
+ pip install -r requirements.txt
48
+ python app.py
49
+ ```
50
+
51
+ The model (MiniCPM5-1B, ~2.17 GB) will be automatically downloaded on first run.
52
+
53
+ ### HuggingFace Deploy
54
+
55
+ 1. Generate your application
56
+ 2. Go to the "Deploy" tab in the output panel
57
+ 3. Enter your HuggingFace repository name and token
58
+ 4. Select the Space SDK (Static, Gradio, Streamlit, or Docker)
59
+ 5. Click "Push to HuggingFace"
60
+
61
+ ### No External APIs
62
+
63
+ This application does not use any external API calls. All model inference runs locally using the `transformers` library with MiniCPM5-1B.
app.py ADDED
@@ -0,0 +1,1044 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fullstack Code Builder - Local AI-powered fullstack app generator.
2
+
3
+ Uses MiniCPM5-1B for local inference (no external APIs).
4
+ Supports generating fullstack applications in any language.
5
+ Can push generated projects to HuggingFace Hub.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import html
11
+ import json
12
+ import logging
13
+ import os
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+ import textwrap
20
+ import threading
21
+ import time
22
+ import zipfile
23
+ from collections.abc import Iterator
24
+ from dataclasses import dataclass, field
25
+ from pathlib import Path
26
+ from typing import Any
27
+
28
+ from gradio import Server
29
+ from fastapi.responses import HTMLResponse, FileResponse
30
+
31
+ APP_TITLE = "Fullstack Code Builder"
32
+ MODEL_ID = "openbmb/MiniCPM5-1B"
33
+ MODEL_URL = "https://huggingface.co/openbmb/MiniCPM5-1B"
34
+
35
+ DEFAULT_TEMPERATURE = 0.6
36
+ DEFAULT_MAX_TOKENS = 4096
37
+ PY_TIMEOUT_S = 15
38
+ PY_MEM_LIMIT_MB = 1024
39
+ MAX_STDIO_CHARS = 16_000
40
+ OUTPUT_PNG = "output.png"
41
+
42
+ THINKING_BLOCK_RE = re.compile(r"<\s*think\s*>.*?<\s*/\s*think\s*>", re.IGNORECASE | re.DOTALL)
43
+ CODE_BLOCK_RE = re.compile(r"```([a-zA-Z0-9_+.#-]*)\s*\n(.*?)```", re.DOTALL)
44
+ FILE_BLOCK_RE = re.compile(r"@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)", re.DOTALL)
45
+
46
+ logger = logging.getLogger(__name__)
47
+ logging.basicConfig(level=logging.INFO)
48
+
49
+ # ─── Supported Languages & Frameworks ───────────────────────────────────
50
+
51
+ LANGUAGE_OPTIONS = [
52
+ ("Python", ["Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]),
53
+ ("JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]),
54
+ ("TypeScript", ["React", "Next.js", "Express.js", "NestJS"]),
55
+ ("HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]),
56
+ ("Java", ["Spring Boot", "Maven", "Gradle"]),
57
+ ("Go", ["Gin", "Fiber", "Echo", "Plain Go"]),
58
+ ("Rust", ["Actix", "Axum", "Rocket"]),
59
+ ("PHP", ["Laravel", "Symfony", "Plain PHP"]),
60
+ ("Ruby", ["Rails", "Sinatra"]),
61
+ ("C#", ["ASP.NET", "Blazor"]),
62
+ ("Swift", ["Vapor", "SwiftUI"]),
63
+ ("Kotlin", ["Ktor", "Spring Boot"]),
64
+ ]
65
+
66
+ LANGUAGE_MAP = {lang: frameworks for lang, frameworks in LANGUAGE_OPTIONS}
67
+
68
+ SYSTEM_PROMPT = """You are a fullstack application code generator running locally. You help users build complete, runnable applications in any programming language and framework.
69
+
70
+ When the user asks you to build an application:
71
+ 1. Generate complete, working code - not snippets or pseudocode
72
+ 2. Include all necessary files for the project to run
73
+ 3. Add proper error handling and comments
74
+ 4. For web apps, make the UI responsive and modern
75
+
76
+ FILE OUTPUT FORMAT - IMPORTANT:
77
+ When generating multi-file projects, wrap each file in this format:
78
+ @@FILE: path/to/file.ext@@
79
+ (file content here)
80
+ @@FILE: path/to/another/file.ext@@
81
+ (another file content here)
82
+ @@END@@
83
+
84
+ For single-file code, use standard markdown fenced blocks:
85
+ ```python for Python
86
+ ```html for HTML/CSS/JS
87
+ ```javascript for JavaScript
88
+ ```typescript for TypeScript
89
+ etc.
90
+
91
+ When generating web apps with HTML/CSS/JS, return a single self-contained HTML document with all CSS and JavaScript inline. Make the page fully responsive: html/body at margin:0 and 100% width/height, use flexbox/grid layouts, and size any canvas to its container.
92
+
93
+ For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code.
94
+ """
95
+
96
+ # Curated starter prompts
97
+ EXAMPLE_PROMPTS: list[tuple[str, str, str, str]] = [
98
+ (
99
+ "🌐 React Todo App",
100
+ "Build a React todo application with add, delete, mark complete, and filter functionality. Use modern hooks and a clean responsive UI.",
101
+ "JavaScript",
102
+ "React",
103
+ ),
104
+ (
105
+ "🐍 Flask API",
106
+ "Create a Flask REST API for a book library with CRUD operations, in-memory storage, and proper error handling.",
107
+ "Python",
108
+ "Flask",
109
+ ),
110
+ (
111
+ "🎨 Landing Page",
112
+ "Build a modern landing page for a SaaS product with a hero section, features grid, pricing cards, and a footer. Use Tailwind-style CSS.",
113
+ "HTML/CSS/JS",
114
+ "Vanilla",
115
+ ),
116
+ (
117
+ "📊 Dashboard",
118
+ "Create an interactive data dashboard with charts (bar, line, pie), a sidebar navigation, and summary cards. All in a single HTML file.",
119
+ "HTML/CSS/JS",
120
+ "Vanilla",
121
+ ),
122
+ (
123
+ "🚀 Express API",
124
+ "Build an Express.js REST API with user authentication endpoints (register, login, profile), JWT token handling, and input validation.",
125
+ "JavaScript",
126
+ "Express.js",
127
+ ),
128
+ (
129
+ "✅ Todo App",
130
+ "Create a self-contained HTML/CSS/JavaScript todo app: add tasks, mark them complete, delete them, filter by all/active/completed, and show a live count of remaining tasks, with a clean, modern, responsive UI.",
131
+ "HTML/CSS/JS",
132
+ "Vanilla",
133
+ ),
134
+ ]
135
+
136
+
137
+ # ─── Model Loading ────────��─────────────────────────────────────────────
138
+
139
+ _model = None
140
+ _tokenizer = None
141
+ _model_loaded = False
142
+ _model_loading = False
143
+ _load_error: str | None = None
144
+
145
+
146
+ def load_model() -> None:
147
+ """Load MiniCPM5-1B model and tokenizer locally."""
148
+ global _model, _tokenizer, _model_loaded, _model_loading, _load_error
149
+
150
+ if _model_loaded or _model_loading:
151
+ return
152
+
153
+ _model_loading = True
154
+ _load_error = None
155
+
156
+ try:
157
+ from transformers import AutoModelForCausalLM, AutoTokenizer
158
+ import torch
159
+
160
+ logger.info("Loading MiniCPM5-1B model...")
161
+
162
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
163
+ device_map = "auto" if torch.cuda.is_available() else None
164
+
165
+ _tokenizer = AutoTokenizer.from_pretrained(
166
+ MODEL_ID,
167
+ trust_remote_code=True,
168
+ )
169
+ _model = AutoModelForCausalLM.from_pretrained(
170
+ MODEL_ID,
171
+ torch_dtype=dtype,
172
+ device_map=device_map,
173
+ trust_remote_code=True,
174
+ low_cpu_mem_usage=True,
175
+ )
176
+
177
+ if device_map is None:
178
+ _model = _model.to("cpu")
179
+
180
+ _model.eval()
181
+ _model_loaded = True
182
+ logger.info("MiniCPM5-1B model loaded successfully.")
183
+
184
+ except Exception as exc:
185
+ _load_error = str(exc)
186
+ logger.exception("Failed to load model: %s", exc)
187
+ finally:
188
+ _model_loading = False
189
+
190
+
191
+ # Start loading model in background thread
192
+ _load_thread = threading.Thread(target=load_model, daemon=True)
193
+ _load_thread.start()
194
+
195
+
196
+ def get_model_status() -> dict[str, Any]:
197
+ """Return current model loading status."""
198
+ if _model_loaded:
199
+ return {"status": "ready", "message": "Model loaded and ready"}
200
+ if _model_loading:
201
+ return {"status": "loading", "message": "Model is loading... (this may take a few minutes on first run)"}
202
+ if _load_error:
203
+ return {"status": "error", "message": f"Model load error: {_load_error}"}
204
+ return {"status": "unknown", "message": "Model not initialized"}
205
+
206
+
207
+ # ─── Model Inference ────────────────────────────────────────────────────
208
+
209
+ def call_model(messages: list[dict[str, Any]], max_new_tokens: int = DEFAULT_MAX_TOKENS) -> Iterator[str]:
210
+ """Stream model text using local MiniCPM5-1B."""
211
+
212
+ if not _model_loaded:
213
+ status = get_model_status()
214
+ yield status["message"]
215
+ return
216
+
217
+ try:
218
+ from transformers import TextIteratorStreamer
219
+ import torch
220
+
221
+ # Build the prompt from messages
222
+ prompt_parts = []
223
+ for msg in messages:
224
+ role = msg.get("role", "user")
225
+ content = msg.get("content", "")
226
+ if role == "system":
227
+ prompt_parts.append(f"System: {content}")
228
+ elif role == "user":
229
+ prompt_parts.append(f"User: {content}")
230
+ elif role == "assistant":
231
+ prompt_parts.append(f"Assistant: {content}")
232
+ prompt_parts.append("Assistant:")
233
+ full_prompt = "\n\n".join(prompt_parts)
234
+
235
+ # Tokenize
236
+ inputs = _tokenizer(full_prompt, return_tensors="pt", truncation=True, max_length=4096)
237
+ if torch.cuda.is_available():
238
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
239
+
240
+ # Stream generation
241
+ streamer = TextIteratorStreamer(_tokenizer, skip_prompt=True, skip_special_tokens=True)
242
+
243
+ generation_kwargs = {
244
+ **inputs,
245
+ "streamer": streamer,
246
+ "max_new_tokens": max_new_tokens,
247
+ "temperature": DEFAULT_TEMPERATURE,
248
+ "do_sample": True,
249
+ "top_p": 0.9,
250
+ "repetition_penalty": 1.1,
251
+ "pad_token_id": _tokenizer.eos_token_id,
252
+ }
253
+
254
+ # Run generation in a separate thread
255
+ thread = threading.Thread(target=_model.generate, kwargs=generation_kwargs)
256
+ thread.start()
257
+
258
+ output = ""
259
+ for new_text in streamer:
260
+ output += new_text
261
+ yield output
262
+
263
+ thread.join()
264
+
265
+ except Exception as exc:
266
+ logger.exception("Error during model inference")
267
+ yield f"_Error during generation: {exc}_"
268
+
269
+
270
+ def call_model_sync(messages: list[dict[str, Any]], max_new_tokens: int = DEFAULT_MAX_TOKENS) -> str:
271
+ """Non-streaming model call - returns complete response."""
272
+ result = ""
273
+ for chunk in call_model(messages, max_new_tokens):
274
+ result = chunk
275
+ return result
276
+
277
+
278
+ # ─── Code Extraction ────────────────────────────────────────────────────
279
+
280
+ def _strip_thinking_blocks(text: str) -> str:
281
+ return THINKING_BLOCK_RE.sub("", text).strip()
282
+
283
+
284
+ def extract_code(response: str) -> tuple[str, str | None]:
285
+ """Return the first fenced code block and its language tag."""
286
+ visible_response = _strip_thinking_blocks(response)
287
+ match = CODE_BLOCK_RE.search(visible_response)
288
+ if not match:
289
+ return "", None
290
+ return match.group(2).strip(), (match.group(1).strip().lower() or None)
291
+
292
+
293
+ def extract_multi_file(response: str) -> dict[str, str]:
294
+ """Extract multi-file project from @@FILE: format.
295
+
296
+ Returns dict of {filepath: content}.
297
+ """
298
+ files: dict[str, str] = {}
299
+ visible = _strip_thinking_blocks(response)
300
+
301
+ for match in FILE_BLOCK_RE.finditer(visible):
302
+ filepath = match.group(1).strip()
303
+ content = match.group(2).strip()
304
+ files[filepath] = content
305
+
306
+ # Fallback: if no @@FILE: blocks found, extract single code block
307
+ if not files:
308
+ code, lang = extract_code(response)
309
+ if code:
310
+ ext_map = {
311
+ "python": "main.py", "py": "main.py",
312
+ "javascript": "index.js", "js": "index.js",
313
+ "typescript": "index.ts", "ts": "index.ts",
314
+ "html": "index.html",
315
+ "css": "styles.css",
316
+ "java": "Main.java",
317
+ "go": "main.go",
318
+ "rust": "main.rs",
319
+ "php": "index.php",
320
+ "ruby": "main.rb",
321
+ "csharp": "Program.cs",
322
+ "swift": "main.swift",
323
+ "kotlin": "Main.kt",
324
+ }
325
+ filename = ext_map.get(lang or "", "code.txt")
326
+ files[filename] = code
327
+
328
+ return files
329
+
330
+
331
+ def _normalize_language(target_language: str | None, fence_lang: str | None) -> str:
332
+ """Normalize language name to a canonical form."""
333
+ lang = (fence_lang or target_language or "python").lower()
334
+ if lang in {"python", "py"}:
335
+ return "python"
336
+ if lang in {"html", "web", "css"}:
337
+ return "web"
338
+ if lang in {"javascript", "js"}:
339
+ return "javascript"
340
+ if lang in {"typescript", "ts"}:
341
+ return "typescript"
342
+ if lang == "java":
343
+ return "java"
344
+ if lang == "go":
345
+ return "go"
346
+ if lang == "rust":
347
+ return "rust"
348
+ if lang == "php":
349
+ return "php"
350
+ if lang == "ruby":
351
+ return "ruby"
352
+ if lang in {"csharp", "c#"}:
353
+ return "csharp"
354
+ if lang == "swift":
355
+ return "swift"
356
+ if lang == "kotlin":
357
+ return "kotlin"
358
+ return lang
359
+
360
+
361
+ # ─── Python Execution ───────────────────────────────────────────────────
362
+
363
+ @dataclass
364
+ class PythonExecutionResult:
365
+ stdout: str
366
+ stderr: str
367
+ image_path: str | None
368
+ returncode: int | None
369
+ timed_out: bool = False
370
+
371
+
372
+ def _apply_subprocess_limits() -> None:
373
+ import resource
374
+ mem_bytes = PY_MEM_LIMIT_MB * 1024 * 1024
375
+ resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
376
+ resource.setrlimit(resource.RLIMIT_CPU, (PY_TIMEOUT_S, PY_TIMEOUT_S))
377
+
378
+
379
+ def _python_runner_source() -> str:
380
+ return textwrap.dedent(
381
+ f"""
382
+ import os
383
+ import runpy
384
+ import sys
385
+ import traceback
386
+
387
+ os.environ.setdefault("MPLBACKEND", "Agg")
388
+ exit_code = 0
389
+ try:
390
+ runpy.run_path(os.path.join(os.getcwd(), "user_code.py"), run_name="__main__")
391
+ except SystemExit as exc:
392
+ code = exc.code
393
+ exit_code = code if isinstance(code, int) else 1
394
+ except Exception:
395
+ traceback.print_exc()
396
+ exit_code = 1
397
+ finally:
398
+ try:
399
+ import matplotlib
400
+ matplotlib.use("Agg", force=True)
401
+ import matplotlib.pyplot as plt
402
+ if plt.get_fignums():
403
+ plt.savefig(os.environ["OUTPUT_PNG"], bbox_inches="tight")
404
+ except ModuleNotFoundError as exc:
405
+ if exc.name != "matplotlib":
406
+ traceback.print_exc()
407
+ except Exception:
408
+ traceback.print_exc()
409
+
410
+ raise SystemExit(exit_code)
411
+ """
412
+ ).strip()
413
+
414
+
415
+ def _truncate_output(text: str) -> str:
416
+ if len(text) <= MAX_STDIO_CHARS:
417
+ return text
418
+ remaining = len(text) - MAX_STDIO_CHARS
419
+ return text[:MAX_STDIO_CHARS] + f"\n\n... truncated {remaining} characters ..."
420
+
421
+
422
+ def _decode_timeout_output(value: str | bytes | None) -> str:
423
+ if value is None:
424
+ return ""
425
+ if isinstance(value, bytes):
426
+ return value.decode("utf-8", errors="replace")
427
+ return value
428
+
429
+
430
+ def run_python(code: str) -> PythonExecutionResult:
431
+ with tempfile.TemporaryDirectory(prefix="fullstack_run_") as tmp:
432
+ workdir = Path(tmp)
433
+ runner_path = workdir / "runner.py"
434
+ user_path = workdir / "user_code.py"
435
+ image_path = workdir / OUTPUT_PNG
436
+
437
+ runner_path.write_text(_python_runner_source(), encoding="utf-8")
438
+ user_path.write_text(code, encoding="utf-8")
439
+
440
+ env = {
441
+ "PATH": "/usr/bin:/bin",
442
+ "HOME": str(workdir),
443
+ "TMPDIR": str(workdir),
444
+ "MPLBACKEND": "Agg",
445
+ "MPLCONFIGDIR": str(workdir / ".matplotlib"),
446
+ "OUTPUT_PNG": str(image_path),
447
+ "PYTHONIOENCODING": "utf-8",
448
+ "PYTHONNOUSERSITE": "1",
449
+ "PYTHONUNBUFFERED": "1",
450
+ "LANG": "C.UTF-8",
451
+ "OPENBLAS_NUM_THREADS": "1",
452
+ "OMP_NUM_THREADS": "1",
453
+ "MKL_NUM_THREADS": "1",
454
+ "NUMEXPR_NUM_THREADS": "1",
455
+ }
456
+
457
+ try:
458
+ completed = subprocess.run(
459
+ [sys.executable, "-I", str(runner_path)],
460
+ cwd=workdir,
461
+ env=env,
462
+ capture_output=True,
463
+ text=True,
464
+ encoding="utf-8",
465
+ errors="replace",
466
+ timeout=PY_TIMEOUT_S,
467
+ preexec_fn=_apply_subprocess_limits if sys.platform == "linux" else None,
468
+ check=False,
469
+ )
470
+ stdout = _truncate_output(completed.stdout)
471
+ stderr = _truncate_output(completed.stderr)
472
+
473
+ if completed.returncode and not stderr:
474
+ stderr = f"Process exited with status {completed.returncode}."
475
+
476
+ saved_image: str | None = None
477
+ if image_path.exists() and image_path.stat().st_size > 0:
478
+ saved = tempfile.NamedTemporaryFile(
479
+ prefix="fullstack_plot_", suffix=".png", delete=False
480
+ )
481
+ saved.close()
482
+ Path(saved.name).write_bytes(image_path.read_bytes())
483
+ saved_image = saved.name
484
+
485
+ return PythonExecutionResult(
486
+ stdout=stdout,
487
+ stderr=stderr,
488
+ image_path=saved_image,
489
+ returncode=completed.returncode,
490
+ )
491
+ except subprocess.TimeoutExpired as exc:
492
+ stdout = _truncate_output(_decode_timeout_output(exc.stdout))
493
+ stderr = _truncate_output(_decode_timeout_output(exc.stderr))
494
+ timeout_note = f"Timed out after {PY_TIMEOUT_S} seconds; the process was killed."
495
+ stderr = f"{stderr}\n{timeout_note}".strip()
496
+ return PythonExecutionResult(
497
+ stdout=stdout,
498
+ stderr=stderr,
499
+ image_path=None,
500
+ returncode=None,
501
+ timed_out=True,
502
+ )
503
+
504
+
505
+ # ─── Web Document ───────────────────────────────────────────────────────
506
+
507
+ def _web_document(code: str, fence_lang: str | None) -> str:
508
+ lang = (fence_lang or "").lower()
509
+ if lang in {"javascript", "js"}:
510
+ return f"<!doctype html><html><body><script>\n{code}\n</script></body></html>"
511
+ if lang == "css":
512
+ return f"<!doctype html><html><head><style>\n{code}\n</style></head><body></body></html>"
513
+ if re.search(r"<!doctype|<html[\s>]", code, flags=re.IGNORECASE):
514
+ return code
515
+ return f"<!doctype html><html><head><meta charset='utf-8'></head><body>\n{code}\n</body></html>"
516
+
517
+
518
+ def build_iframe(code: str, fence_lang: str | None = None) -> str:
519
+ document = _web_document(code, fence_lang)
520
+ srcdoc = html.escape(document, quote=True)
521
+ return (
522
+ '<iframe class="web-frame" '
523
+ 'sandbox="allow-scripts" '
524
+ 'allow="fullscreen" '
525
+ "allowfullscreen "
526
+ f'srcdoc="{srcdoc}" '
527
+ 'style="width:100%; min-height:680px; border:0; border-radius:14px; '
528
+ 'background:white;"></iframe>'
529
+ )
530
+
531
+
532
+ # ─── Project Packaging ──────────────────────────────────────────────────
533
+
534
+ def create_project_zip(files: dict[str, str], project_name: str) -> str:
535
+ """Create a ZIP file from extracted project files.
536
+
537
+ Returns path to the created ZIP file.
538
+ """
539
+ zip_dir = tempfile.mkdtemp(prefix="fullstack_project_")
540
+ zip_path = os.path.join(zip_dir, f"{project_name}.zip")
541
+
542
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
543
+ for filepath, content in files.items():
544
+ zf.writestr(f"{project_name}/{filepath}", content)
545
+
546
+ return zip_path
547
+
548
+
549
+ def create_project_directory(files: dict[str, str], project_name: str) -> str:
550
+ """Create a directory with project files for HuggingFace push.
551
+
552
+ Returns path to the created project directory.
553
+ """
554
+ proj_dir = tempfile.mkdtemp(prefix="fullstack_deploy_")
555
+ target_dir = os.path.join(proj_dir, project_name)
556
+ os.makedirs(target_dir, exist_ok=True)
557
+
558
+ for filepath, content in files.items():
559
+ full_path = os.path.join(target_dir, filepath)
560
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
561
+ Path(full_path).write_text(content, encoding="utf-8")
562
+
563
+ return target_dir
564
+
565
+
566
+ # ─── HuggingFace Hub Push ────────────────────────────��──────────────────
567
+
568
+ def push_to_huggingface(
569
+ files: dict[str, str],
570
+ project_name: str,
571
+ repo_name: str,
572
+ hf_token: str,
573
+ space_sdk: str = "static",
574
+ is_space: bool = True,
575
+ ) -> dict[str, Any]:
576
+ """Push generated project to HuggingFace Hub.
577
+
578
+ Args:
579
+ files: Dict of {filepath: content}
580
+ project_name: Name of the project
581
+ repo_name: HuggingFace repo name (e.g., "username/my-app")
582
+ hf_token: HuggingFace API token
583
+ space_sdk: SDK for Space ("gradio", "streamlit", "static", "docker")
584
+ is_space: Whether to create a Space (True) or Model repo (False)
585
+
586
+ Returns:
587
+ Dict with status and URL info
588
+ """
589
+ try:
590
+ from huggingface_hub import HfApi, create_repo
591
+
592
+ api = HfApi(token=hf_token)
593
+
594
+ # Parse username/repo
595
+ if "/" in repo_name:
596
+ namespace, name = repo_name.split("/", 1)
597
+ else:
598
+ # Use the authenticated user's namespace
599
+ user_info = api.whoami()
600
+ namespace = user_info["name"]
601
+ name = repo_name
602
+ repo_name = f"{namespace}/{name}"
603
+
604
+ # Create repo
605
+ try:
606
+ if is_space:
607
+ create_repo(
608
+ repo_id=repo_name,
609
+ repo_type="space",
610
+ space_sdk=space_sdk,
611
+ token=hf_token,
612
+ exist_ok=True,
613
+ )
614
+ else:
615
+ create_repo(
616
+ repo_id=repo_name,
617
+ repo_type="model",
618
+ token=hf_token,
619
+ exist_ok=True,
620
+ )
621
+ except Exception as e:
622
+ logger.warning("Repo creation warning: %s", e)
623
+
624
+ # Upload files
625
+ with tempfile.TemporaryDirectory(prefix="hf_push_") as tmp_dir:
626
+ for filepath, content in files.items():
627
+ full_path = os.path.join(tmp_dir, filepath)
628
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
629
+ Path(full_path).write_text(content, encoding="utf-8")
630
+
631
+ # Add README if not present
632
+ readme_path = os.path.join(tmp_dir, "README.md")
633
+ if not os.path.exists(readme_path):
634
+ readme_content = f"""---
635
+ title: {name}
636
+ emoji: 🚀
637
+ colorFrom: blue
638
+ colorTo: purple
639
+ sdk: {space_sdk}
640
+ app_file: app.py
641
+ ---
642
+
643
+ # {name}
644
+
645
+ Generated by Fullstack Code Builder using {MODEL_ID}.
646
+ """
647
+ Path(readme_path).write_text(readme_content, encoding="utf-8")
648
+
649
+ api.upload_folder(
650
+ folder_path=tmp_dir,
651
+ repo_id=repo_name,
652
+ repo_type="space" if is_space else "model",
653
+ token=hf_token,
654
+ )
655
+
656
+ repo_url = f"https://huggingface.co/{repo_name}"
657
+ if is_space:
658
+ repo_url = f"https://huggingface.co/spaces/{repo_name}"
659
+
660
+ return {
661
+ "success": True,
662
+ "url": repo_url,
663
+ "repo_name": repo_name,
664
+ "message": f"Successfully pushed to {repo_url}",
665
+ }
666
+
667
+ except Exception as exc:
668
+ logger.exception("Failed to push to HuggingFace")
669
+ return {
670
+ "success": False,
671
+ "url": "",
672
+ "repo_name": repo_name,
673
+ "message": f"Failed to push: {str(exc)}",
674
+ }
675
+
676
+
677
+ # ─── Chat Helpers ───────────────────────────────────────────────────────
678
+
679
+ def _chat_history_to_messages(history: list[dict[str, str]]) -> list[dict[str, Any]]:
680
+ messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
681
+ for item in history:
682
+ role = item.get("role")
683
+ content = str(item.get("content") or "").strip()
684
+ if role not in {"user", "assistant"} or not content:
685
+ continue
686
+ if role == "assistant":
687
+ content = _strip_thinking_blocks(content)
688
+ messages.append({"role": role, "content": content})
689
+ return messages
690
+
691
+
692
+ def _clip_context(text: str, limit: int = 4_000) -> str:
693
+ if len(text) <= limit:
694
+ return text
695
+ return text[:limit] + f"\n... truncated {len(text) - limit} characters ..."
696
+
697
+
698
+ def _iteration_context(execution_context: dict[str, Any] | None) -> str:
699
+ if not execution_context or not execution_context.get("code"):
700
+ return ""
701
+
702
+ code = _clip_context(str(execution_context.get("code") or ""), 6_000)
703
+ target = str(execution_context.get("target") or "code")
704
+ fence_lang = str(execution_context.get("fence_lang") or target)
705
+ status = str(execution_context.get("status") or "")
706
+ stdout = _clip_context(str(execution_context.get("stdout") or ""), 2_000)
707
+ stderr = _clip_context(str(execution_context.get("stderr") or ""), 2_000)
708
+
709
+ parts = [
710
+ "Previous generated code and run result are available for iteration.",
711
+ f"Previous target: {target}",
712
+ f"Previous status: {status}",
713
+ f"Previous code:\n```{fence_lang}\n{code}\n```",
714
+ ]
715
+ if stdout:
716
+ parts.append(f"Previous stdout:\n{stdout}")
717
+ if stderr:
718
+ parts.append(f"Previous stderr / traceback:\n{stderr}")
719
+ parts.append("If the user asks to revise, debug, extend, or explain the prior code, use this context.")
720
+ return "\n\n".join(parts)
721
+
722
+
723
+ def _targeted_prompt(
724
+ prompt: str,
725
+ target_language: str,
726
+ target_framework: str = "",
727
+ execution_context: dict[str, Any] | None = None,
728
+ ) -> str:
729
+ iteration_context = _iteration_context(execution_context)
730
+ context_block = f"\n\n{iteration_context}" if iteration_context else ""
731
+
732
+ framework_hint = f" using {target_framework}" if target_framework else ""
733
+
734
+ return (
735
+ f"Target: {target_language}{framework_hint}. Generate a complete, runnable application. "
736
+ "If the user asks for a web app, include all HTML/CSS/JS. "
737
+ "If they ask for a backend, include the server code and any API definitions. "
738
+ "For single-file apps, use a single code block. For multi-file projects, use the @@FILE: format. "
739
+ "Make the code complete, working, and well-structured."
740
+ f"{context_block}\n\n"
741
+ f"User request:\n{prompt}"
742
+ )
743
+
744
+
745
+ # ─── Run Extracted Code ────────────────────────────────────────────────
746
+
747
+ def _run_extracted_code(
748
+ code: str,
749
+ target: str,
750
+ ) -> tuple[str, str, str | None, str, str]:
751
+ if target == "python":
752
+ result = run_python(code)
753
+ if result.timed_out:
754
+ return result.stdout, result.stderr, result.image_path, f"Timed out after {PY_TIMEOUT_S}s", "error"
755
+ if result.returncode:
756
+ return result.stdout, result.stderr, result.image_path, "Finished with errors", "error"
757
+ return result.stdout, result.stderr, result.image_path, "Ran successfully", "success"
758
+
759
+ return "", "", None, "Preview ready", "success"
760
+
761
+
762
+ # ─── Served Files Registry ──────────────────────────────────────────────
763
+
764
+ _served_files: dict[str, str] = {}
765
+
766
+ # ─── FastAPI / Gradio Application ───────────────────────────────────────
767
+
768
+ app = Server()
769
+
770
+
771
+ @app.get("/", response_class=HTMLResponse)
772
+ async def homepage():
773
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
774
+ with open(html_path, "r", encoding="utf-8") as f:
775
+ content = f.read()
776
+
777
+ config = json.dumps({
778
+ "app_title": APP_TITLE,
779
+ "model_id": MODEL_ID,
780
+ "model_url": MODEL_URL,
781
+ "languages": LANGUAGE_OPTIONS,
782
+ "examples": [
783
+ {"label": label, "prompt": prompt, "language": lang, "framework": fw}
784
+ for label, prompt, lang, fw in EXAMPLE_PROMPTS
785
+ ],
786
+ })
787
+ content = content.replace("__RUNTIME_CONFIG__", config)
788
+ return content
789
+
790
+
791
+ @app.get("/api/model-status")
792
+ async def model_status_endpoint():
793
+ return get_model_status()
794
+
795
+
796
+ @app.get("/images/{filename}")
797
+ async def serve_image(filename: str):
798
+ path = _served_files.get(f"img:{filename}")
799
+ if path and os.path.exists(path):
800
+ return FileResponse(path, media_type="image/png")
801
+ return HTMLResponse("Not found", status_code=404)
802
+
803
+
804
+ @app.get("/download/{filename}")
805
+ async def serve_download(filename: str):
806
+ path = _served_files.get(f"dl:{filename}")
807
+ if path and os.path.exists(path):
808
+ return FileResponse(path, filename=filename, media_type="application/octet-stream")
809
+ return HTMLResponse("Not found", status_code=404)
810
+
811
+
812
+ @app.api(name="chat", concurrency_limit=2)
813
+ def handle_chat(
814
+ prompt: str,
815
+ target_language: str,
816
+ target_framework: str,
817
+ history_json: str,
818
+ exec_context_json: str,
819
+ ) -> str:
820
+ """Stream chat responses with code execution. Yields JSON strings."""
821
+ history = json.loads(history_json) if history_json else []
822
+ execution_context = json.loads(exec_context_json) if exec_context_json else {}
823
+
824
+ prompt = (prompt or "").strip()
825
+ if not prompt:
826
+ yield json.dumps({
827
+ "type": "error",
828
+ "status_text": "Enter a prompt to get started.",
829
+ "status_state": "info",
830
+ "history": history,
831
+ "execution": execution_context,
832
+ })
833
+ return
834
+
835
+ # Check model status
836
+ model_status = get_model_status()
837
+ if model_status["status"] == "loading":
838
+ yield json.dumps({
839
+ "type": "error",
840
+ "status_text": model_status["message"],
841
+ "status_state": "working",
842
+ "history": history,
843
+ "execution": execution_context,
844
+ })
845
+ return
846
+ if model_status["status"] != "ready":
847
+ yield json.dumps({
848
+ "type": "error",
849
+ "status_text": model_status["message"],
850
+ "status_state": "error",
851
+ "history": history,
852
+ "execution": execution_context,
853
+ })
854
+ return
855
+
856
+ # Add user message and placeholder assistant message
857
+ history = list(history) + [
858
+ {"role": "user", "content": prompt},
859
+ {"role": "assistant", "content": ""},
860
+ ]
861
+ yield json.dumps({
862
+ "type": "status",
863
+ "status_text": "Thinking...",
864
+ "status_state": "working",
865
+ "history": history,
866
+ "execution": execution_context,
867
+ })
868
+
869
+ # Build messages for model
870
+ model_history = list(history[:-1])
871
+ model_history[-1] = {
872
+ "role": "user",
873
+ "content": _targeted_prompt(prompt, target_language, target_framework, execution_context),
874
+ }
875
+ messages = _chat_history_to_messages(model_history)
876
+
877
+ final_response = ""
878
+ for partial in call_model(messages):
879
+ final_response = partial
880
+ history[-1]["content"] = partial
881
+ yield json.dumps({
882
+ "type": "streaming",
883
+ "status_text": "Generating...",
884
+ "status_state": "working",
885
+ "history": history,
886
+ "execution": execution_context,
887
+ })
888
+
889
+ if not final_response:
890
+ history[-1]["content"] = "The model did not return a response."
891
+ yield json.dumps({
892
+ "type": "error",
893
+ "status_text": "No model response.",
894
+ "status_state": "error",
895
+ "history": history,
896
+ "execution": execution_context,
897
+ })
898
+ return
899
+
900
+ # Extract code from response
901
+ code, fence_lang = extract_code(final_response)
902
+ target = _normalize_language(target_language, fence_lang)
903
+
904
+ # Also try multi-file extraction
905
+ multi_files = extract_multi_file(final_response)
906
+
907
+ if not code and not multi_files:
908
+ yield json.dumps({
909
+ "type": "complete",
910
+ "status_text": "Answered without running code.",
911
+ "status_state": "info",
912
+ "history": history,
913
+ "execution": execution_context,
914
+ })
915
+ return
916
+
917
+ yield json.dumps({
918
+ "type": "status",
919
+ "status_text": "Running...",
920
+ "status_state": "working",
921
+ "history": history,
922
+ "execution": execution_context,
923
+ })
924
+
925
+ # Execute if Python
926
+ stdout, stderr, image_path, status_text, status_state = "", "", None, "Preview ready", "success"
927
+ if target == "python" and code:
928
+ stdout, stderr, image_path, status_text, status_state = _run_extracted_code(code, target)
929
+
930
+ # Register image for serving
931
+ image_url = None
932
+ if image_path:
933
+ filename = os.path.basename(image_path)
934
+ _served_files[f"img:{filename}"] = image_path
935
+ image_url = f"/images/{filename}"
936
+
937
+ # Register code for download
938
+ download_url = None
939
+ project_files = multi_files if multi_files else {}
940
+
941
+ if project_files:
942
+ # Multi-file project: create ZIP
943
+ project_name = "generated-project"
944
+ zip_path = create_project_zip(project_files, project_name)
945
+ zip_filename = f"{project_name}.zip"
946
+ _served_files[f"dl:{zip_filename}"] = zip_path
947
+ download_url = f"/download/{zip_filename}"
948
+ elif code:
949
+ ext = "py" if target == "python" else "html"
950
+ dl_filename = f"generated.{ext}"
951
+ dl_dir = tempfile.mkdtemp(prefix="fullstack_dl_")
952
+ dl_path = os.path.join(dl_dir, dl_filename)
953
+ Path(dl_path).write_text(code, encoding="utf-8")
954
+ _served_files[f"dl:{dl_filename}"] = dl_path
955
+ download_url = f"/download/{dl_filename}"
956
+
957
+ # Determine if this is web previewable
958
+ is_web = target in {"web", "javascript", "typescript", "html"} or (fence_lang or "") in {"html", "web"}
959
+ web_code = code if is_web else None
960
+
961
+ execution_context = {
962
+ "code": code,
963
+ "target": target,
964
+ "fence_lang": fence_lang or target,
965
+ "stdout": stdout,
966
+ "stderr": stderr,
967
+ "image_url": image_url,
968
+ "image_path": image_path,
969
+ "status": status_text,
970
+ "language": fence_lang or target,
971
+ "suggested_tab": "preview" if (image_path or is_web) else "console",
972
+ "download_url": download_url,
973
+ "project_files": project_files,
974
+ "is_web": is_web,
975
+ "web_code": web_code,
976
+ }
977
+
978
+ yield json.dumps({
979
+ "type": "complete",
980
+ "status_text": status_text,
981
+ "status_state": status_state,
982
+ "history": history,
983
+ "execution": execution_context,
984
+ })
985
+
986
+
987
+ @app.api(name="push_hf", concurrency_limit=1)
988
+ def handle_push_hf(
989
+ exec_context_json: str,
990
+ repo_name: str,
991
+ hf_token: str,
992
+ space_sdk: str = "static",
993
+ is_space: str = "true",
994
+ ) -> str:
995
+ """Push generated project to HuggingFace Hub."""
996
+ try:
997
+ execution_context = json.loads(exec_context_json) if exec_context_json else {}
998
+ project_files = execution_context.get("project_files", {})
999
+
1000
+ if not project_files:
1001
+ # If no multi-file, try single code
1002
+ code = execution_context.get("code", "")
1003
+ if not code:
1004
+ yield json.dumps({
1005
+ "success": False,
1006
+ "message": "No code to push. Generate some code first.",
1007
+ "url": "",
1008
+ })
1009
+ return
1010
+
1011
+ lang = execution_context.get("language", "python")
1012
+ ext_map = {
1013
+ "python": "app.py", "py": "app.py",
1014
+ "javascript": "index.js", "js": "index.js",
1015
+ "html": "index.html", "web": "index.html",
1016
+ "typescript": "index.ts", "ts": "index.ts",
1017
+ }
1018
+ filename = ext_map.get(lang, "app.py")
1019
+ project_files = {filename: code}
1020
+
1021
+ # Extract project name from repo_name
1022
+ project_name = repo_name.split("/")[-1] if "/" in repo_name else repo_name
1023
+
1024
+ result = push_to_huggingface(
1025
+ files=project_files,
1026
+ project_name=project_name,
1027
+ repo_name=repo_name,
1028
+ hf_token=hf_token,
1029
+ space_sdk=space_sdk,
1030
+ is_space=is_space.lower() == "true",
1031
+ )
1032
+
1033
+ yield json.dumps(result)
1034
+
1035
+ except Exception as exc:
1036
+ logger.exception("Push to HuggingFace failed")
1037
+ yield json.dumps({
1038
+ "success": False,
1039
+ "message": f"Push failed: {str(exc)}",
1040
+ "url": "",
1041
+ })
1042
+
1043
+
1044
+ app.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,1852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Fullstack Code Builder</title>
7
+ <meta name="description" content="AI-powered fullstack app generator with local model inference">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600;700&display=swap" rel="stylesheet">
11
+ <style>
12
+ /* ═══════════════════════════════════════════════════════
13
+ RESET & BASE
14
+ ═══════════════════════════════════════════════════════ */
15
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
16
+
17
+ :root {
18
+ --bg-deep: #0a0e14;
19
+ --bg-panel: #0d1117;
20
+ --bg-code: #161b22;
21
+ --border: #1e2a3a;
22
+ --border-focus: #2d4a6a;
23
+ --green: #39ff14;
24
+ --green-dim: #1a7a0a;
25
+ --cyan: #00d4ff;
26
+ --amber: #ffb300;
27
+ --purple: #a855f7;
28
+ --gray-light: #e0e0e0;
29
+ --gray-mid: #8b949e;
30
+ --gray-dim: #484f58;
31
+ --red: #ff5555;
32
+ --success: #50fa7b;
33
+ --code-text: #f8f8f2;
34
+ --glow-green: 0 0 8px rgba(57,255,20,0.3);
35
+ --glow-cyan: 0 0 8px rgba(0,212,255,0.3);
36
+ --glow-amber: 0 0 8px rgba(255,179,0,0.2);
37
+ --glow-purple: 0 0 8px rgba(168,85,247,0.3);
38
+ --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
39
+ --radius: 4px;
40
+ --transition: 0.2s ease;
41
+ }
42
+
43
+ html, body {
44
+ height: 100%;
45
+ background: var(--bg-deep);
46
+ color: var(--gray-light);
47
+ font-family: var(--font-mono);
48
+ font-size: 13px;
49
+ line-height: 1.6;
50
+ overflow: hidden;
51
+ }
52
+
53
+ body::after {
54
+ content: '';
55
+ position: fixed;
56
+ inset: 0;
57
+ pointer-events: none;
58
+ z-index: 9999;
59
+ background: repeating-linear-gradient(
60
+ 0deg,
61
+ transparent,
62
+ transparent 2px,
63
+ rgba(0, 0, 0, 0.03) 2px,
64
+ rgba(0, 0, 0, 0.03) 4px
65
+ );
66
+ }
67
+
68
+ ::selection {
69
+ background: rgba(57, 255, 20, 0.25);
70
+ color: #fff;
71
+ }
72
+
73
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
74
+ ::-webkit-scrollbar-track { background: transparent; }
75
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
76
+ ::-webkit-scrollbar-thumb:hover { background: var(--gray-dim); }
77
+
78
+ a { color: var(--cyan); text-decoration: none; }
79
+ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
80
+
81
+ /* ═══════════════════════════════════════════════════════
82
+ APP SHELL
83
+ ═══════════════════════════════════════════════════════ */
84
+ #app {
85
+ display: flex;
86
+ flex-direction: column;
87
+ height: 100vh;
88
+ max-height: 100vh;
89
+ }
90
+
91
+ /* ═══════════════════════════════════════════════════════
92
+ HEADER
93
+ ═══════════════════════════════════════════════════════ */
94
+ #header {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ padding: 10px 20px;
99
+ background: var(--bg-panel);
100
+ border-bottom: 1px solid var(--border);
101
+ flex-shrink: 0;
102
+ gap: 16px;
103
+ }
104
+
105
+ .header-title {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: 2px;
109
+ }
110
+
111
+ .header-ascii {
112
+ color: var(--green);
113
+ font-size: 11px;
114
+ line-height: 1.3;
115
+ text-shadow: var(--glow-green);
116
+ white-space: pre;
117
+ letter-spacing: 0.5px;
118
+ }
119
+
120
+ .header-subtitle {
121
+ color: var(--gray-mid);
122
+ font-size: 11px;
123
+ padding-left: 3px;
124
+ }
125
+
126
+ .header-actions {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 10px;
130
+ flex-shrink: 0;
131
+ }
132
+
133
+ .pill {
134
+ display: inline-flex;
135
+ align-items: center;
136
+ gap: 5px;
137
+ padding: 4px 10px;
138
+ border: 1px solid var(--border);
139
+ border-radius: 12px;
140
+ font-size: 11px;
141
+ color: var(--gray-mid);
142
+ text-decoration: none;
143
+ transition: all var(--transition);
144
+ }
145
+ .pill:hover {
146
+ border-color: var(--cyan);
147
+ color: var(--cyan);
148
+ text-decoration: none;
149
+ text-shadow: var(--glow-cyan);
150
+ }
151
+ .pill .dot {
152
+ width: 6px;
153
+ height: 6px;
154
+ border-radius: 50%;
155
+ background: var(--success);
156
+ box-shadow: 0 0 6px var(--success);
157
+ }
158
+ .pill .dot.loading {
159
+ background: var(--amber);
160
+ box-shadow: 0 0 6px var(--amber);
161
+ animation: pulse 1.5s ease infinite;
162
+ }
163
+ .pill .dot.error {
164
+ background: var(--red);
165
+ box-shadow: 0 0 6px var(--red);
166
+ }
167
+
168
+ @keyframes pulse {
169
+ 0%, 100% { opacity: 1; }
170
+ 50% { opacity: 0.4; }
171
+ }
172
+
173
+ #btn-new-chat {
174
+ background: transparent;
175
+ border: 1px solid var(--border);
176
+ color: var(--amber);
177
+ font-family: var(--font-mono);
178
+ font-size: 11px;
179
+ padding: 5px 12px;
180
+ border-radius: var(--radius);
181
+ cursor: pointer;
182
+ transition: all var(--transition);
183
+ letter-spacing: 1px;
184
+ }
185
+ #btn-new-chat:hover {
186
+ border-color: var(--amber);
187
+ background: rgba(255,179,0,0.08);
188
+ text-shadow: var(--glow-amber);
189
+ }
190
+
191
+ /* ═══════════════════════════════════════════════════════
192
+ BANNER
193
+ ═══════════════════════════════════════════════════════ */
194
+ #playground-banner {
195
+ background: linear-gradient(90deg, rgba(168,85,247,0.08), rgba(57,255,20,0.05));
196
+ border-bottom: 1px solid var(--border);
197
+ color: var(--gray-mid);
198
+ font-size: 12px;
199
+ padding: 7px 18px;
200
+ text-align: center;
201
+ flex-shrink: 0;
202
+ }
203
+ #playground-banner strong { color: var(--gray-light); font-weight: 600; }
204
+ #playground-banner a { color: var(--purple); font-weight: 600; }
205
+
206
+ /* ═══════════════════════════════════════════════════════
207
+ MAIN LAYOUT
208
+ ═══════════════════════════════════════════════════════ */
209
+ #main {
210
+ display: flex;
211
+ flex: 1;
212
+ min-height: 0;
213
+ overflow: hidden;
214
+ }
215
+
216
+ /* ═══════════════════════════════════════════════════════
217
+ TERMINAL (LEFT PANEL)
218
+ ═══════════════════════════════════════════════════════ */
219
+ #terminal-panel {
220
+ display: flex;
221
+ flex-direction: column;
222
+ flex: 1;
223
+ min-width: 0;
224
+ border-right: 1px solid var(--border);
225
+ }
226
+
227
+ .panel-label {
228
+ padding: 6px 16px;
229
+ font-size: 10px;
230
+ letter-spacing: 2px;
231
+ color: var(--gray-dim);
232
+ border-bottom: 1px solid var(--border);
233
+ background: rgba(13,17,23,0.6);
234
+ text-transform: uppercase;
235
+ flex-shrink: 0;
236
+ }
237
+
238
+ #chat-messages {
239
+ flex: 1;
240
+ overflow-y: auto;
241
+ padding: 16px;
242
+ scroll-behavior: smooth;
243
+ }
244
+
245
+ /* Message styles */
246
+ .msg {
247
+ margin-bottom: 14px;
248
+ line-height: 1.65;
249
+ animation: msgFadeIn 0.25s ease;
250
+ }
251
+
252
+ @keyframes msgFadeIn {
253
+ from { opacity: 0; transform: translateY(6px); }
254
+ to { opacity: 1; transform: translateY(0); }
255
+ }
256
+
257
+ .msg-prefix { font-weight: 600; margin-right: 4px; }
258
+ .msg-user .msg-prefix { color: var(--green); text-shadow: var(--glow-green); }
259
+ .msg-user .msg-content { color: var(--green); text-shadow: var(--glow-green); }
260
+ .msg-assistant .msg-prefix { color: var(--cyan); text-shadow: var(--glow-cyan); }
261
+ .msg-assistant .msg-content { color: var(--gray-light); }
262
+ .msg-system .msg-prefix { color: var(--amber); text-shadow: var(--glow-amber); }
263
+ .msg-system .msg-content { color: var(--amber); opacity: 0.85; }
264
+
265
+ /* Markdown elements */
266
+ .msg-content strong { color: #fff; font-weight: 600; }
267
+ .msg-content em { font-style: italic; color: var(--gray-mid); }
268
+ .msg-content code:not(pre code) {
269
+ background: var(--bg-code);
270
+ color: var(--code-text);
271
+ padding: 1px 5px;
272
+ border-radius: 3px;
273
+ font-size: 12px;
274
+ border: 1px solid var(--border);
275
+ }
276
+ .msg-content a { color: var(--cyan); }
277
+ .msg-content ul, .msg-content ol { margin: 6px 0 6px 20px; }
278
+ .msg-content li { margin-bottom: 3px; }
279
+ .msg-content h1, .msg-content h2, .msg-content h3 {
280
+ color: var(--cyan); margin: 10px 0 6px; font-size: 14px; text-shadow: var(--glow-cyan);
281
+ }
282
+ .msg-content h1 { font-size: 16px; }
283
+ .msg-content h2 { font-size: 15px; }
284
+ .msg-content p { margin: 4px 0; }
285
+
286
+ /* Code blocks */
287
+ .code-block-wrap {
288
+ margin: 8px 0;
289
+ border: 1px solid var(--border);
290
+ border-radius: var(--radius);
291
+ overflow: hidden;
292
+ background: var(--bg-code);
293
+ }
294
+ .code-block-header {
295
+ display: flex; align-items: center; justify-content: space-between;
296
+ padding: 4px 10px; background: rgba(30,42,58,0.5);
297
+ border-bottom: 1px solid var(--border); font-size: 11px;
298
+ }
299
+ .code-lang { color: var(--amber); text-transform: uppercase; letter-spacing: 1px; }
300
+ .btn-copy {
301
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
302
+ font-family: var(--font-mono); font-size: 10px; padding: 2px 8px;
303
+ border-radius: 3px; cursor: pointer; transition: all var(--transition);
304
+ }
305
+ .btn-copy:hover { border-color: var(--green); color: var(--green); }
306
+ .btn-copy.copied { border-color: var(--success); color: var(--success); }
307
+ .code-block-wrap pre {
308
+ margin: 0; padding: 10px 12px; overflow-x: auto;
309
+ font-size: 12px; line-height: 1.5; color: var(--code-text);
310
+ }
311
+ .code-block-wrap pre code { font-family: var(--font-mono); background: none; border: none; padding: 0; }
312
+
313
+ /* Thinking blocks */
314
+ .think-block {
315
+ margin: 8px 0; border: 1px solid rgba(255,179,0,0.15);
316
+ border-radius: var(--radius); background: rgba(255,179,0,0.03);
317
+ }
318
+ .think-summary {
319
+ display: block; width: 100%; background: transparent; border: none;
320
+ padding: 6px 10px; cursor: pointer; font-size: 12px;
321
+ font-family: var(--font-mono); text-align: left; color: var(--gray-dim);
322
+ user-select: none; transition: color var(--transition);
323
+ }
324
+ .think-summary:hover { color: var(--amber); }
325
+ .think-block .think-content {
326
+ padding: 6px 12px 10px; font-size: 12px; color: var(--gray-dim);
327
+ line-height: 1.55; border-top: 1px solid rgba(255,179,0,0.1);
328
+ }
329
+ .think-block:not(.open) .think-content { display: none; }
330
+
331
+ /* Streaming cursor */
332
+ .streaming-cursor::after {
333
+ content: '\u2588'; animation: blink 0.8s step-end infinite;
334
+ color: var(--green); margin-left: 2px;
335
+ }
336
+ @keyframes blink { 50% { opacity: 0; } }
337
+
338
+ /* ═══════════════════════════════════════════════════════
339
+ INPUT AREA
340
+ ═══════════════════════════════════════════════════════ */
341
+ #input-area {
342
+ flex-shrink: 0;
343
+ border-top: 1px solid var(--border);
344
+ background: var(--bg-panel);
345
+ padding: 10px 16px 8px;
346
+ }
347
+
348
+ /* Target selectors */
349
+ #target-selector {
350
+ display: flex;
351
+ gap: 10px;
352
+ margin-bottom: 6px;
353
+ align-items: center;
354
+ flex-wrap: wrap;
355
+ }
356
+
357
+ .selector-group {
358
+ display: flex;
359
+ align-items: center;
360
+ gap: 6px;
361
+ }
362
+
363
+ .selector-label {
364
+ font-size: 10px;
365
+ color: var(--gray-dim);
366
+ letter-spacing: 1px;
367
+ text-transform: uppercase;
368
+ }
369
+
370
+ #lang-select, #framework-select {
371
+ background: var(--bg-deep);
372
+ border: 1px solid var(--border);
373
+ color: var(--green);
374
+ font-family: var(--font-mono);
375
+ font-size: 11px;
376
+ padding: 3px 8px;
377
+ border-radius: var(--radius);
378
+ outline: none;
379
+ cursor: pointer;
380
+ transition: border-color var(--transition);
381
+ }
382
+ #lang-select:focus, #framework-select:focus {
383
+ border-color: var(--border-focus);
384
+ }
385
+ #lang-select option, #framework-select option {
386
+ background: var(--bg-deep);
387
+ color: var(--gray-light);
388
+ }
389
+
390
+ #input-row {
391
+ display: flex;
392
+ gap: 8px;
393
+ align-items: flex-end;
394
+ }
395
+
396
+ .input-prompt-symbol {
397
+ color: var(--green);
398
+ font-weight: 700;
399
+ font-size: 14px;
400
+ line-height: 36px;
401
+ text-shadow: var(--glow-green);
402
+ flex-shrink: 0;
403
+ }
404
+
405
+ #chat-input {
406
+ flex: 1;
407
+ background: var(--bg-deep);
408
+ border: 1px solid var(--border);
409
+ border-radius: var(--radius);
410
+ color: var(--green);
411
+ font-family: var(--font-mono);
412
+ font-size: 13px;
413
+ padding: 8px 12px;
414
+ resize: none;
415
+ outline: none;
416
+ min-height: 36px;
417
+ max-height: 120px;
418
+ line-height: 1.5;
419
+ transition: border-color var(--transition);
420
+ caret-color: var(--green);
421
+ text-shadow: var(--glow-green);
422
+ }
423
+ #chat-input::placeholder { color: var(--gray-dim); text-shadow: none; }
424
+ #chat-input:focus { border-color: var(--border-focus); }
425
+
426
+ #btn-send, #btn-stop {
427
+ font-family: var(--font-mono); font-size: 12px; padding: 8px 14px;
428
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
429
+ letter-spacing: 1px; flex-shrink: 0; height: 36px;
430
+ display: flex; align-items: center; gap: 4px;
431
+ }
432
+
433
+ #btn-send {
434
+ background: transparent; border: 1px solid var(--green); color: var(--green);
435
+ }
436
+ #btn-send:hover:not(:disabled) {
437
+ background: var(--green); color: var(--bg-deep);
438
+ box-shadow: 0 0 12px rgba(57,255,20,0.3);
439
+ }
440
+ #btn-send:disabled { opacity: 0.3; cursor: not-allowed; }
441
+
442
+ #btn-stop {
443
+ background: transparent; border: 1px solid var(--red); color: var(--red); display: none;
444
+ }
445
+ #btn-stop:hover {
446
+ background: var(--red); color: var(--bg-deep);
447
+ box-shadow: 0 0 12px rgba(255,85,85,0.3);
448
+ }
449
+
450
+ /* Examples */
451
+ #examples-row {
452
+ display: flex; align-items: center; gap: 8px;
453
+ margin-top: 8px; flex-wrap: wrap;
454
+ }
455
+ .examples-label {
456
+ font-size: 10px; color: var(--gray-dim); letter-spacing: 1px;
457
+ text-transform: uppercase; flex-shrink: 0;
458
+ }
459
+ .example-chip {
460
+ background: rgba(30,42,58,0.4); border: 1px solid var(--border);
461
+ border-radius: 12px; padding: 3px 10px; font-family: var(--font-mono);
462
+ font-size: 11px; color: var(--gray-mid); cursor: pointer;
463
+ transition: all var(--transition); white-space: nowrap;
464
+ }
465
+ .example-chip:hover {
466
+ border-color: var(--purple); color: var(--purple);
467
+ background: rgba(168,85,247,0.05); text-shadow: var(--glow-purple);
468
+ }
469
+
470
+ /* ═══════════════════════════════════════════════════════
471
+ OUTPUT PANEL (RIGHT)
472
+ ═══════════════════════════════════════════════════════ */
473
+ #output-panel {
474
+ display: flex; flex-direction: column; width: 45%; min-width: 340px;
475
+ max-width: 55%; min-height: 0; background: var(--bg-panel);
476
+ }
477
+
478
+ #output-tabs {
479
+ display: flex; border-bottom: 1px solid var(--border);
480
+ background: rgba(13,17,23,0.6); flex-shrink: 0;
481
+ }
482
+
483
+ .output-tab {
484
+ flex: 1; background: transparent; border: none;
485
+ border-bottom: 2px solid transparent; color: var(--gray-dim);
486
+ font-family: var(--font-mono); font-size: 11px; padding: 8px 12px;
487
+ cursor: pointer; transition: all var(--transition);
488
+ letter-spacing: 1px; text-transform: uppercase;
489
+ }
490
+ .output-tab:hover { color: var(--gray-mid); }
491
+ .output-tab.active {
492
+ color: var(--cyan); border-bottom-color: var(--cyan);
493
+ text-shadow: var(--glow-cyan);
494
+ }
495
+
496
+ #output-content {
497
+ flex: 1; min-height: 0; overflow: hidden; position: relative;
498
+ }
499
+
500
+ /* Tab panes */
501
+ .tab-pane { display: none; height: 100%; min-height: 0; }
502
+ .tab-pane.active { display: flex; flex-direction: column; }
503
+
504
+ /* Preview tab */
505
+ #pane-preview {
506
+ align-items: stretch; justify-content: stretch;
507
+ position: relative; min-height: 0; overflow: hidden;
508
+ }
509
+
510
+ .preview-placeholder {
511
+ align-self: center; margin: auto; text-align: center;
512
+ color: var(--gray-dim); padding: 40px 20px;
513
+ }
514
+ .preview-placeholder .ascii-art {
515
+ font-size: 11px; line-height: 1.3; margin-bottom: 16px; color: var(--border-focus);
516
+ }
517
+ .preview-placeholder .placeholder-text { font-size: 12px; letter-spacing: 0.5px; }
518
+
519
+ #preview-image {
520
+ display: none; max-width: 100%; max-height: 100%;
521
+ object-fit: contain; padding: 12px;
522
+ }
523
+
524
+ #preview-iframe {
525
+ display: none; position: absolute; inset: 0; width: 100%; height: 100%;
526
+ min-height: 0; border: none; background: #fff;
527
+ }
528
+
529
+ #btn-fullscreen {
530
+ display: none; position: absolute; top: 8px; right: 8px;
531
+ background: rgba(13,17,23,0.8); border: 1px solid var(--border);
532
+ color: var(--gray-mid); font-family: var(--font-mono); font-size: 11px;
533
+ padding: 4px 10px; border-radius: var(--radius); cursor: pointer;
534
+ z-index: 5; transition: all var(--transition);
535
+ }
536
+ #btn-fullscreen:hover { border-color: var(--cyan); color: var(--cyan); }
537
+
538
+ /* Console tab */
539
+ #pane-console { padding: 12px 16px; gap: 12px; overflow-y: auto; }
540
+
541
+ .console-section { margin-bottom: 8px; }
542
+ .console-label {
543
+ font-size: 10px; letter-spacing: 2px; color: var(--gray-dim);
544
+ margin-bottom: 4px; text-transform: uppercase;
545
+ }
546
+ .console-output {
547
+ background: var(--bg-deep); border: 1px solid var(--border);
548
+ border-radius: var(--radius); padding: 10px 12px; font-size: 12px;
549
+ line-height: 1.5; white-space: pre-wrap; word-break: break-word;
550
+ min-height: 40px; max-height: 280px; overflow-y: auto;
551
+ }
552
+ #console-stdout { color: var(--success); }
553
+ #console-stderr { color: var(--red); }
554
+
555
+ /* Code tab */
556
+ #pane-code { padding: 0; }
557
+
558
+ .code-tab-header {
559
+ display: flex; align-items: center; justify-content: space-between;
560
+ padding: 8px 12px; border-bottom: 1px solid var(--border);
561
+ background: rgba(30,42,58,0.3); flex-shrink: 0;
562
+ }
563
+ .code-tab-lang {
564
+ font-size: 11px; color: var(--amber); letter-spacing: 1px; text-transform: uppercase;
565
+ }
566
+ .code-tab-actions { display: flex; gap: 8px; }
567
+ .code-tab-btn {
568
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
569
+ font-family: var(--font-mono); font-size: 10px; padding: 3px 8px;
570
+ border-radius: 3px; cursor: pointer; text-decoration: none;
571
+ transition: all var(--transition); display: inline-flex;
572
+ align-items: center; gap: 4px;
573
+ }
574
+ .code-tab-btn:hover { border-color: var(--cyan); color: var(--cyan); text-decoration: none; }
575
+
576
+ #code-display {
577
+ flex: 1; overflow: auto; padding: 12px; background: var(--bg-code);
578
+ }
579
+ #code-display pre { margin: 0; font-size: 12px; line-height: 1.5; color: var(--code-text); }
580
+
581
+ .code-placeholder {
582
+ display: flex; align-items: center; justify-content: center;
583
+ height: 100%; color: var(--gray-dim); font-size: 12px;
584
+ }
585
+
586
+ /* ═══════════════════════════════════════════════════════
587
+ DEPLOY TAB
588
+ ═══════════════════════════════════════════════════════ */
589
+ #pane-deploy { padding: 16px; gap: 14px; overflow-y: auto; }
590
+
591
+ .deploy-section {
592
+ border: 1px solid var(--border);
593
+ border-radius: var(--radius);
594
+ padding: 14px;
595
+ background: var(--bg-code);
596
+ }
597
+
598
+ .deploy-title {
599
+ font-size: 12px;
600
+ font-weight: 600;
601
+ color: var(--purple);
602
+ text-shadow: var(--glow-purple);
603
+ margin-bottom: 10px;
604
+ letter-spacing: 1px;
605
+ text-transform: uppercase;
606
+ }
607
+
608
+ .deploy-field {
609
+ margin-bottom: 10px;
610
+ }
611
+
612
+ .deploy-field label {
613
+ display: block;
614
+ font-size: 10px;
615
+ color: var(--gray-dim);
616
+ letter-spacing: 1px;
617
+ text-transform: uppercase;
618
+ margin-bottom: 4px;
619
+ }
620
+
621
+ .deploy-field input, .deploy-field select {
622
+ width: 100%;
623
+ background: var(--bg-deep);
624
+ border: 1px solid var(--border);
625
+ color: var(--green);
626
+ font-family: var(--font-mono);
627
+ font-size: 12px;
628
+ padding: 6px 10px;
629
+ border-radius: var(--radius);
630
+ outline: none;
631
+ transition: border-color var(--transition);
632
+ }
633
+ .deploy-field input:focus, .deploy-field select:focus {
634
+ border-color: var(--border-focus);
635
+ }
636
+ .deploy-field input::placeholder {
637
+ color: var(--gray-dim);
638
+ }
639
+ .deploy-field select option {
640
+ background: var(--bg-deep);
641
+ color: var(--gray-light);
642
+ }
643
+
644
+ .deploy-hint {
645
+ font-size: 10px;
646
+ color: var(--gray-dim);
647
+ margin-top: 3px;
648
+ }
649
+
650
+ #btn-push-hf {
651
+ width: 100%;
652
+ background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(57,255,20,0.1));
653
+ border: 1px solid var(--purple);
654
+ color: var(--purple);
655
+ font-family: var(--font-mono);
656
+ font-size: 12px;
657
+ padding: 8px 14px;
658
+ border-radius: var(--radius);
659
+ cursor: pointer;
660
+ transition: all var(--transition);
661
+ letter-spacing: 1px;
662
+ margin-top: 6px;
663
+ }
664
+ #btn-push-hf:hover:not(:disabled) {
665
+ background: var(--purple);
666
+ color: white;
667
+ box-shadow: 0 0 12px rgba(168,85,247,0.4);
668
+ text-shadow: none;
669
+ }
670
+ #btn-push-hf:disabled {
671
+ opacity: 0.4;
672
+ cursor: not-allowed;
673
+ }
674
+
675
+ .deploy-status {
676
+ margin-top: 10px;
677
+ padding: 8px 12px;
678
+ border-radius: var(--radius);
679
+ font-size: 11px;
680
+ display: none;
681
+ }
682
+ .deploy-status.success {
683
+ display: block;
684
+ background: rgba(80,250,123,0.1);
685
+ border: 1px solid var(--success);
686
+ color: var(--success);
687
+ }
688
+ .deploy-status.error {
689
+ display: block;
690
+ background: rgba(255,85,85,0.1);
691
+ border: 1px solid var(--red);
692
+ color: var(--red);
693
+ }
694
+ .deploy-status.working {
695
+ display: block;
696
+ background: rgba(255,179,0,0.1);
697
+ border: 1px solid var(--amber);
698
+ color: var(--amber);
699
+ }
700
+
701
+ .deploy-status a {
702
+ color: var(--cyan);
703
+ font-weight: 600;
704
+ }
705
+
706
+ /* Project files list */
707
+ .project-files {
708
+ margin-top: 10px;
709
+ }
710
+
711
+ .file-item {
712
+ display: flex;
713
+ align-items: center;
714
+ gap: 6px;
715
+ padding: 4px 0;
716
+ font-size: 11px;
717
+ color: var(--gray-mid);
718
+ border-bottom: 1px solid rgba(30,42,58,0.5);
719
+ }
720
+ .file-item:last-child { border-bottom: none; }
721
+ .file-icon { color: var(--amber); }
722
+ .file-name { color: var(--cyan); }
723
+
724
+ /* ═══════════════════════════════════════════════════════
725
+ STATUS BAR
726
+ ═══════════════════════════════════════════════════════ */
727
+ #status-bar {
728
+ display: flex; align-items: center; gap: 8px; padding: 5px 16px;
729
+ border-top: 1px solid var(--border); background: var(--bg-panel);
730
+ font-size: 11px; flex-shrink: 0;
731
+ }
732
+
733
+ .status-indicator {
734
+ display: inline-flex; align-items: center; gap: 6px;
735
+ }
736
+ .status-dot { font-size: 10px; line-height: 1; }
737
+ #status-text { letter-spacing: 1px; text-transform: uppercase; }
738
+
739
+ .status-idle { color: var(--gray-dim); }
740
+ .status-working { color: var(--amber); text-shadow: var(--glow-amber); }
741
+ .status-success { color: var(--success); text-shadow: 0 0 8px rgba(80,250,123,0.3); }
742
+ .status-error { color: var(--red); text-shadow: 0 0 8px rgba(255,85,85,0.3); }
743
+ .status-info { color: var(--cyan); text-shadow: var(--glow-cyan); }
744
+
745
+ @keyframes spin { to { transform: rotate(360deg); } }
746
+ .status-working .status-dot { display: inline-block; animation: spin 1s linear infinite; }
747
+
748
+ /* ═══════════════════════════════════════════════════════
749
+ FULLSCREEN OVERLAY
750
+ ═══════════════════════════════════════════════════════ */
751
+ #fullscreen-overlay {
752
+ display: none; position: fixed; inset: 0; z-index: 1000;
753
+ background: var(--bg-deep); flex-direction: column;
754
+ }
755
+ #fullscreen-overlay.active { display: flex; }
756
+
757
+ #fullscreen-bar {
758
+ display: flex; align-items: center; justify-content: space-between;
759
+ padding: 8px 16px; border-bottom: 1px solid var(--border); background: var(--bg-panel);
760
+ }
761
+ #fullscreen-bar span { color: var(--cyan); font-size: 12px; letter-spacing: 1px; }
762
+ #btn-exit-fullscreen {
763
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
764
+ font-family: var(--font-mono); font-size: 11px; padding: 4px 12px;
765
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
766
+ }
767
+ #btn-exit-fullscreen:hover { border-color: var(--red); color: var(--red); }
768
+ #fullscreen-iframe { flex: 1; border: none; background: #fff; }
769
+
770
+ /* ═══════════════════════════════════════════════════════
771
+ RESPONSIVE
772
+ ═══════════════════════════════════════════════════════ */
773
+ @media (max-width: 900px) {
774
+ #main { flex-direction: column; }
775
+ #terminal-panel { border-right: none; border-bottom: 1px solid var(--border); max-height: 55vh; }
776
+ #output-panel { width: 100%; max-width: 100%; min-width: 0; flex: 1; }
777
+ .header-ascii { font-size: 10px; }
778
+ #chat-input { font-size: 12px; }
779
+ #preview-iframe { min-height: 400px; }
780
+ }
781
+
782
+ @media (max-width: 600px) {
783
+ #header { padding: 8px 12px; gap: 8px; }
784
+ .header-ascii { display: none; }
785
+ .header-subtitle { display: none; }
786
+ .pill { font-size: 10px; padding: 3px 8px; }
787
+ #chat-messages { padding: 10px; }
788
+ #input-area { padding: 8px 10px 6px; }
789
+ #examples-row { display: none; }
790
+ #target-selector { gap: 6px; }
791
+ }
792
+ </style>
793
+ </head>
794
+ <body>
795
+ <div id="app">
796
+ <!-- Header -->
797
+ <header id="header">
798
+ <div class="header-title">
799
+ <div class="header-ascii">╔═══ FULLSTACK CODE BUILDER ═══╗</div>
800
+ <div class="header-subtitle">Local AI App Generator | MiniCPM5-1B</div>
801
+ </div>
802
+ <div class="header-actions">
803
+ <a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
804
+ <span class="dot loading" id="model-dot"></span>
805
+ <span id="model-pill-text">MiniCPM5-1B</span>
806
+ </a>
807
+ <button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
808
+ </div>
809
+ </header>
810
+
811
+ <div id="playground-banner">
812
+ Powered by <a id="banner-model-link" href="https://huggingface.co/openbmb/MiniCPM5-1B" target="_blank" rel="noopener"><strong>MiniCPM5-1B</strong></a> running locally — no external APIs. Generate fullstack apps in any language and deploy to HuggingFace.
813
+ </div>
814
+
815
+ <!-- Main Layout -->
816
+ <div id="main">
817
+ <!-- Terminal Panel -->
818
+ <div id="terminal-panel">
819
+ <div class="panel-label">Terminal</div>
820
+ <div id="chat-messages"></div>
821
+ <div id="input-area">
822
+ <div id="target-selector">
823
+ <div class="selector-group">
824
+ <span class="selector-label">Lang:</span>
825
+ <select id="lang-select" onchange="onLanguageChange()"></select>
826
+ </div>
827
+ <div class="selector-group">
828
+ <span class="selector-label">Framework:</span>
829
+ <select id="framework-select"></select>
830
+ </div>
831
+ </div>
832
+ <div id="input-row">
833
+ <span class="input-prompt-symbol">&#10095;</span>
834
+ <textarea id="chat-input" rows="1" placeholder="Describe the app you want to build..." spellcheck="false"></textarea>
835
+ <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">&#10148;</button>
836
+ <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
837
+ </div>
838
+ <div id="examples-row"></div>
839
+ </div>
840
+ </div>
841
+
842
+ <!-- Output Panel -->
843
+ <div id="output-panel">
844
+ <div id="output-tabs">
845
+ <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
846
+ <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
847
+ <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
848
+ <button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
849
+ </div>
850
+ <div id="output-content">
851
+ <!-- Preview Pane -->
852
+ <div class="tab-pane active" id="pane-preview">
853
+ <div class="preview-placeholder" id="preview-placeholder">
854
+ <div class="ascii-art">
855
+ ┌──────────────────────┐
856
+ │ ╭━━━╮ │
857
+ │ ┃ ▶ ┃ OUTPUT │
858
+ │ ╰━━━╯ │
859
+ └──────────────────────┘</div>
860
+ <div class="placeholder-text">Generate code to see output here</div>
861
+ </div>
862
+ <img id="preview-image" alt="Generated output">
863
+ <iframe id="preview-iframe" sandbox="allow-scripts"></iframe>
864
+ <button id="btn-fullscreen" onclick="openFullscreen()">&#10570; FULLSCREEN</button>
865
+ </div>
866
+
867
+ <!-- Console Pane -->
868
+ <div class="tab-pane" id="pane-console">
869
+ <div class="console-section">
870
+ <div class="console-label">stdout:</div>
871
+ <div class="console-output" id="console-stdout">No output yet.</div>
872
+ </div>
873
+ <div class="console-section">
874
+ <div class="console-label">stderr:</div>
875
+ <div class="console-output" id="console-stderr">No errors.</div>
876
+ </div>
877
+ </div>
878
+
879
+ <!-- Code Pane -->
880
+ <div class="tab-pane" id="pane-code">
881
+ <div class="code-tab-header">
882
+ <span class="code-tab-lang" id="code-tab-lang">&mdash;</span>
883
+ <div class="code-tab-actions">
884
+ <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">&#128203; Copy</button>
885
+ <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">&#11015; Download</a>
886
+ </div>
887
+ </div>
888
+ <div id="code-display">
889
+ <div class="code-placeholder">No code generated yet.</div>
890
+ </div>
891
+ </div>
892
+
893
+ <!-- Deploy Pane -->
894
+ <div class="tab-pane" id="pane-deploy">
895
+ <div class="deploy-section">
896
+ <div class="deploy-title">&#128640; Deploy to HuggingFace</div>
897
+ <div class="deploy-field">
898
+ <label for="hf-repo-name">Repository Name</label>
899
+ <input type="text" id="hf-repo-name" placeholder="username/my-app" autocomplete="off">
900
+ <div class="deploy-hint">Format: username/repo-name or just repo-name</div>
901
+ </div>
902
+ <div class="deploy-field">
903
+ <label for="hf-token">HuggingFace Token</label>
904
+ <input type="password" id="hf-token" placeholder="hf_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
905
+ <div class="deploy-hint">Get your token at <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a></div>
906
+ </div>
907
+ <div class="deploy-field">
908
+ <label for="hf-space-sdk">Space SDK</label>
909
+ <select id="hf-space-sdk">
910
+ <option value="static">Static (HTML/CSS/JS)</option>
911
+ <option value="gradio">Gradio</option>
912
+ <option value="streamlit">Streamlit</option>
913
+ <option value="docker">Docker</option>
914
+ </select>
915
+ </div>
916
+ <button id="btn-push-hf" onclick="pushToHuggingFace()" disabled>&#128640; Push to HuggingFace</button>
917
+ <div class="deploy-status" id="deploy-status"></div>
918
+ <div class="project-files" id="project-files"></div>
919
+ </div>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ </div>
924
+
925
+ <!-- Status Bar -->
926
+ <div id="status-bar">
927
+ <div class="status-indicator status-idle" id="status-indicator">
928
+ <span class="status-dot">&#9679;</span>
929
+ <span id="status-text">LOADING MODEL...</span>
930
+ </div>
931
+ </div>
932
+ </div>
933
+
934
+ <!-- Fullscreen Overlay -->
935
+ <div id="fullscreen-overlay">
936
+ <div id="fullscreen-bar">
937
+ <span>WEB PREVIEW</span>
938
+ <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[&#10005; CLOSE]</button>
939
+ </div>
940
+ <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe>
941
+ </div>
942
+
943
+ <script>
944
+ // ═══════════════════════════════════════════════════════
945
+ // CONFIG
946
+ // ═══════════════════════════════════════════════════════
947
+ const CONFIG = __RUNTIME_CONFIG__;
948
+
949
+ // ═══════════════════════════════════════════════════════
950
+ // STATE
951
+ // ═══════════════════════════════════════════════════════
952
+ const state = {
953
+ history: [],
954
+ executionContext: {},
955
+ targetLanguage: 'Python',
956
+ targetFramework: 'Flask',
957
+ isGenerating: false,
958
+ currentEventSource: null,
959
+ activeTab: 'preview',
960
+ lastExecution: null,
961
+ lastCode: '',
962
+ lastCodeLang: '',
963
+ pendingWebPreviewCode: '',
964
+ loadedWebPreviewCode: '',
965
+ scheduledWebPreviewCode: '',
966
+ reasoningExpanded: false,
967
+ lastReasoningPressAt: 0,
968
+ modelReady: false,
969
+ };
970
+
971
+ // ═══════════════════════════════════════════════════════
972
+ // INITIALIZATION
973
+ // ═══════════════════════════════════════════════════════
974
+ document.addEventListener('DOMContentLoaded', () => {
975
+ document.title = CONFIG.app_title || 'Fullstack Code Builder';
976
+
977
+ if (CONFIG.model_url) {
978
+ document.getElementById('model-pill').href = CONFIG.model_url;
979
+ document.getElementById('banner-model-link').href = CONFIG.model_url;
980
+ }
981
+ if (CONFIG.model_id) {
982
+ document.getElementById('model-pill-text').textContent = CONFIG.model_id.split('/').pop();
983
+ }
984
+
985
+ // Populate language/framework selects
986
+ populateLanguageSelects();
987
+
988
+ // Render examples
989
+ renderExamples();
990
+
991
+ // Welcome message
992
+ addSystemMessage('Welcome to Fullstack Code Builder. The model is loading locally (no API keys needed). Select a language and framework, then describe the app you want to build.');
993
+
994
+ // Input auto-resize & keybinding
995
+ const input = document.getElementById('chat-input');
996
+ input.addEventListener('input', autoResize);
997
+ input.addEventListener('keydown', (e) => {
998
+ if (e.key === 'Enter' && e.shiftKey) {
999
+ e.preventDefault();
1000
+ handleSend();
1001
+ }
1002
+ });
1003
+
1004
+ document.addEventListener('pointerdown', handleReasoningPress, true);
1005
+ document.addEventListener('mousedown', handleReasoningPress, true);
1006
+ document.addEventListener('keydown', handleReasoningKeydown, true);
1007
+ document.addEventListener('keydown', handleFullscreenKeydown);
1008
+ observePreviewSize();
1009
+
1010
+ // Poll model status
1011
+ pollModelStatus();
1012
+ });
1013
+
1014
+ function autoResize() {
1015
+ const el = document.getElementById('chat-input');
1016
+ el.style.height = 'auto';
1017
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
1018
+ }
1019
+
1020
+ // ══════════════════════════════���════════════════════════
1021
+ // MODEL STATUS POLLING
1022
+ // ═══════════════════════════════════════════════════════
1023
+ async function pollModelStatus() {
1024
+ try {
1025
+ const resp = await fetch('/api/model-status');
1026
+ const data = await resp.json();
1027
+
1028
+ const dot = document.getElementById('model-dot');
1029
+ const statusText = document.getElementById('status-text');
1030
+ const indicator = document.getElementById('status-indicator');
1031
+
1032
+ if (data.status === 'ready') {
1033
+ state.modelReady = true;
1034
+ dot.className = 'dot';
1035
+ statusText.textContent = 'MODEL READY';
1036
+ indicator.className = 'status-indicator status-success';
1037
+ document.getElementById('btn-push-hf').disabled = false;
1038
+ // Update after 3 seconds to idle
1039
+ setTimeout(() => {
1040
+ if (!state.isGenerating) {
1041
+ indicator.className = 'status-indicator status-idle';
1042
+ statusText.textContent = 'IDLE';
1043
+ }
1044
+ }, 3000);
1045
+ return;
1046
+ } else if (data.status === 'loading') {
1047
+ dot.className = 'dot loading';
1048
+ statusText.textContent = 'LOADING MODEL...';
1049
+ indicator.className = 'status-indicator status-working';
1050
+ } else {
1051
+ dot.className = 'dot error';
1052
+ statusText.textContent = 'MODEL ERROR';
1053
+ indicator.className = 'status-indicator status-error';
1054
+ }
1055
+
1056
+ // Poll again in 3 seconds
1057
+ setTimeout(pollModelStatus, 3000);
1058
+ } catch (err) {
1059
+ console.error('Model status poll error:', err);
1060
+ setTimeout(pollModelStatus, 5000);
1061
+ }
1062
+ }
1063
+
1064
+ // ═══════════════════════════════════════════════════════
1065
+ // LANGUAGE / FRAMEWORK SELECTS
1066
+ // ═══════════════════════════════════════════════════════
1067
+ function populateLanguageSelects() {
1068
+ const langSelect = document.getElementById('lang-select');
1069
+ const fwSelect = document.getElementById('framework-select');
1070
+
1071
+ if (CONFIG.languages) {
1072
+ CONFIG.languages.forEach(([lang, frameworks]) => {
1073
+ const opt = document.createElement('option');
1074
+ opt.value = lang;
1075
+ opt.textContent = lang;
1076
+ if (lang === 'Python') opt.selected = true;
1077
+ langSelect.appendChild(opt);
1078
+ });
1079
+ }
1080
+
1081
+ onLanguageChange();
1082
+ }
1083
+
1084
+ function onLanguageChange() {
1085
+ const langSelect = document.getElementById('lang-select');
1086
+ const fwSelect = document.getElementById('framework-select');
1087
+ const selectedLang = langSelect.value;
1088
+
1089
+ state.targetLanguage = selectedLang;
1090
+
1091
+ // Update frameworks
1092
+ fwSelect.innerHTML = '';
1093
+ const frameworks = (CONFIG.languages || []).find(([l]) => l === selectedLang);
1094
+ if (frameworks && frameworks[1]) {
1095
+ frameworks[1].forEach((fw) => {
1096
+ const opt = document.createElement('option');
1097
+ opt.value = fw;
1098
+ opt.textContent = fw;
1099
+ fwSelect.appendChild(opt);
1100
+ });
1101
+ state.targetFramework = frameworks[1][0];
1102
+ }
1103
+
1104
+ fwSelect.onchange = () => {
1105
+ state.targetFramework = fwSelect.value;
1106
+ };
1107
+ }
1108
+
1109
+ // ═══════════════════════════════════════════════════════
1110
+ // EXAMPLES
1111
+ // ═══════════════════════════════════════════════════════
1112
+ function renderExamples() {
1113
+ const row = document.getElementById('examples-row');
1114
+ if (!CONFIG.examples || CONFIG.examples.length === 0) {
1115
+ row.style.display = 'none';
1116
+ return;
1117
+ }
1118
+ row.innerHTML = '<span class="examples-label">Try:</span>';
1119
+ CONFIG.examples.forEach((ex) => {
1120
+ const chip = document.createElement('button');
1121
+ chip.className = 'example-chip';
1122
+ chip.textContent = ex.label;
1123
+ chip.title = ex.prompt;
1124
+ chip.addEventListener('click', () => {
1125
+ if (state.isGenerating) return;
1126
+ resetConversation();
1127
+ if (ex.language) {
1128
+ document.getElementById('lang-select').value = ex.language;
1129
+ onLanguageChange();
1130
+ }
1131
+ if (ex.framework) {
1132
+ document.getElementById('framework-select').value = ex.framework;
1133
+ state.targetFramework = ex.framework;
1134
+ }
1135
+ sendMessage(ex.prompt);
1136
+ });
1137
+ row.appendChild(chip);
1138
+ });
1139
+ }
1140
+
1141
+ // ═══════════════════════════════════════════════════════
1142
+ // CHAT MESSAGES
1143
+ // ═══════════════════════════════════════════════════════
1144
+ function addSystemMessage(text) {
1145
+ const container = document.getElementById('chat-messages');
1146
+ const div = document.createElement('div');
1147
+ div.className = 'msg msg-system';
1148
+ div.innerHTML = `<span class="msg-prefix">system&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1149
+ container.appendChild(div);
1150
+ scrollToBottom();
1151
+ }
1152
+
1153
+ function addUserMessage(text) {
1154
+ const container = document.getElementById('chat-messages');
1155
+ const div = document.createElement('div');
1156
+ div.className = 'msg msg-user';
1157
+ div.innerHTML = `<span class="msg-prefix">user&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1158
+ container.appendChild(div);
1159
+ scrollToBottom();
1160
+ }
1161
+
1162
+ function addAssistantMessage() {
1163
+ const container = document.getElementById('chat-messages');
1164
+ const div = document.createElement('div');
1165
+ div.className = 'msg msg-assistant';
1166
+ div.id = 'current-assistant-msg';
1167
+ div.innerHTML = `<span class="msg-prefix">ai&gt;</span><span class="msg-content streaming-cursor"></span>`;
1168
+ container.appendChild(div);
1169
+ state.reasoningExpanded = false;
1170
+ scrollToBottom();
1171
+ return div;
1172
+ }
1173
+
1174
+ function updateAssistantMessage(content, isStreaming) {
1175
+ const div = document.getElementById('current-assistant-msg');
1176
+ if (!div) return;
1177
+ const contentEl = div.querySelector('.msg-content');
1178
+ const keepReasoningExpanded = state.reasoningExpanded || Boolean(contentEl.querySelector('.think-block.open'));
1179
+ state.reasoningExpanded = keepReasoningExpanded;
1180
+ contentEl.innerHTML = parseMarkdown(content);
1181
+ contentEl.querySelectorAll('.think-block').forEach((block) => {
1182
+ setReasoningBlockOpen(block, keepReasoningExpanded);
1183
+ });
1184
+ if (isStreaming) {
1185
+ contentEl.classList.add('streaming-cursor');
1186
+ } else {
1187
+ contentEl.classList.remove('streaming-cursor');
1188
+ }
1189
+ scrollToBottom();
1190
+ }
1191
+
1192
+ function finalizeAssistantMessage() {
1193
+ const div = document.getElementById('current-assistant-msg');
1194
+ if (div) {
1195
+ div.id = '';
1196
+ const contentEl = div.querySelector('.msg-content');
1197
+ if (contentEl) contentEl.classList.remove('streaming-cursor');
1198
+ }
1199
+ }
1200
+
1201
+ function scrollToBottom() {
1202
+ const container = document.getElementById('chat-messages');
1203
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1204
+ }
1205
+
1206
+ // ═══════════════════════════════════════════════════════
1207
+ // MARKDOWN PARSER
1208
+ // ═══════════════════════════════════════════════════════
1209
+ function parseMarkdown(text) {
1210
+ if (!text) return '';
1211
+
1212
+ const thinkBlocks = [];
1213
+ text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
1214
+ const idx = thinkBlocks.length;
1215
+ thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (click to expand)'));
1216
+ return `@@THINKBLOCK_${idx}@@`;
1217
+ });
1218
+ text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
1219
+ const idx = thinkBlocks.length;
1220
+ thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (thinking...)'));
1221
+ return `@@THINKBLOCK_${idx}@@`;
1222
+ });
1223
+
1224
+ const codeBlocks = [];
1225
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1226
+ const idx = codeBlocks.length;
1227
+ codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() });
1228
+ return `@@CODEBLOCK_${idx}@@`;
1229
+ });
1230
+
1231
+ text = escapeHtml(text);
1232
+
1233
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1234
+ text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
1235
+ text = text.replace(/`([^`]+?)`/g, '<code>$1</code>');
1236
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
1237
+
1238
+ text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
1239
+ text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
1240
+ text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');
1241
+
1242
+ text = text.replace(/^(?:[-*]) (.+)$/gm, '<li>$1</li>');
1243
+ text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
1244
+ text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
1245
+ text = text.replace(/(<li>(?:(?!<\/?[uo]l>).)*<\/li>(?:\s*<li>(?:(?!<\/?[uo]l>).)*<\/li>)*)/g, (match) => {
1246
+ if (!match.includes('<ul>') && !match.includes('</ul>')) return '<ol>' + match + '</ol>';
1247
+ return match;
1248
+ });
1249
+
1250
+ text = text.replace(/@@CODEBLOCK_(\d+)@@/g, (_, idx) => {
1251
+ const block = codeBlocks[parseInt(idx)];
1252
+ const escapedCode = escapeHtml(block.code);
1253
+ const id = `code-${Date.now()}-${idx}`;
1254
+ return `<div class="code-block-wrap"><div class="code-block-header"><span class="code-lang">${escapeHtml(block.lang)}</span><button class="btn-copy" onclick="copyBlock(this, '${id}')">&#128203; Copy</button></div><pre><code id="${id}">${escapedCode}</code></pre></div>`;
1255
+ });
1256
+ text = text.replace(/@@THINKBLOCK_(\d+)@@/g, (_, idx) => thinkBlocks[parseInt(idx)]);
1257
+
1258
+ text = text.replace(/\n\n/g, '</p><p>');
1259
+ text = text.replace(/\n/g, '<br>');
1260
+ text = '<p>' + text + '</p>';
1261
+ text = text.replace(/<p>\s*<\/p>/g, '');
1262
+ text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]))/g, '$1');
1263
+ text = text.replace(/(<\/(?:div|ul|ol|h[1-3])>)<\/p>/g, '$1');
1264
+
1265
+ return text;
1266
+ }
1267
+
1268
+ function renderThinkBlock(content, summary) {
1269
+ const escapedContent = escapeHtml(content.trim()).replace(/\n/g, '<br>');
1270
+ const openClass = state.reasoningExpanded ? ' open' : '';
1271
+ const expanded = state.reasoningExpanded ? 'true' : 'false';
1272
+ return `<div class="think-block${openClass}"><button type="button" class="think-summary" aria-expanded="${expanded}">${summary}</button><div class="think-content">${escapedContent}</div></div>`;
1273
+ }
1274
+
1275
+ function handleReasoningPress(event) { updateReasoningFromEvent(event); }
1276
+ function handleReasoningKeydown(event) {
1277
+ if (event.key !== 'Enter' && event.key !== ' ') return;
1278
+ updateReasoningFromEvent(event);
1279
+ }
1280
+
1281
+ function updateReasoningFromEvent(event) {
1282
+ if (event.type === 'mousedown' && Date.now() - state.lastReasoningPressAt < 500) return;
1283
+ const target = event.target;
1284
+ if (!target || !target.closest) return;
1285
+ const button = target.closest('.think-summary');
1286
+ if (!button) return;
1287
+ const block = button.closest('.think-block');
1288
+ if (!block) return;
1289
+ event.preventDefault();
1290
+ event.stopPropagation();
1291
+ if (event.stopImmediatePropagation) event.stopImmediatePropagation();
1292
+ state.lastReasoningPressAt = Date.now();
1293
+ const nextOpen = !block.classList.contains('open');
1294
+ state.reasoningExpanded = nextOpen;
1295
+ const scope = block.closest('.msg-content') || document;
1296
+ scope.querySelectorAll('.think-block').forEach((trace) => {
1297
+ setReasoningBlockOpen(trace, nextOpen);
1298
+ });
1299
+ }
1300
+
1301
+ function setReasoningBlockOpen(block, open) {
1302
+ block.classList.toggle('open', open);
1303
+ const button = block.querySelector('.think-summary');
1304
+ if (button) button.setAttribute('aria-expanded', open ? 'true' : 'false');
1305
+ }
1306
+
1307
+ function escapeHtml(text) {
1308
+ const div = document.createElement('div');
1309
+ div.textContent = text;
1310
+ return div.innerHTML;
1311
+ }
1312
+
1313
+ // ═══════════════════════════════════════════════════════
1314
+ // COPY FUNCTIONS
1315
+ // ═══════════════════════════════════════════════════════
1316
+ function copyBlock(button, codeId) {
1317
+ const codeEl = document.getElementById(codeId);
1318
+ if (!codeEl) return;
1319
+ navigator.clipboard.writeText(codeEl.textContent).then(() => {
1320
+ button.textContent = '\u2713 Copied!';
1321
+ button.classList.add('copied');
1322
+ setTimeout(() => { button.textContent = '\ud83d\udccb Copy'; button.classList.remove('copied'); }, 2000);
1323
+ });
1324
+ }
1325
+
1326
+ function copyCode() {
1327
+ if (!state.lastCode) return;
1328
+ const btn = document.getElementById('btn-copy-code');
1329
+ navigator.clipboard.writeText(state.lastCode).then(() => {
1330
+ btn.textContent = '\u2713 Copied!';
1331
+ setTimeout(() => { btn.textContent = '\ud83d\udccb Copy'; }, 2000);
1332
+ });
1333
+ }
1334
+
1335
+ // ═══════════════════════════════════════════════════════
1336
+ // STATUS BAR
1337
+ // ═══════════════════════════════════════════════════════
1338
+ function renderStatus(text, statusState) {
1339
+ const indicator = document.getElementById('status-indicator');
1340
+ const textEl = document.getElementById('status-text');
1341
+ const dotEl = indicator.querySelector('.status-dot');
1342
+
1343
+ indicator.className = 'status-indicator';
1344
+ switch (statusState) {
1345
+ case 'working': indicator.classList.add('status-working'); dotEl.textContent = '\u25d0'; break;
1346
+ case 'success': indicator.classList.add('status-success'); dotEl.textContent = '\u2713'; break;
1347
+ case 'error': indicator.classList.add('status-error'); dotEl.textContent = '\u2717'; break;
1348
+ case 'info': indicator.classList.add('status-info'); dotEl.textContent = '\u2139'; break;
1349
+ default: indicator.classList.add('status-idle'); dotEl.textContent = '\u25cf';
1350
+ }
1351
+ textEl.textContent = text || 'IDLE';
1352
+ }
1353
+
1354
+ // ═══════════════════════════════════════════════════════
1355
+ // OUTPUT PANEL
1356
+ // ═══════════════════════════════════════════════════════
1357
+ function switchTab(tab, { forcePreviewReload = false } = {}) {
1358
+ const wasPreview = state.activeTab === 'preview';
1359
+ state.activeTab = tab;
1360
+ document.querySelectorAll('.output-tab').forEach((btn) => {
1361
+ btn.classList.toggle('active', btn.dataset.tab === tab);
1362
+ });
1363
+ document.querySelectorAll('.tab-pane').forEach((pane) => {
1364
+ pane.classList.toggle('active', pane.id === `pane-${tab}`);
1365
+ });
1366
+ if (tab === 'preview') {
1367
+ ensureWebPreviewLoaded({ forceReload: forcePreviewReload || !wasPreview });
1368
+ }
1369
+ }
1370
+
1371
+ function renderExecution(execution) {
1372
+ if (!execution) return;
1373
+ state.lastExecution = execution;
1374
+
1375
+ // Console
1376
+ document.getElementById('console-stdout').textContent = execution.stdout || 'No output.';
1377
+ document.getElementById('console-stderr').textContent = execution.stderr || 'No errors.';
1378
+
1379
+ // Code
1380
+ if (execution.code) {
1381
+ state.lastCode = execution.code;
1382
+ state.lastCodeLang = execution.language || 'code';
1383
+ document.getElementById('code-tab-lang').textContent = state.lastCodeLang;
1384
+ document.getElementById('code-display').innerHTML = `<pre>${escapeHtml(execution.code)}</pre>`;
1385
+ }
1386
+
1387
+ // Download
1388
+ const dlBtn = document.getElementById('btn-download');
1389
+ if (execution.download_url) {
1390
+ dlBtn.href = execution.download_url;
1391
+ dlBtn.style.display = 'inline-flex';
1392
+ dlBtn.setAttribute('download', '');
1393
+ } else {
1394
+ dlBtn.style.display = 'none';
1395
+ }
1396
+
1397
+ // Preview
1398
+ const placeholder = document.getElementById('preview-placeholder');
1399
+ const img = document.getElementById('preview-image');
1400
+ const iframe = getPreviewIframe();
1401
+ const fsBtn = document.getElementById('btn-fullscreen');
1402
+
1403
+ if (execution.image_url) {
1404
+ placeholder.style.display = 'none';
1405
+ iframe.style.display = 'none';
1406
+ fsBtn.style.display = 'none';
1407
+ img.src = execution.image_url;
1408
+ img.style.display = 'block';
1409
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'deploy') {
1410
+ switchTab('preview');
1411
+ }
1412
+ } else if (execution.is_web && execution.code) {
1413
+ placeholder.style.display = 'none';
1414
+ img.style.display = 'none';
1415
+ iframe.style.display = 'block';
1416
+ fsBtn.style.display = 'block';
1417
+ state.pendingWebPreviewCode = execution.code;
1418
+ state.loadedWebPreviewCode = '';
1419
+ state.scheduledWebPreviewCode = '';
1420
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'deploy') {
1421
+ switchTab('preview', { forcePreviewReload: true });
1422
+ } else {
1423
+ iframe.srcdoc = '';
1424
+ }
1425
+ } else {
1426
+ if (execution.stdout || execution.stderr) {
1427
+ const suggested = execution.suggested_tab || 'console';
1428
+ if (state.activeTab !== 'deploy') switchTab(suggested);
1429
+ }
1430
+ }
1431
+
1432
+ // Deploy tab - project files
1433
+ renderProjectFiles(execution.project_files || {});
1434
+
1435
+ // Enable deploy button
1436
+ document.getElementById('btn-push-hf').disabled = !execution.code;
1437
+ }
1438
+
1439
+ function renderProjectFiles(files) {
1440
+ const container = document.getElementById('project-files');
1441
+ if (!files || Object.keys(files).length === 0) {
1442
+ container.innerHTML = '';
1443
+ return;
1444
+ }
1445
+
1446
+ let html = '<div style="margin-top: 12px; font-size: 10px; color: var(--gray-dim); letter-spacing: 1px; text-transform: uppercase;">Project Files:</div>';
1447
+ for (const [filepath, content] of Object.entries(files)) {
1448
+ const ext = filepath.split('.').pop();
1449
+ const icon = getFileIcon(ext);
1450
+ html += `<div class="file-item"><span class="file-icon">${icon}</span><span class="file-name">${escapeHtml(filepath)}</span><span style="color:var(--gray-dim);font-size:10px;">(${content.length} chars)</span></div>`;
1451
+ }
1452
+ container.innerHTML = html;
1453
+ }
1454
+
1455
+ function getFileIcon(ext) {
1456
+ const icons = {
1457
+ 'py': '\ud83d\udc0d', 'js': '\u26a1', 'ts': '\ud83d\udde1\ufe0f', 'html': '\ud83c\udf10',
1458
+ 'css': '\ud83c\udfa8', 'json': '\ud83d\udcc4', 'md': '\ud83d\udcd3', 'yml': '\u2699\ufe0f',
1459
+ 'yaml': '\u2699\ufe0f', 'java': '\u2615', 'go': '\ud83e\udd85', 'rs': '\ud83e\udd80',
1460
+ 'php': '\ud83d\udc18', 'rb': '\ud83d\udc8e', 'swift': '\ud83e\udd85', 'kt': '\ud83c\udf0a',
1461
+ };
1462
+ return icons[ext] || '\ud83d\udcc1';
1463
+ }
1464
+
1465
+ function resetOutput() {
1466
+ const iframe = getPreviewIframe();
1467
+ document.getElementById('preview-placeholder').style.display = '';
1468
+ document.getElementById('preview-image').style.display = 'none';
1469
+ iframe.style.display = 'none';
1470
+ iframe.srcdoc = '';
1471
+ document.getElementById('btn-fullscreen').style.display = 'none';
1472
+ document.getElementById('console-stdout').textContent = 'No output.';
1473
+ document.getElementById('console-stderr').textContent = 'No errors.';
1474
+ document.getElementById('code-display').innerHTML = '<div class="code-placeholder">No code generated yet.</div>';
1475
+ document.getElementById('code-tab-lang').textContent = '\u2014';
1476
+ document.getElementById('btn-download').style.display = 'none';
1477
+ document.getElementById('project-files').innerHTML = '';
1478
+ document.getElementById('deploy-status').className = 'deploy-status';
1479
+ document.getElementById('deploy-status').style.display = 'none';
1480
+ state.lastExecution = null;
1481
+ state.lastCode = '';
1482
+ state.lastCodeLang = '';
1483
+ state.pendingWebPreviewCode = '';
1484
+ state.loadedWebPreviewCode = '';
1485
+ state.scheduledWebPreviewCode = '';
1486
+ }
1487
+
1488
+ // ═══════════════════════════════════════════════════════
1489
+ // FULLSCREEN
1490
+ // ═══════════════════════════════════════════════════════
1491
+ function getPreviewIframe() { return document.getElementById('preview-iframe'); }
1492
+
1493
+ function recreatePreviewIframe() {
1494
+ const oldFrame = getPreviewIframe();
1495
+ const freshFrame = document.createElement('iframe');
1496
+ freshFrame.id = 'preview-iframe';
1497
+ freshFrame.setAttribute('sandbox', 'allow-scripts');
1498
+ freshFrame.style.display = oldFrame.style.display || 'block';
1499
+ oldFrame.replaceWith(freshFrame);
1500
+ return freshFrame;
1501
+ }
1502
+
1503
+ function ensureWebPreviewLoaded({ forceReload = false } = {}) {
1504
+ const iframe = getPreviewIframe();
1505
+ if (!state.pendingWebPreviewCode || state.activeTab !== 'preview' || iframe.style.display === 'none') return;
1506
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) {
1507
+ schedulePreviewResize(iframe);
1508
+ return;
1509
+ }
1510
+ if (!forceReload && state.scheduledWebPreviewCode === state.pendingWebPreviewCode) return;
1511
+
1512
+ state.scheduledWebPreviewCode = state.pendingWebPreviewCode;
1513
+ iframe.srcdoc = '';
1514
+ const loadWhenLaidOut = () => {
1515
+ if (state.activeTab !== 'preview' || !state.pendingWebPreviewCode) {
1516
+ state.scheduledWebPreviewCode = '';
1517
+ return;
1518
+ }
1519
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) return;
1520
+ const visibleFrame = getPreviewIframe();
1521
+ const rect = visibleFrame.getBoundingClientRect();
1522
+ if (rect.width < 10 || rect.height < 10) {
1523
+ state.scheduledWebPreviewCode = '';
1524
+ setTimeout(() => ensureWebPreviewLoaded({ forceReload }), 50);
1525
+ return;
1526
+ }
1527
+ const freshFrame = recreatePreviewIframe();
1528
+ freshFrame.srcdoc = state.pendingWebPreviewCode;
1529
+ state.loadedWebPreviewCode = state.pendingWebPreviewCode;
1530
+ state.scheduledWebPreviewCode = '';
1531
+ schedulePreviewResize(freshFrame);
1532
+ };
1533
+ requestAnimationFrame(() => requestAnimationFrame(loadWhenLaidOut));
1534
+ setTimeout(loadWhenLaidOut, 75);
1535
+ }
1536
+
1537
+ function schedulePreviewResize(iframe) {
1538
+ const dispatchResize = () => {
1539
+ try { iframe.contentWindow?.dispatchEvent(new Event('resize')); } catch (_err) {}
1540
+ };
1541
+ requestAnimationFrame(() => requestAnimationFrame(dispatchResize));
1542
+ setTimeout(dispatchResize, 100);
1543
+ setTimeout(dispatchResize, 350);
1544
+ }
1545
+
1546
+ function observePreviewSize() {
1547
+ const previewPane = document.getElementById('pane-preview');
1548
+ if (!previewPane) return;
1549
+ window.addEventListener('resize', () => {
1550
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1551
+ schedulePreviewResize(getPreviewIframe());
1552
+ }
1553
+ });
1554
+ if (typeof ResizeObserver === 'undefined') return;
1555
+ const observer = new ResizeObserver(() => {
1556
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1557
+ schedulePreviewResize(getPreviewIframe());
1558
+ }
1559
+ });
1560
+ observer.observe(previewPane);
1561
+ }
1562
+
1563
+ function openFullscreen() {
1564
+ const overlay = document.getElementById('fullscreen-overlay');
1565
+ const iframe = document.getElementById('fullscreen-iframe');
1566
+ if (state.lastExecution && state.lastExecution.is_web && state.lastExecution.code) {
1567
+ iframe.srcdoc = state.lastExecution.code;
1568
+ }
1569
+ overlay.classList.add('active');
1570
+ }
1571
+
1572
+ function closeFullscreen() {
1573
+ document.getElementById('fullscreen-overlay').classList.remove('active');
1574
+ document.getElementById('fullscreen-iframe').srcdoc = '';
1575
+ }
1576
+
1577
+ function handleFullscreenKeydown(event) {
1578
+ if (event.key !== 'Escape') return;
1579
+ const overlay = document.getElementById('fullscreen-overlay');
1580
+ if (!overlay.classList.contains('active')) return;
1581
+ event.preventDefault();
1582
+ closeFullscreen();
1583
+ }
1584
+
1585
+ // ═══════════════════════════════════════════════════════
1586
+ // SEND / RECEIVE
1587
+ // ═══════════════════════════════════════════════════════
1588
+ function handleSend() {
1589
+ const input = document.getElementById('chat-input');
1590
+ const prompt = input.value.trim();
1591
+ if (!prompt || state.isGenerating) return;
1592
+ input.value = '';
1593
+ autoResize();
1594
+ sendMessage(prompt);
1595
+ }
1596
+
1597
+ async function sendMessage(prompt) {
1598
+ if (state.isGenerating) return;
1599
+
1600
+ if (!state.modelReady) {
1601
+ addSystemMessage('The model is still loading. Please wait...');
1602
+ return;
1603
+ }
1604
+
1605
+ state.isGenerating = true;
1606
+ toggleInputState(true);
1607
+ addUserMessage(prompt);
1608
+ addAssistantMessage();
1609
+ renderStatus('Thinking...', 'working');
1610
+
1611
+ const historyJSON = JSON.stringify(state.history);
1612
+ const execContextJSON = JSON.stringify(state.executionContext);
1613
+ const framework = document.getElementById('framework-select')?.value || state.targetFramework;
1614
+
1615
+ try {
1616
+ const resp = await fetch('/gradio_api/call/chat', {
1617
+ method: 'POST',
1618
+ headers: { 'Content-Type': 'application/json' },
1619
+ body: JSON.stringify({
1620
+ data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON]
1621
+ })
1622
+ });
1623
+
1624
+ if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
1625
+
1626
+ const { event_id } = await resp.json();
1627
+ const eventSource = new EventSource(`/gradio_api/call/chat/${event_id}`);
1628
+ state.currentEventSource = eventSource;
1629
+
1630
+ eventSource.addEventListener('generating', (e) => {
1631
+ try {
1632
+ const dataArray = JSON.parse(e.data);
1633
+ const payload = JSON.parse(dataArray[0]);
1634
+ handlePayload(payload, true);
1635
+ } catch (err) { console.error('Parse error (generating):', err); }
1636
+ });
1637
+
1638
+ eventSource.addEventListener('complete', (e) => {
1639
+ try {
1640
+ const dataArray = JSON.parse(e.data);
1641
+ const payload = JSON.parse(dataArray[0]);
1642
+ handlePayload(payload, false);
1643
+ } catch (err) { console.error('Parse error (complete):', err); }
1644
+ eventSource.close();
1645
+ onGenerationEnd();
1646
+ });
1647
+
1648
+ eventSource.addEventListener('error', (e) => {
1649
+ let errorMsg = 'An error occurred during generation.';
1650
+ if (e.data) errorMsg = e.data;
1651
+ console.error('SSE error:', errorMsg);
1652
+ finalizeAssistantMessage();
1653
+ addSystemMessage(`Error: ${errorMsg}`);
1654
+ renderStatus('Error', 'error');
1655
+ eventSource.close();
1656
+ onGenerationEnd();
1657
+ });
1658
+
1659
+ } catch (err) {
1660
+ console.error('Send error:', err);
1661
+ finalizeAssistantMessage();
1662
+ addSystemMessage(`Error: ${err.message}`);
1663
+ renderStatus('Error', 'error');
1664
+ onGenerationEnd();
1665
+ }
1666
+ }
1667
+
1668
+ function handlePayload(payload, isStreaming) {
1669
+ if (payload.status_text) renderStatus(payload.status_text, payload.status_state || 'working');
1670
+
1671
+ if (payload.history) {
1672
+ state.history = payload.history;
1673
+ const lastMsg = payload.history[payload.history.length - 1];
1674
+ if (lastMsg && lastMsg.role === 'assistant') {
1675
+ updateAssistantMessage(lastMsg.content, isStreaming);
1676
+ }
1677
+ }
1678
+
1679
+ if (payload.execution) {
1680
+ renderExecution(payload.execution);
1681
+ if (payload.execution) state.executionContext = payload.execution;
1682
+ }
1683
+
1684
+ if (payload.type === 'complete') {
1685
+ finalizeAssistantMessage();
1686
+ renderStatus('Done', 'success');
1687
+ setTimeout(() => { if (!state.isGenerating) renderStatus('Idle', 'idle'); }, 3000);
1688
+ }
1689
+
1690
+ if (payload.type === 'error') {
1691
+ finalizeAssistantMessage();
1692
+ addSystemMessage(`Error: ${payload.status_text || 'Unknown error'}`);
1693
+ renderStatus('Error', 'error');
1694
+ }
1695
+
1696
+ if (payload.execution && payload.execution.suggested_tab) {
1697
+ switchTab(payload.execution.suggested_tab);
1698
+ }
1699
+ }
1700
+
1701
+ function onGenerationEnd() {
1702
+ state.isGenerating = false;
1703
+ state.currentEventSource = null;
1704
+ toggleInputState(false);
1705
+ }
1706
+
1707
+ function toggleInputState(generating) {
1708
+ const sendBtn = document.getElementById('btn-send');
1709
+ const stopBtn = document.getElementById('btn-stop');
1710
+ const input = document.getElementById('chat-input');
1711
+
1712
+ if (generating) {
1713
+ sendBtn.style.display = 'none';
1714
+ stopBtn.style.display = 'flex';
1715
+ input.disabled = true;
1716
+ input.placeholder = 'Generating...';
1717
+ } else {
1718
+ sendBtn.style.display = 'flex';
1719
+ stopBtn.style.display = 'none';
1720
+ sendBtn.disabled = false;
1721
+ input.disabled = false;
1722
+ input.placeholder = 'Describe the app you want to build...';
1723
+ input.focus();
1724
+ }
1725
+ }
1726
+
1727
+ function stopGeneration() {
1728
+ if (state.currentEventSource) {
1729
+ state.currentEventSource.close();
1730
+ state.currentEventSource = null;
1731
+ }
1732
+ finalizeAssistantMessage();
1733
+ addSystemMessage('Generation stopped by user.');
1734
+ renderStatus('Stopped', 'info');
1735
+ onGenerationEnd();
1736
+ }
1737
+
1738
+ function resetConversation(announcement) {
1739
+ state.history = [];
1740
+ state.executionContext = {};
1741
+ state.lastExecution = null;
1742
+ state.lastCode = '';
1743
+ state.lastCodeLang = '';
1744
+ state.reasoningExpanded = false;
1745
+
1746
+ if (state.currentEventSource) {
1747
+ state.currentEventSource.close();
1748
+ state.currentEventSource = null;
1749
+ }
1750
+ state.isGenerating = false;
1751
+ toggleInputState(false);
1752
+
1753
+ document.getElementById('chat-messages').innerHTML = '';
1754
+ resetOutput();
1755
+ switchTab('preview');
1756
+ renderStatus('Idle', 'idle');
1757
+ if (announcement) addSystemMessage(announcement);
1758
+ }
1759
+
1760
+ function newChat() {
1761
+ resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'Fullstack Code Builder'}.`);
1762
+ }
1763
+
1764
+ // ═══════════════════════════════════════════════════════
1765
+ // HUGGINGFACE PUSH
1766
+ // ═══════════════════════════════════════════════════════
1767
+ async function pushToHuggingFace() {
1768
+ const repoName = document.getElementById('hf-repo-name').value.trim();
1769
+ const hfToken = document.getElementById('hf-token').value.trim();
1770
+ const spaceSdk = document.getElementById('hf-space-sdk').value;
1771
+ const statusEl = document.getElementById('deploy-status');
1772
+
1773
+ if (!repoName) {
1774
+ statusEl.className = 'deploy-status error';
1775
+ statusEl.textContent = 'Please enter a repository name.';
1776
+ statusEl.style.display = 'block';
1777
+ return;
1778
+ }
1779
+ if (!hfToken) {
1780
+ statusEl.className = 'deploy-status error';
1781
+ statusEl.textContent = 'Please enter your HuggingFace token.';
1782
+ statusEl.style.display = 'block';
1783
+ return;
1784
+ }
1785
+
1786
+ if (!state.executionContext || !state.executionContext.code) {
1787
+ statusEl.className = 'deploy-status error';
1788
+ statusEl.textContent = 'No code to push. Generate some code first.';
1789
+ statusEl.style.display = 'block';
1790
+ return;
1791
+ }
1792
+
1793
+ statusEl.className = 'deploy-status working';
1794
+ statusEl.textContent = 'Pushing to HuggingFace...';
1795
+ statusEl.style.display = 'block';
1796
+
1797
+ const btn = document.getElementById('btn-push-hf');
1798
+ btn.disabled = true;
1799
+
1800
+ try {
1801
+ const execContextJSON = JSON.stringify(state.executionContext);
1802
+
1803
+ const resp = await fetch('/gradio_api/call/push_hf', {
1804
+ method: 'POST',
1805
+ headers: { 'Content-Type': 'application/json' },
1806
+ body: JSON.stringify({
1807
+ data: [execContextJSON, repoName, hfToken, spaceSdk, 'true']
1808
+ })
1809
+ });
1810
+
1811
+ if (!resp.ok) throw new Error(`API error: ${resp.status}`);
1812
+
1813
+ const { event_id } = await resp.json();
1814
+
1815
+ const eventSource = new EventSource(`/gradio_api/call/push_hf/${event_id}`);
1816
+
1817
+ eventSource.addEventListener('complete', (e) => {
1818
+ try {
1819
+ const dataArray = JSON.parse(e.data);
1820
+ const result = JSON.parse(dataArray[0]);
1821
+
1822
+ if (result.success) {
1823
+ statusEl.className = 'deploy-status success';
1824
+ statusEl.innerHTML = `\u2713 ${result.message}<br><a href="${result.url}" target="_blank" rel="noopener">${result.url} \u2197</a>`;
1825
+ } else {
1826
+ statusEl.className = 'deploy-status error';
1827
+ statusEl.textContent = `\u2717 ${result.message}`;
1828
+ }
1829
+ } catch (err) {
1830
+ statusEl.className = 'deploy-status error';
1831
+ statusEl.textContent = `Parse error: ${err.message}`;
1832
+ }
1833
+ eventSource.close();
1834
+ btn.disabled = false;
1835
+ });
1836
+
1837
+ eventSource.addEventListener('error', (e) => {
1838
+ statusEl.className = 'deploy-status error';
1839
+ statusEl.textContent = `Push failed: ${e.data || 'Unknown error'}`;
1840
+ eventSource.close();
1841
+ btn.disabled = false;
1842
+ });
1843
+
1844
+ } catch (err) {
1845
+ statusEl.className = 'deploy-status error';
1846
+ statusEl.textContent = `Push failed: ${err.message}`;
1847
+ btn.disabled = false;
1848
+ }
1849
+ }
1850
+ </script>
1851
+ </body>
1852
+ </html>
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ gradio==6.14.0
2
+ transformers>=4.45.0
3
+ torch>=2.1.0
4
+ accelerate>=0.25.0
5
+ huggingface_hub>=0.20.0
6
+ matplotlib>=3.8