R-Kentaren commited on
Commit
ccb935d
Β·
verified Β·
1 Parent(s): c677d26

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,63 @@
1
  ---
2
- title: Sonicoder
3
- emoji: ⚑
4
  colorFrom: purple
5
- colorTo: yellow
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: SoniCoder
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
+ ## SoniCoder
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,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """SoniCoder β€” entry point.
2
+
3
+ Uses MiniCPM5-1B (text) or MiniCPM-V-4.6 (vision+text) for local inference.
4
+ No external APIs required.
5
+ Supports generating fullstack applications in any language.
6
+ Can push generated projects to HuggingFace Hub.
7
+ Web search via Google scraping (no API keys needed).
8
+ Gradio app support for Python.
9
+ Image understanding with MiniCPM-V-4.6.
10
+
11
+ Project structure:
12
+ code/
13
+ β”œβ”€β”€ config/constants.py App constants, model configs, system prompt
14
+ β”œβ”€β”€ model/loader.py Dual model loading & switching
15
+ β”œβ”€β”€ model/inference.py Streaming inference (text + VLM)
16
+ β”œβ”€β”€ execution/code_extractor.py Code extraction & language normalization
17
+ β”œβ”€β”€ execution/python_runner.py Sandboxed Python execution
18
+ β”œβ”€β”€ execution/gradio_runner.py Gradio app subprocess runner
19
+ β”œβ”€β”€ websearch/google_scraper.py Web search scraping (no API)
20
+ β”œβ”€β”€ huggingface/push.py HuggingFace Hub push & ZIP packaging
21
+ β”œβ”€β”€ server/chat_helpers.py Chat history & prompt building
22
+ └── server/routes.py FastAPI / Gradio server routes
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+
29
+ from code.model.loader import start_background_load
30
+ from code.server.routes import get_app
31
+
32
+ logging.basicConfig(level=logging.INFO)
33
+ logger = logging.getLogger(__name__)
34
+
35
+ # Start loading default model in background
36
+ start_background_load()
37
+
38
+ # Launch the server
39
+ application = get_app()
40
+ application.launch(show_error=True)
code/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """SoniCoder β€” modular package."""
code/config/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Configuration and constants."""
code/config/constants.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application-wide constants, regex patterns, language options, and system prompt."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ # ─── App Identity ────────────────────────────────────────────────────────
8
+
9
+ APP_TITLE = "SoniCoder"
10
+ MODEL_URL = "https://huggingface.co/openbmb/MiniCPM5-1B"
11
+
12
+ # ─── Model Configs ───────────────────────────────────────────────────────
13
+
14
+ MODEL_CONFIGS = {
15
+ "minicpm5-1b": {
16
+ "id": "openbmb/MiniCPM5-1B",
17
+ "name": "MiniCPM5-1B",
18
+ "type": "text",
19
+ "description": "Text-only, fast code generation",
20
+ "auto_class": "AutoModelForCausalLM",
21
+ "tokenizer_class": "AutoTokenizer",
22
+ "size_gb": 2.17,
23
+ },
24
+ "minicpm-v-4.6": {
25
+ "id": "openbmb/MiniCPM-V-4.6",
26
+ "name": "MiniCPM-V-4.6",
27
+ "type": "vlm",
28
+ "description": "Vision + Text, image understanding & code",
29
+ "auto_class": "AutoModelForImageTextToText",
30
+ "processor_class": "AutoProcessor",
31
+ "size_gb": 2.8,
32
+ },
33
+ }
34
+
35
+ DEFAULT_MODEL_KEY = "minicpm5-1b"
36
+
37
+ # Keep backward compat aliases
38
+ MODEL_ID = MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"]
39
+
40
+ # ─── Runtime Defaults ───────────────────────────────────────────────────
41
+
42
+ DEFAULT_TEMPERATURE = 0.4
43
+ DEFAULT_MAX_TOKENS = 2048
44
+ PY_TIMEOUT_S = 15
45
+ GRADIO_TIMEOUT_S = 30
46
+ PY_MEM_LIMIT_MB = 1024
47
+ MAX_STDIO_CHARS = 16_000
48
+ OUTPUT_PNG = "output.png"
49
+
50
+ # ─── Regex Patterns ─────────────────────────────────────────────────────
51
+
52
+ THINKING_BLOCK_RE = re.compile(
53
+ r"<\s*think\s*>.*?<\s*/\s*think\s*>", re.IGNORECASE | re.DOTALL
54
+ )
55
+ CODE_BLOCK_RE = re.compile(
56
+ r"```([a-zA-Z0-9_+.#-]*)\s*\n(.*?)```", re.DOTALL
57
+ )
58
+ FILE_BLOCK_RE = re.compile(
59
+ r"@@FILE:\s*(.+?)@@\s*\n(.*?)(?=@@FILE:|@@END@@)", re.DOTALL
60
+ )
61
+
62
+ # ─── Supported Languages & Frameworks ───────────────────────────────────
63
+
64
+ LANGUAGE_OPTIONS: list[tuple[str, list[str]]] = [
65
+ ("Python", ["Gradio", "Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]),
66
+ ("JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]),
67
+ ("TypeScript", ["React", "Next.js", "Express.js", "NestJS"]),
68
+ ("HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]),
69
+ ("Java", ["Spring Boot", "Maven", "Gradle"]),
70
+ ("Go", ["Gin", "Fiber", "Echo", "Plain Go"]),
71
+ ("Rust", ["Actix", "Axum", "Rocket"]),
72
+ ("PHP", ["Laravel", "Symfony", "Plain PHP"]),
73
+ ("Ruby", ["Rails", "Sinatra"]),
74
+ ("C#", ["ASP.NET", "Blazor"]),
75
+ ("Swift", ["Vapor", "SwiftUI"]),
76
+ ("Kotlin", ["Ktor", "Spring Boot"]),
77
+ ]
78
+
79
+ LANGUAGE_MAP: dict[str, list[str]] = {lang: frameworks for lang, frameworks in LANGUAGE_OPTIONS}
80
+
81
+ # ─── System Prompt ───────────────────────────────────────────────────────
82
+
83
+ SYSTEM_PROMPT = """You are a code generator. Output ONLY the code. No thinking, no explanation, no commentary.
84
+
85
+ CRITICAL RULES:
86
+ - Do NOT use <think> or <thinking> tags. Do NOT reason aloud. Just output code directly.
87
+ - Do NOT write explanations before or after code. Just output the code.
88
+ - If you must explain something, keep it to ONE short sentence.
89
+
90
+ When the user asks you to build an application:
91
+ 1. Generate complete, working code - not snippets or pseudocode
92
+ 2. Include all necessary files for the project to run
93
+ 3. Add proper error handling and comments
94
+ 4. For web apps, make the UI responsive and modern
95
+ 5. For Gradio apps, use gradio library and create a complete working app with gr.Interface or gr.Blocks
96
+
97
+ FILE OUTPUT FORMAT - IMPORTANT:
98
+ When generating multi-file projects, wrap each file in this format:
99
+ @@FILE: path/to/file.ext@@
100
+ (file content here)
101
+ @@FILE: path/to/another/file.ext@@
102
+ (another file content here)
103
+ @@END@@
104
+
105
+ For single-file code, use standard markdown fenced blocks:
106
+ ```python for Python
107
+ ```html for HTML/CSS/JS
108
+ ```javascript for JavaScript
109
+ ```typescript for TypeScript
110
+ etc.
111
+
112
+ JAVASCRIPT / TYPESCRIPT PROJECTS:
113
+ For React, Next.js, Vue.js, Express, NestJS, or any JS/TS framework:
114
+ - ALWAYS use the @@FILE: multi-file format
115
+ - Include a package.json with name, version, scripts, and dependencies
116
+ - Include all source files (src/App.jsx, src/index.js, etc.)
117
+ - For React+Vite: include vite.config.js and index.html
118
+ - For Next.js: include next.config.js with output: 'standalone'
119
+ - For Express: main entry is index.js with app.listen(7860)
120
+ - Server ports MUST be 7860 and bind to 0.0.0.0
121
+ - Do NOT include node_modules or lock files
122
+
123
+ 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.
124
+
125
+ When generating Gradio apps, create a complete app.py with:
126
+ - import gradio as gr
127
+ - Define the interface using gr.Interface() or gr.Blocks()
128
+ - Call iface.launch(server_name="0.0.0.0", server_port=7860) at the end
129
+ - Include all necessary processing logic inline
130
+
131
+ For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code (except Gradio apps which are server-based).
132
+
133
+ If web search results are provided in the context, use them to inform your code generation. Incorporate relevant information from the search results into the generated code.
134
+
135
+ If the user provides an image, analyze it and generate code based on what you see in the image. For example: replicate a UI from a screenshot, generate code from a wireframe, or build an app described in a document.
136
+ """
137
+
138
+ # ─── Example Prompts ────────────────────────────────────────────────────
139
+
140
+ EXAMPLE_PROMPTS: list[tuple[str, str, str, str]] = [
141
+ (
142
+ "🎨 Gradio Image Filter",
143
+ "Create a Gradio app that lets users upload an image and apply filters like grayscale, blur, sepia, and edge detection using PIL. Show the original and filtered images side by side.",
144
+ "Python",
145
+ "Gradio",
146
+ ),
147
+ (
148
+ "πŸ€– Gradio Chat App",
149
+ "Build a Gradio chatbot app with gr.Blocks that has a chat interface, a text input, and a send button. Include a simple echo bot that repeats the user's message with a fun twist.",
150
+ "Python",
151
+ "Gradio",
152
+ ),
153
+ (
154
+ "🌐 React Todo App",
155
+ "Build a React todo application with add, delete, mark complete, and filter functionality. Use modern hooks and a clean responsive UI.",
156
+ "JavaScript",
157
+ "React",
158
+ ),
159
+ (
160
+ "🐍 Flask API",
161
+ "Create a Flask REST API for a book library with CRUD operations, in-memory storage, and proper error handling.",
162
+ "Python",
163
+ "Flask",
164
+ ),
165
+ (
166
+ "🎨 Landing Page",
167
+ "Build a modern landing page for a SaaS product with a hero section, features grid, pricing cards, and a footer. Use Tailwind-style CSS.",
168
+ "HTML/CSS/JS",
169
+ "Vanilla",
170
+ ),
171
+ (
172
+ "πŸ“Š Dashboard",
173
+ "Create an interactive data dashboard with charts (bar, line, pie), a sidebar navigation, and summary cards. All in a single HTML file.",
174
+ "HTML/CSS/JS",
175
+ "Vanilla",
176
+ ),
177
+ ]
code/execution/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Code execution engines."""
code/execution/code_extractor.py ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Code extraction from model responses.
2
+
3
+ Extracts fenced code blocks and multi-file @@FILE: blocks.
4
+ Normalizes language names and detects Gradio code.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import html
10
+ import re
11
+
12
+ from code.config.constants import (
13
+ CODE_BLOCK_RE,
14
+ FILE_BLOCK_RE,
15
+ THINKING_BLOCK_RE,
16
+ )
17
+
18
+
19
+ def strip_thinking_blocks(text: str) -> str:
20
+ """Remove <think/> blocks from model output."""
21
+ return THINKING_BLOCK_RE.sub("", text).strip()
22
+
23
+
24
+ def extract_code(response: str) -> tuple[str, str | None]:
25
+ """Return the first fenced code block and its language tag."""
26
+ visible_response = strip_thinking_blocks(response)
27
+ match = CODE_BLOCK_RE.search(visible_response)
28
+ if not match:
29
+ return "", None
30
+ return match.group(2).strip(), (match.group(1).strip().lower() or None)
31
+
32
+
33
+ def extract_multi_file(response: str) -> dict[str, str]:
34
+ """Extract multi-file project from @@FILE: format.
35
+
36
+ Returns dict of {filepath: content}.
37
+ """
38
+ files: dict[str, str] = {}
39
+ visible = strip_thinking_blocks(response)
40
+
41
+ for match in FILE_BLOCK_RE.finditer(visible):
42
+ filepath = match.group(1).strip()
43
+ content = match.group(2).strip()
44
+ files[filepath] = content
45
+
46
+ # Fallback: if no @@FILE: blocks found, extract single code block
47
+ if not files:
48
+ code, lang = extract_code(response)
49
+ if code:
50
+ ext_map = {
51
+ "python": "main.py", "py": "main.py",
52
+ "javascript": "index.js", "js": "index.js",
53
+ "typescript": "index.ts", "ts": "index.ts",
54
+ "html": "index.html",
55
+ "css": "styles.css",
56
+ "java": "Main.java",
57
+ "go": "main.go",
58
+ "rust": "main.rs",
59
+ "php": "index.php",
60
+ "ruby": "main.rb",
61
+ "csharp": "Program.cs",
62
+ "swift": "main.swift",
63
+ "kotlin": "Main.kt",
64
+ }
65
+ filename = ext_map.get(lang or "", "code.txt")
66
+ files[filename] = code
67
+
68
+ return files
69
+
70
+
71
+ def normalize_language(target_language: str | None, fence_lang: str | None) -> str:
72
+ """Normalize language name to a canonical form."""
73
+ lang = (fence_lang or target_language or "python").lower()
74
+ if lang in {"python", "py"}:
75
+ return "python"
76
+ if lang in {"html", "web", "css"}:
77
+ return "web"
78
+ if lang in {"javascript", "js"}:
79
+ return "javascript"
80
+ if lang in {"typescript", "ts"}:
81
+ return "typescript"
82
+ if lang == "java":
83
+ return "java"
84
+ if lang == "go":
85
+ return "go"
86
+ if lang == "rust":
87
+ return "rust"
88
+ if lang == "php":
89
+ return "php"
90
+ if lang == "ruby":
91
+ return "ruby"
92
+ if lang in {"csharp", "c#"}:
93
+ return "csharp"
94
+ if lang == "swift":
95
+ return "swift"
96
+ if lang == "kotlin":
97
+ return "kotlin"
98
+ return lang
99
+
100
+
101
+ def is_gradio_code(code: str) -> bool:
102
+ """Detect if Python code is a Gradio app."""
103
+ return bool(
104
+ re.search(
105
+ r"import\s+gradio|from\s+gradio\s+import|gr\.\s*(Interface|Blocks|TabbedInterface|ChatInterface|App)",
106
+ code,
107
+ )
108
+ )
109
+
110
+
111
+ # ─── Web Document / Iframe Builder ─────────────────────────────────────
112
+
113
+ def _web_document(code: str, fence_lang: str | None) -> str:
114
+ """Wrap code in an HTML document if needed."""
115
+ lang = (fence_lang or "").lower()
116
+ if lang in {"javascript", "js"}:
117
+ return f"<!doctype html><html><body><script>\n{code}\n</script></body></html>"
118
+ if lang == "css":
119
+ return f"<!doctype html><html><head><style>\n{code}\n</style></head><body></body></html>"
120
+ if re.search(r"<!doctype|<html[\s>]", code, flags=re.IGNORECASE):
121
+ return code
122
+ return f"<!doctype html><html><head><meta charset='utf-8'></head><body>\n{code}\n</body></html>"
123
+
124
+
125
+ def build_iframe(code: str, fence_lang: str | None = None) -> str:
126
+ """Build a sandboxed iframe HTML string for web preview."""
127
+ document = _web_document(code, fence_lang)
128
+ srcdoc = html.escape(document, quote=True)
129
+ return (
130
+ '<iframe class="web-frame" '
131
+ 'sandbox="allow-scripts" '
132
+ 'allow="fullscreen" '
133
+ "allowfullscreen "
134
+ f'srcdoc="{srcdoc}" '
135
+ 'style="width:100%; min-height:680px; border:0; border-radius:14px; '
136
+ 'background:white;"></iframe>'
137
+ )
code/execution/gradio_runner.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio app runner β€” launches Gradio apps as subprocess servers.
2
+
3
+ Manages the lifecycle of Gradio app processes: start, status check, and stop.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import logging
9
+ import os
10
+ import re
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ # ─── Registry for running Gradio subprocesses ───────────────────────────
20
+
21
+ _running_gradio_procs: dict[str, subprocess.Popen] = {}
22
+
23
+
24
+ def run_gradio_app(code: str, port: int = 7861) -> dict[str, Any]:
25
+ """Launch a Gradio app as a subprocess and return its URL.
26
+
27
+ The Gradio app is run on the specified port. We modify the code
28
+ to ensure it launches on the correct port and is accessible.
29
+ """
30
+ # Kill any previously running Gradio app
31
+ _stop_all_procs()
32
+
33
+ # Patch the code: ensure launch uses correct server_name and server_port
34
+ patched_code = code
35
+
36
+ # Replace .launch() with correct params
37
+ patched_code = re.sub(
38
+ r"(\w+)\.launch\([^)]*\)",
39
+ f'\\1.launch(server_name="0.0.0.0", server_port={port}, share=False)',
40
+ patched_code,
41
+ )
42
+
43
+ # If no .launch() found, add one
44
+ if ".launch(" not in patched_code:
45
+ patched_code += (
46
+ f'\n\nif __name__ == "__main__":\n'
47
+ f' iface.launch(server_name="0.0.0.0", server_port={port}, share=False)\n'
48
+ )
49
+
50
+ with tempfile.TemporaryDirectory(prefix="gradio_app_") as tmp:
51
+ app_path = Path(tmp) / "gradio_app.py"
52
+ app_path.write_text(patched_code, encoding="utf-8")
53
+
54
+ env = {
55
+ **os.environ,
56
+ "PYTHONUNBUFFERED": "1",
57
+ "GRADIO_SERVER_NAME": "0.0.0.0",
58
+ "GRADIO_SERVER_PORT": str(port),
59
+ }
60
+
61
+ try:
62
+ proc = subprocess.Popen(
63
+ [sys.executable, str(app_path)],
64
+ cwd=tmp,
65
+ env=env,
66
+ stdout=subprocess.PIPE,
67
+ stderr=subprocess.PIPE,
68
+ text=True,
69
+ )
70
+
71
+ proc_id = f"gradio_{port}"
72
+ _running_gradio_procs[proc_id] = proc
73
+
74
+ # Wait a bit for the server to start
75
+ import time as _time
76
+ _time.sleep(3)
77
+
78
+ # Check if process is still running
79
+ poll = proc.poll()
80
+ if poll is not None:
81
+ stdout = proc.stdout.read() if proc.stdout else ""
82
+ stderr = proc.stderr.read() if proc.stderr else ""
83
+ return {
84
+ "success": False,
85
+ "url": "",
86
+ "message": f"Gradio app exited with code {poll}",
87
+ "stdout": stdout[-2000:] if stdout else "",
88
+ "stderr": stderr[-2000:] if stderr else "",
89
+ }
90
+
91
+ gradio_url = f"http://localhost:{port}"
92
+ return {
93
+ "success": True,
94
+ "url": gradio_url,
95
+ "message": f"Gradio app running at {gradio_url}",
96
+ "port": port,
97
+ }
98
+
99
+ except Exception as exc:
100
+ logger.exception("Failed to launch Gradio app")
101
+ return {
102
+ "success": False,
103
+ "url": "",
104
+ "message": f"Failed to launch: {exc}",
105
+ }
106
+
107
+
108
+ def stop_gradio_app() -> dict[str, Any]:
109
+ """Stop any running Gradio app subprocess."""
110
+ stopped = _stop_all_procs()
111
+ return {"success": True, "message": f"Stopped {stopped} Gradio app(s)"}
112
+
113
+
114
+ def _stop_all_procs() -> int:
115
+ """Stop all running Gradio processes. Returns count of stopped procs."""
116
+ stopped = 0
117
+ for pid, proc in list(_running_gradio_procs.items()):
118
+ try:
119
+ proc.terminate()
120
+ proc.wait(timeout=3)
121
+ stopped += 1
122
+ except Exception:
123
+ try:
124
+ proc.kill()
125
+ stopped += 1
126
+ except Exception:
127
+ pass
128
+ _running_gradio_procs.clear()
129
+ return stopped
code/execution/python_runner.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sandboxed Python code execution.
2
+
3
+ Runs user Python code in a subprocess with resource limits,
4
+ captures stdout/stderr, and saves matplotlib figures.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ import subprocess
11
+ import sys
12
+ import tempfile
13
+ import textwrap
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ from code.config.constants import (
18
+ MAX_STDIO_CHARS,
19
+ OUTPUT_PNG,
20
+ PY_MEM_LIMIT_MB,
21
+ PY_TIMEOUT_S,
22
+ )
23
+
24
+
25
+ @dataclass
26
+ class PythonExecutionResult:
27
+ """Result of a sandboxed Python execution."""
28
+ stdout: str
29
+ stderr: str
30
+ image_path: str | None
31
+ returncode: int | None
32
+ timed_out: bool = False
33
+
34
+
35
+ def _apply_subprocess_limits() -> None:
36
+ """Set resource limits for the subprocess (Linux only)."""
37
+ import resource
38
+ mem_bytes = PY_MEM_LIMIT_MB * 1024 * 1024
39
+ resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
40
+ resource.setrlimit(resource.RLIMIT_CPU, (PY_TIMEOUT_S, PY_TIMEOUT_S))
41
+
42
+
43
+ def _python_runner_source() -> str:
44
+ """Return the source code of the runner script that wraps user code."""
45
+ return textwrap.dedent(
46
+ f"""
47
+ import os
48
+ import runpy
49
+ import sys
50
+ import traceback
51
+
52
+ os.environ.setdefault("MPLBACKEND", "Agg")
53
+ exit_code = 0
54
+ try:
55
+ runpy.run_path(os.path.join(os.getcwd(), "user_code.py"), run_name="__main__")
56
+ except SystemExit as exc:
57
+ code = exc.code
58
+ exit_code = code if isinstance(code, int) else 1
59
+ except Exception:
60
+ traceback.print_exc()
61
+ exit_code = 1
62
+ finally:
63
+ try:
64
+ import matplotlib
65
+ matplotlib.use("Agg", force=True)
66
+ import matplotlib.pyplot as plt
67
+ if plt.get_fignums():
68
+ plt.savefig(os.environ["OUTPUT_PNG"], bbox_inches="tight")
69
+ except ModuleNotFoundError as exc:
70
+ if exc.name != "matplotlib":
71
+ traceback.print_exc()
72
+ except Exception:
73
+ traceback.print_exc()
74
+
75
+ raise SystemExit(exit_code)
76
+ """
77
+ ).strip()
78
+
79
+
80
+ def _truncate_output(text: str) -> str:
81
+ """Truncate output to MAX_STDIO_CHARS with a note."""
82
+ if len(text) <= MAX_STDIO_CHARS:
83
+ return text
84
+ remaining = len(text) - MAX_STDIO_CHARS
85
+ return text[:MAX_STDIO_CHARS] + f"\n\n... truncated {remaining} characters ..."
86
+
87
+
88
+ def _decode_timeout_output(value: str | bytes | None) -> str:
89
+ """Safely decode subprocess output from timeout exceptions."""
90
+ if value is None:
91
+ return ""
92
+ if isinstance(value, bytes):
93
+ return value.decode("utf-8", errors="replace")
94
+ return value
95
+
96
+
97
+ def run_python(code: str) -> PythonExecutionResult:
98
+ """Execute Python code in a sandboxed subprocess.
99
+
100
+ Returns a PythonExecutionResult with stdout, stderr, image path, and status.
101
+ """
102
+ with tempfile.TemporaryDirectory(prefix="fullstack_run_") as tmp:
103
+ workdir = Path(tmp)
104
+ runner_path = workdir / "runner.py"
105
+ user_path = workdir / "user_code.py"
106
+ image_path = workdir / OUTPUT_PNG
107
+
108
+ runner_path.write_text(_python_runner_source(), encoding="utf-8")
109
+ user_path.write_text(code, encoding="utf-8")
110
+
111
+ env = {
112
+ "PATH": "/usr/bin:/bin",
113
+ "HOME": str(workdir),
114
+ "TMPDIR": str(workdir),
115
+ "MPLBACKEND": "Agg",
116
+ "MPLCONFIGDIR": str(workdir / ".matplotlib"),
117
+ "OUTPUT_PNG": str(image_path),
118
+ "PYTHONIOENCODING": "utf-8",
119
+ "PYTHONNOUSERSITE": "1",
120
+ "PYTHONUNBUFFERED": "1",
121
+ "LANG": "C.UTF-8",
122
+ "OPENBLAS_NUM_THREADS": "1",
123
+ "OMP_NUM_THREADS": "1",
124
+ "MKL_NUM_THREADS": "1",
125
+ "NUMEXPR_NUM_THREADS": "1",
126
+ }
127
+
128
+ try:
129
+ completed = subprocess.run(
130
+ [sys.executable, "-I", str(runner_path)],
131
+ cwd=workdir,
132
+ env=env,
133
+ capture_output=True,
134
+ text=True,
135
+ encoding="utf-8",
136
+ errors="replace",
137
+ timeout=PY_TIMEOUT_S,
138
+ preexec_fn=_apply_subprocess_limits if sys.platform == "linux" else None,
139
+ check=False,
140
+ )
141
+ stdout = _truncate_output(completed.stdout)
142
+ stderr = _truncate_output(completed.stderr)
143
+
144
+ if completed.returncode and not stderr:
145
+ stderr = f"Process exited with status {completed.returncode}."
146
+
147
+ saved_image: str | None = None
148
+ if image_path.exists() and image_path.stat().st_size > 0:
149
+ saved = tempfile.NamedTemporaryFile(
150
+ prefix="fullstack_plot_", suffix=".png", delete=False
151
+ )
152
+ saved.close()
153
+ Path(saved.name).write_bytes(image_path.read_bytes())
154
+ saved_image = saved.name
155
+
156
+ return PythonExecutionResult(
157
+ stdout=stdout,
158
+ stderr=stderr,
159
+ image_path=saved_image,
160
+ returncode=completed.returncode,
161
+ )
162
+ except subprocess.TimeoutExpired as exc:
163
+ stdout = _truncate_output(_decode_timeout_output(exc.stdout))
164
+ stderr = _truncate_output(_decode_timeout_output(exc.stderr))
165
+ timeout_note = f"Timed out after {PY_TIMEOUT_S} seconds; the process was killed."
166
+ stderr = f"{stderr}\n{timeout_note}".strip()
167
+ return PythonExecutionResult(
168
+ stdout=stdout,
169
+ stderr=stderr,
170
+ image_path=None,
171
+ returncode=None,
172
+ timed_out=True,
173
+ )
code/huggingface/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """HuggingFace Hub push."""
code/huggingface/dockerfile_gen.py ADDED
@@ -0,0 +1,626 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Dockerfile and package.json generator for JS/TS frameworks.
2
+
3
+ Auto-generates Dockerfile, package.json, and .dockerignore for
4
+ React, Next.js, Vue.js, Express, NestJS, and plain Node.js projects
5
+ so they can be pushed to HuggingFace Docker Spaces.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from typing import Any
12
+
13
+ # ─── Framework Detection ────────────────────────────────────────────────
14
+
15
+ # Keywords in code that identify a framework
16
+ FRAMEWORK_SIGNALS: dict[str, list[str]] = {
17
+ "nextjs": [
18
+ "from 'next", 'from "next',
19
+ "next/link", "next/image", "next/router", "next/head",
20
+ "NextResponse", "NextRequest", "next/navigation",
21
+ "getServerSideProps", "getStaticProps",
22
+ ],
23
+ "react": [
24
+ "from 'react", 'from "react',
25
+ "react-dom", "ReactDOM", "useState", "useEffect",
26
+ "jsx", "tsx", "React.Component", "React.createElement",
27
+ ],
28
+ "vue": [
29
+ "from 'vue", 'from "vue',
30
+ "createApp", "Vue.createApp", "<template>",
31
+ "defineComponent", "ref(", "reactive(",
32
+ ],
33
+ "express": [
34
+ "require('express')", 'require("express")',
35
+ "from 'express", 'from "express',
36
+ "express()", "express.Router",
37
+ ],
38
+ "nestjs": [
39
+ "@Module", "@Controller", "@Get", "@Post", "@Put", "@Delete",
40
+ "from '@nestjs", 'from "@nestjs',
41
+ "NestFactory.create",
42
+ ],
43
+ "nodejs": [
44
+ "require('http')", "http.createServer",
45
+ "const http = require", "import http from",
46
+ ],
47
+ }
48
+
49
+
50
+ def detect_framework(files: dict[str, str]) -> str:
51
+ """Detect the JS/TS framework from project files.
52
+
53
+ Returns one of: 'nextjs', 'react', 'vue', 'express', 'nestjs', 'nodejs', 'static'
54
+ """
55
+ all_code = "\n".join(files.values())
56
+
57
+ # Check file names first (strong signal)
58
+ has_next_config = any(
59
+ f.startswith("next.config") for f in files
60
+ )
61
+ if has_next_config:
62
+ return "nextjs"
63
+
64
+ # Check package.json if present
65
+ for fname, content in files.items():
66
+ if fname == "package.json" or fname.endswith("/package.json"):
67
+ try:
68
+ import json
69
+ pkg = json.loads(content)
70
+ deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
71
+ if "next" in deps:
72
+ return "nextjs"
73
+ if "vue" in deps or "@vue/cli-service" in deps:
74
+ return "vue"
75
+ if "@nestjs/core" in deps:
76
+ return "nestjs"
77
+ if "express" in deps:
78
+ return "express"
79
+ if "react" in deps or "react-dom" in deps:
80
+ return "react"
81
+ except Exception:
82
+ pass
83
+
84
+ # Check code content for framework signals
85
+ for fw, signals in FRAMEWORK_SIGNALS.items():
86
+ for signal in signals:
87
+ if signal in all_code:
88
+ return fw
89
+
90
+ # Check file extensions
91
+ has_jsx_tsx = any(f.endswith((".jsx", ".tsx")) for f in files)
92
+ has_vue = any(f.endswith(".vue") for f in files)
93
+
94
+ if has_vue:
95
+ return "vue"
96
+ if has_jsx_tsx:
97
+ return "react"
98
+
99
+ return "static"
100
+
101
+
102
+ def is_js_project(files: dict[str, str]) -> bool:
103
+ """Check if the project is a JavaScript/TypeScript project."""
104
+ js_extensions = {".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs", ".cjs"}
105
+ has_package_json = any("package.json" in f for f in files)
106
+ has_js_files = any(
107
+ any(f.endswith(ext) for ext in js_extensions)
108
+ for f in files
109
+ )
110
+ return has_package_json or has_js_files
111
+
112
+
113
+ # ─── Dockerfile Templates ───────────────────────────────────────────────
114
+
115
+ def _dockerfile_nextjs() -> str:
116
+ """Dockerfile for Next.js projects."""
117
+ return """FROM node:20-slim AS base
118
+
119
+ # Install dependencies only when needed
120
+ FROM base AS deps
121
+ WORKDIR /app
122
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
123
+ RUN \\
124
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
125
+ elif [ -f package-lock.json ]; then npm ci; \\
126
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\
127
+ else npm i; \\
128
+ fi
129
+
130
+ # Rebuild source code only when needed
131
+ FROM base AS builder
132
+ WORKDIR /app
133
+ COPY --from=deps /app/node_modules ./node_modules
134
+ COPY . .
135
+ RUN npm run build
136
+
137
+ # Production image
138
+ FROM base AS runner
139
+ WORKDIR /app
140
+ ENV NODE_ENV=production
141
+ RUN addgroup --system --gid 1001 nodejs
142
+ RUN adduser --system --uid 1001 nextjs
143
+ COPY --from=builder /app/public ./public
144
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
145
+ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
146
+ USER nextjs
147
+ EXPOSE 3000
148
+ ENV PORT=3000
149
+ ENV HOSTNAME="0.0.0.0"
150
+ CMD ["node", "server.js"]
151
+ """
152
+
153
+
154
+ def _dockerfile_react() -> str:
155
+ """Dockerfile for React (Vite/CRA) projects β€” served with nginx."""
156
+ return """FROM node:20-slim AS build
157
+ WORKDIR /app
158
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
159
+ RUN \\
160
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
161
+ elif [ -f package-lock.json ]; then npm ci; \\
162
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\
163
+ else npm i; \\
164
+ fi
165
+ COPY . .
166
+ RUN npm run build
167
+
168
+ FROM nginx:alpine
169
+ COPY --from=build /app/build /usr/share/nginx/html
170
+ COPY --from=build /app/dist /usr/share/nginx/html 2>/dev/null || true
171
+ RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
172
+ server {
173
+ listen 7860;
174
+ server_name localhost;
175
+ root /usr/share/nginx/html;
176
+ index index.html;
177
+ location / {
178
+ try_files $uri $uri/ /index.html;
179
+ }
180
+ }
181
+ EOF
182
+ EXPOSE 7860
183
+ CMD ["nginx", "-g", "daemon off;"]
184
+ """
185
+
186
+
187
+ def _dockerfile_vue() -> str:
188
+ """Dockerfile for Vue.js projects β€” served with nginx."""
189
+ return """FROM node:20-slim AS build
190
+ WORKDIR /app
191
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
192
+ RUN \\
193
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
194
+ elif [ -f package-lock.json ]; then npm ci; \\
195
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\
196
+ else npm i; \\
197
+ fi
198
+ COPY . .
199
+ RUN npm run build
200
+
201
+ FROM nginx:alpine
202
+ COPY --from=build /app/dist /usr/share/nginx/html
203
+ RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
204
+ server {
205
+ listen 7860;
206
+ server_name localhost;
207
+ root /usr/share/nginx/html;
208
+ index index.html;
209
+ location / {
210
+ try_files $uri $uri/ /index.html;
211
+ }
212
+ }
213
+ EOF
214
+ EXPOSE 7860
215
+ CMD ["nginx", "-g", "daemon off;"]
216
+ """
217
+
218
+
219
+ def _dockerfile_express() -> str:
220
+ """Dockerfile for Express/Node.js server projects."""
221
+ return """FROM node:20-slim
222
+ WORKDIR /app
223
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
224
+ RUN \\
225
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
226
+ elif [ -f package-lock.json ]; then npm ci; \\
227
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\
228
+ else npm i; \\
229
+ fi
230
+ COPY . .
231
+ RUN addgroup --system --gid 1001 appuser && adduser --system --uid 1001 appuser
232
+ USER appuser
233
+ EXPOSE 7860
234
+ ENV PORT=7860
235
+ ENV HOST=0.0.0.0
236
+ CMD ["node", "index.js"]
237
+ """
238
+
239
+
240
+ def _dockerfile_nestjs() -> str:
241
+ """Dockerfile for NestJS projects."""
242
+ return """FROM node:20-slim AS build
243
+ WORKDIR /app
244
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
245
+ RUN \\
246
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \\
247
+ elif [ -f package-lock.json ]; then npm ci; \\
248
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \\
249
+ else npm i; \\
250
+ fi
251
+ COPY . .
252
+ RUN npm run build
253
+
254
+ FROM node:20-slim
255
+ WORKDIR /app
256
+ COPY package.json package-lock.json* yarn.lock* pnpm-lock.yaml* ./
257
+ RUN \\
258
+ if [ -f yarn.lock ]; then yarn --frozen-lockfile --production; \\
259
+ elif [ -f package-lock.json ]; then npm ci --only=production; \\
260
+ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile --prod; \\
261
+ else npm i --only=production; \\
262
+ fi
263
+ COPY --from=build /app/dist ./dist
264
+ RUN addgroup --system --gid 1001 appuser && adduser --system --uid 1001 appuser
265
+ USER appuser
266
+ EXPOSE 7860
267
+ ENV PORT=7860
268
+ CMD ["node", "dist/main.js"]
269
+ """
270
+
271
+
272
+ def generate_dockerfile(framework: str) -> str:
273
+ """Generate a Dockerfile for the given framework.
274
+
275
+ Args:
276
+ framework: One of 'nextjs', 'react', 'vue', 'express', 'nestjs', 'nodejs', 'static'
277
+
278
+ Returns:
279
+ Dockerfile content as string.
280
+ """
281
+ templates = {
282
+ "nextjs": _dockerfile_nextjs,
283
+ "react": _dockerfile_react,
284
+ "vue": _dockerfile_vue,
285
+ "express": _dockerfile_express,
286
+ "nestjs": _dockerfile_nestjs,
287
+ "nodejs": _dockerfile_express, # Same as express for plain Node
288
+ }
289
+ gen = templates.get(framework)
290
+ if gen:
291
+ return gen()
292
+ # Fallback: generic node server
293
+ return _dockerfile_express()
294
+
295
+
296
+ # ─── package.json Generator ─────────────────────────────────────────────
297
+
298
+ def _scan_js_imports(code: str) -> set[str]:
299
+ """Scan JS/TS code for import/require statements and return package names."""
300
+ packages = set()
301
+
302
+ # ESM: import xxx from 'pkg' / import 'pkg'
303
+ for m in re.finditer(r"import\s+.*?\s+from\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)", code):
304
+ packages.add(m.group(1))
305
+
306
+ # ESM: import 'pkg'
307
+ for m in re.finditer(r"import\s+['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]", code):
308
+ packages.add(m.group(1))
309
+
310
+ # CJS: require('pkg')
311
+ for m in re.finditer(r"require\s*\(\s*['\"](@?[\w-]+/[\w-]+|[\w-]+)['\"]\s*\)", code):
312
+ packages.add(m.group(1))
313
+
314
+ return packages
315
+
316
+
317
+ # Known packages that should NOT go in dependencies (built-in or types)
318
+ _SKIP_PACKAGES = {
319
+ "react", "react-dom", "next", "vue", "express",
320
+ "path", "fs", "http", "https", "url", "os", "crypto",
321
+ "stream", "util", "events", "buffer", "child_process",
322
+ "net", "tls", "zlib", "assert", "querystring",
323
+ }
324
+
325
+ # Framework-specific dependency sets
326
+ _FRAMEWORK_DEPS: dict[str, dict[str, str]] = {
327
+ "nextjs": {
328
+ "next": "14.2.0",
329
+ "react": "^18.3.0",
330
+ "react-dom": "^18.3.0",
331
+ },
332
+ "react": {
333
+ "react": "^18.3.0",
334
+ "react-dom": "^18.3.0",
335
+ "react-scripts": "5.0.1",
336
+ },
337
+ "vue": {
338
+ "vue": "^3.4.0",
339
+ },
340
+ "express": {
341
+ "express": "^4.19.0",
342
+ },
343
+ "nestjs": {
344
+ "@nestjs/core": "^10.3.0",
345
+ "@nestjs/common": "^10.3.0",
346
+ "@nestjs/platform-express": "^10.3.0",
347
+ "reflect-metadata": "^0.2.0",
348
+ "rxjs": "^7.8.0",
349
+ },
350
+ "nodejs": {},
351
+ }
352
+
353
+ # Common package version mapping
354
+ _PACKAGE_VERSIONS: dict[str, str] = {
355
+ "axios": "^1.6.0",
356
+ "lodash": "^4.17.21",
357
+ "cors": "^2.8.5",
358
+ "dotenv": "^16.4.0",
359
+ "mongoose": "^8.2.0",
360
+ "prisma": "^5.10.0",
361
+ "@prisma/client": "^5.10.0",
362
+ "zod": "^3.22.0",
363
+ "socket.io": "^4.7.0",
364
+ "multer": "^1.4.4",
365
+ "cookie-parser": "^1.4.6",
366
+ "express-session": "^1.18.0",
367
+ "jsonwebtoken": "^9.0.0",
368
+ "bcrypt": "^5.1.0",
369
+ "uuid": "^9.0.0",
370
+ "dayjs": "^1.11.10",
371
+ "chart.js": "^4.4.0",
372
+ "framer-motion": "^11.0.0",
373
+ "lucide-react": "^0.350.0",
374
+ "tailwindcss": "^3.4.0",
375
+ "postcss": "^8.4.0",
376
+ "autoprefixer": "^10.4.0",
377
+ "@vitejs/plugin-react": "^4.2.0",
378
+ "vite": "^5.1.0",
379
+ "typescript": "^5.3.0",
380
+ "@types/react": "^18.3.0",
381
+ "@types/react-dom": "^18.3.0",
382
+ "@types/node": "^20.11.0",
383
+ "tailwind-merge": "^2.2.0",
384
+ "clsx": "^2.1.0",
385
+ "class-variance-authority": "^0.7.0",
386
+ "@radix-ui/react-slot": "^1.0.2",
387
+ "next-themes": "^0.3.0",
388
+ "recharts": "^2.12.0",
389
+ "react-hook-form": "^7.50.0",
390
+ "@hookform/resolvers": "^3.3.0",
391
+ "zustand": "^4.5.0",
392
+ "jotai": "^2.6.0",
393
+ "tanstack": "^5.24.0",
394
+ "@tanstack/react-query": "^5.24.0",
395
+ "swr": "^2.2.0",
396
+ "nodemon": "^3.1.0",
397
+ "ts-node": "^10.9.0",
398
+ "ts-node-dev": "^2.0.0",
399
+ }
400
+
401
+
402
+ def generate_package_json(
403
+ framework: str,
404
+ project_name: str = "my-app",
405
+ extra_deps: set[str] | None = None,
406
+ existing_content: str | None = None,
407
+ ) -> str:
408
+ """Generate a package.json for the given framework.
409
+
410
+ If existing_content is provided, merges dependencies into it.
411
+ """
412
+ import json
413
+
414
+ # Start with existing or fresh
415
+ if existing_content:
416
+ try:
417
+ pkg = json.loads(existing_content)
418
+ except Exception:
419
+ pkg = {}
420
+ else:
421
+ pkg = {}
422
+
423
+ pkg.setdefault("name", project_name)
424
+ pkg.setdefault("version", "1.0.0")
425
+ pkg.setdefault("private", True)
426
+
427
+ deps = pkg.get("dependencies", {})
428
+ dev_deps = pkg.get("devDependencies", {})
429
+
430
+ # Add framework core deps
431
+ fw_deps = _FRAMEWORK_DEPS.get(framework, {})
432
+ for name, version in fw_deps.items():
433
+ deps[name] = version
434
+
435
+ # Add scanned extra deps
436
+ if extra_deps:
437
+ for dep in extra_deps:
438
+ if dep in _SKIP_PACKAGES:
439
+ continue
440
+ if dep in deps or dep in dev_deps:
441
+ continue
442
+ version = _PACKAGE_VERSIONS.get(dep, "^1.0.0")
443
+ # Dev deps
444
+ if dep.startswith("@types/") or dep in {"typescript", "nodemon", "ts-node", "ts-node-dev"}:
445
+ dev_deps[dep] = version
446
+ else:
447
+ deps[dep] = version
448
+
449
+ pkg["dependencies"] = deps
450
+ if dev_deps:
451
+ pkg["devDependencies"] = dev_deps
452
+
453
+ # Add scripts based on framework
454
+ scripts = pkg.get("scripts", {})
455
+ if framework == "nextjs":
456
+ scripts.setdefault("dev", "next dev")
457
+ scripts.setdefault("build", "next build")
458
+ scripts.setdefault("start", "next start -p 7860")
459
+ elif framework in ("react",):
460
+ scripts.setdefault("dev", "vite")
461
+ scripts.setdefault("build", "vite build")
462
+ scripts.setdefault("start", "vite preview --port 7860 --host 0.0.0.0")
463
+ # Ensure vite is in devDeps
464
+ if "vite" not in dev_deps and "vite" not in deps:
465
+ dev_deps["vite"] = "^5.1.0"
466
+ if "@vitejs/plugin-react" not in dev_deps:
467
+ dev_deps["@vitejs/plugin-react"] = "^4.2.0"
468
+ elif framework == "vue":
469
+ scripts.setdefault("dev", "vite")
470
+ scripts.setdefault("build", "vite build")
471
+ scripts.setdefault("start", "vite preview --port 7860 --host 0.0.0.0")
472
+ if "vite" not in dev_deps and "vite" not in deps:
473
+ dev_deps["vite"] = "^5.1.0"
474
+ elif framework in ("express", "nodejs"):
475
+ scripts.setdefault("dev", "node index.js")
476
+ scripts.setdefault("start", "node index.js")
477
+ elif framework == "nestjs":
478
+ scripts.setdefault("build", "nest build")
479
+ scripts.setdefault("start", "node dist/main.js")
480
+
481
+ pkg["scripts"] = scripts
482
+ pkg["dependencies"] = deps
483
+ if dev_deps:
484
+ pkg["devDependencies"] = dev_deps
485
+
486
+ return json.dumps(pkg, indent=2) + "\n"
487
+
488
+
489
+ # ─── .dockerignore ──────────────────────────────────────────────────────
490
+
491
+ DOCKERIGNORE = """node_modules
492
+ npm-debug.log*
493
+ yarn-debug.log*
494
+ yarn-error.log*
495
+ .next
496
+ .git
497
+ .gitignore
498
+ README.md
499
+ .env
500
+ .env.local
501
+ .env.production
502
+ .DS_Store
503
+ """
504
+
505
+
506
+ # ─── Vite Config Generators ─────────────────────────────────────────────
507
+
508
+ def generate_vite_config(framework: str) -> str | None:
509
+ """Generate a vite.config.js/ts if needed for React or Vue."""
510
+ if framework == "react":
511
+ return """import { defineConfig } from 'vite'
512
+ import react from '@vitejs/plugin-react'
513
+
514
+ export default defineConfig({
515
+ plugins: [react()],
516
+ server: {
517
+ host: '0.0.0.0',
518
+ port: 7860,
519
+ },
520
+ })
521
+ """
522
+ if framework == "vue":
523
+ return """import { defineConfig } from 'vite'
524
+ import vue from '@vitejs/plugin-vue'
525
+
526
+ export default defineConfig({
527
+ plugins: [vue()],
528
+ server: {
529
+ host: '0.0.0.0',
530
+ port: 7860,
531
+ },
532
+ })
533
+ """
534
+ return None
535
+
536
+
537
+ # ─── Next.js Config ────────────────────────────────────────────────────
538
+
539
+ def generate_next_config() -> str:
540
+ """Generate next.config.js with standalone output for Docker."""
541
+ return """/** @type {import('next').NextConfig} */
542
+ const nextConfig = {
543
+ output: 'standalone',
544
+ }
545
+
546
+ module.exports = nextConfig
547
+ """
548
+
549
+
550
+ # ─── Public index.html for Vite ─────────────────────────────────────────
551
+
552
+ def generate_index_html(title: str = "App") -> str:
553
+ """Generate a minimal index.html for Vite projects."""
554
+ return f"""<!DOCTYPE html>
555
+ <html lang="en">
556
+ <head>
557
+ <meta charset="UTF-8" />
558
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
559
+ <title>{title}</title>
560
+ </head>
561
+ <body>
562
+ <div id="root"></div>
563
+ <script type="module" src="/src/main.jsx"></script>
564
+ </body>
565
+ </html>
566
+ """
567
+
568
+
569
+ # ─── Full Project Scaffold ──────────────────────────────────────────────
570
+
571
+ def scaffold_js_project(
572
+ files: dict[str, str],
573
+ framework: str,
574
+ project_name: str = "my-app",
575
+ ) -> dict[str, str]:
576
+ """Add Dockerfile, package.json, and config files to a JS project.
577
+
578
+ Takes existing generated files and returns an augmented dict with
579
+ Docker support files added.
580
+ """
581
+ augmented = dict(files)
582
+
583
+ # Detect imports from all JS/TS files
584
+ all_js_code = "\n".join(
585
+ content for fname, content in files.items()
586
+ if fname.endswith((".js", ".jsx", ".ts", ".tsx", ".vue", ".mjs"))
587
+ )
588
+ extra_deps = _scan_js_imports(all_js_code)
589
+
590
+ # Add Dockerfile
591
+ if "Dockerfile" not in augmented:
592
+ augmented["Dockerfile"] = generate_dockerfile(framework)
593
+
594
+ # Add .dockerignore
595
+ if ".dockerignore" not in augmented:
596
+ augmented[".dockerignore"] = DOCKERIGNORE
597
+
598
+ # Add or merge package.json
599
+ if "package.json" in augmented:
600
+ augmented["package.json"] = generate_package_json(
601
+ framework=framework,
602
+ project_name=project_name,
603
+ extra_deps=extra_deps,
604
+ existing_content=augmented["package.json"],
605
+ )
606
+ else:
607
+ augmented["package.json"] = generate_package_json(
608
+ framework=framework,
609
+ project_name=project_name,
610
+ extra_deps=extra_deps,
611
+ )
612
+
613
+ # Framework-specific config files
614
+ if framework == "nextjs" and "next.config.js" not in augmented and "next.config.mjs" not in augmented:
615
+ augmented["next.config.js"] = generate_next_config()
616
+
617
+ if framework in ("react", "vue"):
618
+ vite_cfg = generate_vite_config(framework)
619
+ if vite_cfg and "vite.config.js" not in augmented and "vite.config.ts" not in augmented:
620
+ augmented["vite.config.js"] = vite_cfg
621
+
622
+ # Add index.html entry point for Vite if not present
623
+ if "index.html" not in augmented:
624
+ augmented["index.html"] = generate_index_html(project_name)
625
+
626
+ return augmented
code/huggingface/push.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """HuggingFace Hub push and project ZIP packaging.
2
+
3
+ Creates ZIP archives from extracted project files and pushes
4
+ projects to HuggingFace Spaces or model repos.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ import os
11
+ import re
12
+ import tempfile
13
+ import zipfile
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from code.config.constants import MODEL_ID
18
+ from code.huggingface.dockerfile_gen import (
19
+ detect_framework,
20
+ is_js_project,
21
+ scaffold_js_project,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ # ─── Import-to-Package Mapping ──────────────────────────────────────────
27
+
28
+ IMPORT_TO_PACKAGE: dict[str, str] = {
29
+ "gradio": "gradio>=4.0.0",
30
+ "flask": "flask>=3.0.0",
31
+ "django": "django>=4.2.0",
32
+ "fastapi": "fastapi>=0.100.0",
33
+ "uvicorn": "uvicorn>=0.23.0",
34
+ "streamlit": "streamlit>=1.28.0",
35
+ "matplotlib": "matplotlib>=3.8.0",
36
+ "PIL": "Pillow>=10.0.0",
37
+ "Pillow": "Pillow>=10.0.0",
38
+ "numpy": "numpy>=1.24.0",
39
+ "pandas": "pandas>=2.0.0",
40
+ "scipy": "scipy>=1.11.0",
41
+ "sklearn": "scikit-learn>=1.3.0",
42
+ "scikit_learn": "scikit-learn>=1.3.0",
43
+ "torch": "torch>=2.1.0",
44
+ "tensorflow": "tensorflow>=2.14.0",
45
+ "transformers": "transformers>=4.35.0",
46
+ "requests": "requests>=2.31.0",
47
+ "beautifulsoup4": "beautifulsoup4>=4.12.0",
48
+ "bs4": "beautifulsoup4>=4.12.0",
49
+ "selenium": "selenium>=4.15.0",
50
+ "sqlalchemy": "sqlalchemy>=2.0.0",
51
+ "pydantic": "pydantic>=2.0.0",
52
+ "httpx": "httpx>=0.25.0",
53
+ "aiohttp": "aiohttp>=3.9.0",
54
+ "opencv": "opencv-python-headless>=4.8.0",
55
+ "cv2": "opencv-python-headless>=4.8.0",
56
+ "plotly": "plotly>=5.18.0",
57
+ "seaborn": "seaborn>=0.13.0",
58
+ "wordcloud": "wordcloud>=1.9.0",
59
+ "networkx": "networkx>=3.2.0",
60
+ "sympy": "sympy>=1.12",
61
+ "Pillow": "Pillow>=10.0.0",
62
+ "skimage": "scikit-image>=0.21.0",
63
+ "soundfile": "soundfile>=0.12.0",
64
+ "pydub": "pydub>=0.25.1",
65
+ "moviepy": "moviepy>=1.0.3",
66
+ "openpyxl": "openpyxl>=3.1.0",
67
+ "xlsxwriter": "xlsxwriter>=3.1.0",
68
+ "python-docx": "python-docx>=0.8.11",
69
+ "docx": "python-docx>=0.8.11",
70
+ "reportlab": "reportlab>=4.0.0",
71
+ "jinja2": "jinja2>=3.1.0",
72
+ "wtforms": "wtforms>=3.1.0",
73
+ "flask_sqlalchemy": "flask-sqlalchemy>=3.1.0",
74
+ "flask_login": "flask-login>=0.6.0",
75
+ "flask_wtf": "flask-wtf>=1.2.0",
76
+ "flask_cors": "flask-cors>=4.0.0",
77
+ }
78
+
79
+
80
+ def _scan_imports(code: str) -> list[str]:
81
+ """Scan Python code for import statements and return package names."""
82
+ packages = set()
83
+
84
+ # Match: import xxx
85
+ for m in re.finditer(r"^\s*import\s+([a-zA-Z_][\w.]*)", code, re.MULTILINE):
86
+ top_level = m.group(1).split(".")[0]
87
+ packages.add(top_level)
88
+
89
+ # Match: from xxx import ...
90
+ for m in re.finditer(r"^\s*from\s+([a-zA-Z_][\w.]*)", code, re.MULTILINE):
91
+ top_level = m.group(1).split(".")[0]
92
+ packages.add(top_level)
93
+
94
+ return sorted(packages)
95
+
96
+
97
+ def generate_requirements(code: str) -> str:
98
+ """Generate requirements.txt content from code by scanning imports.
99
+
100
+ Returns a newline-separated string of pip package specs.
101
+ """
102
+ packages = _scan_imports(code)
103
+ reqs: list[str] = []
104
+
105
+ for pkg in packages:
106
+ if pkg in IMPORT_TO_PACKAGE:
107
+ req_spec = IMPORT_TO_PACKAGE[pkg]
108
+ if req_spec not in reqs:
109
+ reqs.append(req_spec)
110
+ # Skip stdlib modules (os, sys, json, re, math, etc.)
111
+
112
+ # Always include gradio for Gradio apps if not already
113
+ if "import gradio" in code or "from gradio" in code:
114
+ if "gradio" not in [r.split(">=")[0].split("[")[0] for r in reqs]:
115
+ reqs.insert(0, "gradio>=4.0.0")
116
+
117
+ return "\n".join(reqs) + "\n" if reqs else ""
118
+
119
+
120
+ def _find_entry_point(files: dict[str, str]) -> str:
121
+ """Find the main entry point file for a project.
122
+
123
+ Looks for app.py, main.py, or any Python file with a launcher pattern.
124
+ """
125
+ # Priority order for Python entry points
126
+ candidates = ["app.py", "main.py", "index.py", "server.py", "run.py"]
127
+ for c in candidates:
128
+ if c in files:
129
+ return c
130
+
131
+ # Priority order for JS entry points
132
+ js_candidates = ["index.js", "server.js", "src/index.js", "src/main.jsx", "src/main.tsx"]
133
+ for c in js_candidates:
134
+ if c in files:
135
+ return c
136
+
137
+ # Look for any .py file with if __name__ == "__main__" or .launch()
138
+ for fname, content in files.items():
139
+ if fname.endswith(".py"):
140
+ if "__main__" in content or ".launch(" in content or "app.run(" in content:
141
+ return fname
142
+
143
+ # Fall back to first .py file
144
+ for fname in files:
145
+ if fname.endswith(".py"):
146
+ return fname
147
+
148
+ # Fall back to first file
149
+ return next(iter(files), "app.py")
150
+
151
+
152
+ def _detect_sdk(files: dict[str, str], entry: str) -> str:
153
+ """Auto-detect the best Space SDK from the project files."""
154
+ all_code = "\n".join(files.values())
155
+
156
+ if "import streamlit" in all_code or "from streamlit" in all_code:
157
+ return "streamlit"
158
+ if "import gradio" in all_code or "from gradio" in all_code:
159
+ return "gradio"
160
+
161
+ # JS/TS projects β†’ Docker
162
+ if is_js_project(files):
163
+ return "docker"
164
+
165
+ if any(f.endswith(".html") for f in files):
166
+ return "static"
167
+ if entry.endswith(".py"):
168
+ return "gradio" # Default Python to Gradio SDK
169
+
170
+ return "static"
171
+
172
+
173
+ def create_project_zip(files: dict[str, str], project_name: str) -> str:
174
+ """Create a ZIP file from extracted project files.
175
+
176
+ Returns the path to the created ZIP file.
177
+ """
178
+ zip_dir = tempfile.mkdtemp(prefix="fullstack_project_")
179
+ zip_path = os.path.join(zip_dir, f"{project_name}.zip")
180
+
181
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
182
+ for filepath, content in files.items():
183
+ zf.writestr(f"{project_name}/{filepath}", content)
184
+
185
+ return zip_path
186
+
187
+
188
+ def push_to_huggingface(
189
+ files: dict[str, str],
190
+ project_name: str,
191
+ repo_name: str,
192
+ hf_token: str,
193
+ space_sdk: str = "static",
194
+ is_space: bool = True,
195
+ ) -> dict[str, Any]:
196
+ """Push generated project to HuggingFace Hub.
197
+
198
+ Creates the repo if it doesn't exist, writes all files,
199
+ and adds README.md, Dockerfile, package.json, and requirements.txt as needed.
200
+ """
201
+ try:
202
+ from huggingface_hub import HfApi, create_repo
203
+
204
+ api = HfApi(token=hf_token)
205
+
206
+ if "/" in repo_name:
207
+ namespace, name = repo_name.split("/", 1)
208
+ else:
209
+ user_info = api.whoami()
210
+ namespace = user_info["name"]
211
+ name = repo_name
212
+ repo_name = f"{namespace}/{name}"
213
+
214
+ # Find entry point and auto-detect SDK
215
+ entry_point = _find_entry_point(files)
216
+ detected_sdk = _detect_sdk(files, entry_point)
217
+
218
+ # Use detected SDK if user left it as "static" but project needs something else
219
+ if space_sdk == "static" and detected_sdk != "static":
220
+ space_sdk = detected_sdk
221
+
222
+ # For JS projects, scaffold Docker support files
223
+ if is_js_project(files) or space_sdk == "docker":
224
+ framework = detect_framework(files)
225
+ if framework == "static":
226
+ # Single HTML file or simple JS β€” keep as static
227
+ if any(f.endswith(".html") for f in files) and not is_js_project(files):
228
+ space_sdk = "static"
229
+ else:
230
+ framework = "nodejs"
231
+ space_sdk = "docker"
232
+
233
+ if space_sdk == "docker":
234
+ files = scaffold_js_project(files, framework, project_name)
235
+
236
+ try:
237
+ if is_space:
238
+ create_repo(
239
+ repo_id=repo_name,
240
+ repo_type="space",
241
+ space_sdk=space_sdk,
242
+ token=hf_token,
243
+ exist_ok=True,
244
+ )
245
+ else:
246
+ create_repo(
247
+ repo_id=repo_name,
248
+ repo_type="model",
249
+ token=hf_token,
250
+ exist_ok=True,
251
+ )
252
+ except Exception as e:
253
+ logger.warning("Repo creation warning: %s", e)
254
+
255
+ with tempfile.TemporaryDirectory(prefix="hf_push_") as tmp_dir:
256
+ # Write all project files
257
+ for filepath, content in files.items():
258
+ full_path = os.path.join(tmp_dir, filepath)
259
+ os.makedirs(os.path.dirname(full_path), exist_ok=True)
260
+ Path(full_path).write_text(content, encoding="utf-8")
261
+
262
+ # Ensure the entry point is named app.py for HF Spaces (Python)
263
+ if entry_point != "app.py" and entry_point.endswith(".py") and is_space and space_sdk in ("gradio", "streamlit"):
264
+ src = os.path.join(tmp_dir, entry_point)
265
+ dst = os.path.join(tmp_dir, "app.py")
266
+ if os.path.exists(src) and not os.path.exists(dst):
267
+ import shutil
268
+ shutil.copy2(src, dst)
269
+
270
+ # Determine app_file for README
271
+ if space_sdk == "docker":
272
+ app_file = "Dockerfile"
273
+ elif space_sdk in ("gradio", "streamlit"):
274
+ app_file = "app.py"
275
+ elif space_sdk == "static":
276
+ if "index.html" in files:
277
+ app_file = "index.html"
278
+ elif any(f.endswith(".html") for f in files):
279
+ app_file = next(f for f in files if f.endswith(".html"))
280
+ else:
281
+ app_file = entry_point
282
+ else:
283
+ app_file = entry_point
284
+
285
+ # Add README.md if not present
286
+ readme_path = os.path.join(tmp_dir, "README.md")
287
+ if not os.path.exists(readme_path):
288
+ readme_content = f"""---
289
+ title: {name}
290
+ emoji: πŸš€
291
+ colorFrom: blue
292
+ colorTo: purple
293
+ sdk: {space_sdk}
294
+ app_file: {app_file}
295
+ pinned: false
296
+ ---
297
+
298
+ # {name}
299
+
300
+ Generated by SoniCoder using {MODEL_ID}.
301
+ """
302
+ Path(readme_path).write_text(readme_content, encoding="utf-8")
303
+ else:
304
+ # Update app_file in existing README to match entry point
305
+ existing = Path(readme_path).read_text(encoding="utf-8")
306
+ if "app_file:" in existing:
307
+ existing = re.sub(
308
+ r"app_file:\s*\S+", f"app_file: {app_file}", existing
309
+ )
310
+ if "sdk:" in existing:
311
+ existing = re.sub(
312
+ r"sdk:\s*\S+", f"sdk: {space_sdk}", existing
313
+ )
314
+ Path(readme_path).write_text(existing, encoding="utf-8")
315
+
316
+ # Add/merge requirements.txt for Python projects
317
+ req_path = os.path.join(tmp_dir, "requirements.txt")
318
+ has_python = any(f.endswith(".py") for f in files.keys())
319
+
320
+ if has_python and space_sdk != "docker":
321
+ # Scan all Python code for imports
322
+ all_py_code = "\n".join(
323
+ content for fname, content in files.items()
324
+ if fname.endswith(".py")
325
+ )
326
+ auto_reqs = generate_requirements(all_py_code)
327
+
328
+ if os.path.exists(req_path):
329
+ # Merge with existing requirements.txt
330
+ existing_reqs = Path(req_path).read_text(encoding="utf-8").strip()
331
+ merged = set()
332
+ for line in (existing_reqs + "\n" + auto_reqs).splitlines():
333
+ line = line.strip()
334
+ if line and not line.startswith("#"):
335
+ merged.add(line)
336
+
337
+ Path(req_path).write_text("\n".join(sorted(merged)) + "\n", encoding="utf-8")
338
+ elif auto_reqs:
339
+ Path(req_path).write_text(auto_reqs, encoding="utf-8")
340
+ else:
341
+ # Minimal requirements for Python Spaces
342
+ Path(req_path).write_text("gradio>=4.0.0\n", encoding="utf-8")
343
+
344
+ api.upload_folder(
345
+ folder_path=tmp_dir,
346
+ repo_id=repo_name,
347
+ repo_type="space" if is_space else "model",
348
+ token=hf_token,
349
+ )
350
+
351
+ repo_url = f"https://huggingface.co/{repo_name}"
352
+ if is_space:
353
+ repo_url = f"https://huggingface.co/spaces/{repo_name}"
354
+
355
+ return {
356
+ "success": True,
357
+ "url": repo_url,
358
+ "repo_name": repo_name,
359
+ "message": f"Successfully pushed to {repo_url}",
360
+ }
361
+
362
+ except Exception as exc:
363
+ logger.exception("Failed to push to HuggingFace")
364
+ return {
365
+ "success": False,
366
+ "url": "",
367
+ "repo_name": repo_name,
368
+ "message": f"Failed to push: {str(exc)}",
369
+ }
code/model/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Model loading and inference."""
code/model/inference.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model inference β€” streaming and synchronous generation.
2
+
3
+ Supports two inference paths:
4
+ - Text-only (MiniCPM5-1B): uses TextIteratorStreamer for real-time streaming
5
+ - VLM (MiniCPM-V-4.6): uses processor.apply_chat_template() with image support
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import threading
12
+ from collections.abc import Iterator
13
+ from typing import Any
14
+
15
+ from code.config.constants import DEFAULT_TEMPERATURE, DEFAULT_MAX_TOKENS, MODEL_CONFIGS
16
+ from code.model.loader import (
17
+ get_model,
18
+ get_tokenizer_or_processor,
19
+ get_model_status,
20
+ is_model_loaded,
21
+ get_current_model_key,
22
+ get_current_model_type,
23
+ )
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def call_model(
29
+ messages: list[dict[str, Any]],
30
+ max_new_tokens: int = DEFAULT_MAX_TOKENS,
31
+ image_url: str | None = None,
32
+ ) -> Iterator[str]:
33
+ """Stream model text. Yields progressively longer strings (full text so far).
34
+
35
+ For VLM models, if image_url is provided, it's included in the last user message.
36
+ """
37
+ if not is_model_loaded():
38
+ status = get_model_status()
39
+ yield status["message"]
40
+ return
41
+
42
+ model_type = get_current_model_type()
43
+
44
+ if model_type == "vlm":
45
+ yield from _call_vlm_model(messages, max_new_tokens, image_url)
46
+ else:
47
+ yield from _call_text_model(messages, max_new_tokens)
48
+
49
+
50
+ def _call_text_model(
51
+ messages: list[dict[str, Any]],
52
+ max_new_tokens: int,
53
+ ) -> Iterator[str]:
54
+ """Stream text from a text-only model using TextIteratorStreamer."""
55
+ model = get_model()
56
+ tokenizer = get_tokenizer_or_processor()
57
+
58
+ try:
59
+ from transformers import TextIteratorStreamer
60
+ import torch
61
+
62
+ # Build the prompt from messages
63
+ prompt_parts: list[str] = []
64
+ for msg in messages:
65
+ role = msg.get("role", "user")
66
+ content = msg.get("content", "")
67
+ if role == "system":
68
+ prompt_parts.append(f"System: {content}")
69
+ elif role == "user":
70
+ prompt_parts.append(f"User: {content}")
71
+ elif role == "assistant":
72
+ prompt_parts.append(f"Assistant: {content}")
73
+ prompt_parts.append("Assistant:")
74
+ full_prompt = "\n\n".join(prompt_parts)
75
+
76
+ # Tokenize
77
+ inputs = tokenizer(full_prompt, return_tensors="pt", truncation=True, max_length=4096)
78
+ if torch.cuda.is_available():
79
+ inputs = {k: v.to("cuda") for k, v in inputs.items()}
80
+
81
+ # Stream generation
82
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True)
83
+
84
+ generation_kwargs = {
85
+ **inputs,
86
+ "streamer": streamer,
87
+ "max_new_tokens": max_new_tokens,
88
+ "temperature": DEFAULT_TEMPERATURE,
89
+ "do_sample": True,
90
+ "top_p": 0.9,
91
+ "repetition_penalty": 1.1,
92
+ "pad_token_id": tokenizer.eos_token_id,
93
+ }
94
+
95
+ # Run generation in a separate thread
96
+ thread = threading.Thread(target=model.generate, kwargs=generation_kwargs)
97
+ thread.start()
98
+
99
+ output = ""
100
+ for new_text in streamer:
101
+ output += new_text
102
+ yield output
103
+
104
+ thread.join()
105
+
106
+ except Exception as exc:
107
+ logger.exception("Error during text model inference")
108
+ yield f"_Error during generation: {exc}_"
109
+
110
+
111
+ def _call_vlm_model(
112
+ messages: list[dict[str, Any]],
113
+ max_new_tokens: int,
114
+ image_url: str | None = None,
115
+ ) -> Iterator[str]:
116
+ """Stream text from a VLM model with optional image input.
117
+
118
+ Uses processor.apply_chat_template() for proper image+text processing,
119
+ then generates with streaming via a thread.
120
+ """
121
+ model = get_model()
122
+ processor = get_tokenizer_or_processor()
123
+
124
+ try:
125
+ import torch
126
+
127
+ # Build VLM-style messages with image support
128
+ vlm_messages = _build_vlm_messages(messages, image_url)
129
+
130
+ # Apply chat template
131
+ try:
132
+ inputs = processor.apply_chat_template(
133
+ vlm_messages,
134
+ tokenize=True,
135
+ add_generation_prompt=True,
136
+ return_dict=True,
137
+ return_tensors="pt",
138
+ downsample_mode="16x",
139
+ max_slice_nums=9,
140
+ )
141
+ except TypeError:
142
+ # Fallback for older transformers without downsample_mode
143
+ inputs = processor.apply_chat_template(
144
+ vlm_messages,
145
+ tokenize=True,
146
+ add_generation_prompt=True,
147
+ return_dict=True,
148
+ return_tensors="pt",
149
+ )
150
+
151
+ if torch.cuda.is_available():
152
+ inputs = inputs.to("cuda")
153
+ else:
154
+ # Move to CPU explicitly
155
+ inputs = inputs.to("cpu")
156
+
157
+ # Generate with streaming
158
+ try:
159
+ from transformers import TextIteratorStreamer
160
+ streamer = TextIteratorStreamer(
161
+ processor.tokenizer if hasattr(processor, 'tokenizer') else processor,
162
+ skip_prompt=True,
163
+ skip_special_tokens=True,
164
+ )
165
+
166
+ gen_kwargs = {
167
+ **inputs,
168
+ "streamer": streamer,
169
+ "max_new_tokens": max_new_tokens,
170
+ "temperature": DEFAULT_TEMPERATURE,
171
+ "do_sample": True,
172
+ "top_p": 0.9,
173
+ "repetition_penalty": 1.1,
174
+ }
175
+ # Add downsample_mode if supported
176
+ try:
177
+ gen_kwargs["downsample_mode"] = "16x"
178
+ except Exception:
179
+ pass
180
+
181
+ # Ensure pad_token_id
182
+ if hasattr(processor, 'tokenizer') and hasattr(processor.tokenizer, 'eos_token_id'):
183
+ gen_kwargs["pad_token_id"] = processor.tokenizer.eos_token_id
184
+ elif hasattr(processor, 'eos_token_id'):
185
+ gen_kwargs["pad_token_id"] = processor.eos_token_id
186
+
187
+ thread = threading.Thread(target=model.generate, kwargs=gen_kwargs)
188
+ thread.start()
189
+
190
+ output = ""
191
+ for new_text in streamer:
192
+ output += new_text
193
+ yield output
194
+
195
+ thread.join()
196
+
197
+ except Exception as stream_err:
198
+ # Fallback: non-streaming generation
199
+ logger.warning("Streaming failed for VLM, falling back to sync: %s", stream_err)
200
+ gen_kwargs = {
201
+ **inputs,
202
+ "max_new_tokens": max_new_tokens,
203
+ "temperature": DEFAULT_TEMPERATURE,
204
+ "do_sample": True,
205
+ "top_p": 0.9,
206
+ }
207
+ try:
208
+ gen_kwargs["downsample_mode"] = "16x"
209
+ except Exception:
210
+ pass
211
+
212
+ generated_ids = model.generate(**gen_kwargs)
213
+ # Trim input tokens from output
214
+ input_len = inputs["input_ids"].shape[1] if hasattr(inputs, "shape") else len(inputs["input_ids"])
215
+ generated_ids_trimmed = [
216
+ out_ids[len(in_ids):]
217
+ for in_ids, out_ids in zip(inputs["input_ids"], generated_ids)
218
+ ]
219
+ tok = processor.tokenizer if hasattr(processor, 'tokenizer') else processor
220
+ output_text = tok.batch_decode(
221
+ generated_ids_trimmed, skip_special_tokens=True, clean_up_tokenization_spaces=False
222
+ )
223
+ yield output_text[0] if output_text else ""
224
+
225
+ except Exception as exc:
226
+ logger.exception("Error during VLM model inference")
227
+ yield f"_Error during generation: {exc}_"
228
+
229
+
230
+ def _build_vlm_messages(
231
+ messages: list[dict[str, Any]],
232
+ image_url: str | None = None,
233
+ ) -> list[dict[str, Any]]:
234
+ """Build VLM-style messages with image content blocks.
235
+
236
+ If an image_url is provided, it's injected into the last user message
237
+ as a content block with type "image".
238
+ """
239
+ vlm_messages = []
240
+
241
+ for i, msg in enumerate(messages):
242
+ role = msg.get("role", "user")
243
+ content = msg.get("content", "")
244
+
245
+ if role == "system":
246
+ # System messages stay as-is for VLM
247
+ vlm_messages.append({"role": "system", "content": content})
248
+ continue
249
+
250
+ # For the last user message with an image, use structured content
251
+ is_last_user = (i == len(messages) - 1) and role == "user"
252
+
253
+ if is_last_user and image_url:
254
+ # Build content list with image + text
255
+ content_list = [{"type": "image", "url": image_url}]
256
+ if content.strip():
257
+ content_list.append({"type": "text", "text": content})
258
+ vlm_messages.append({"role": "user", "content": content_list})
259
+ else:
260
+ vlm_messages.append({"role": role, "content": content})
261
+
262
+ return vlm_messages
263
+
264
+
265
+ def call_model_sync(
266
+ messages: list[dict[str, Any]],
267
+ max_new_tokens: int = DEFAULT_MAX_TOKENS,
268
+ image_url: str | None = None,
269
+ ) -> str:
270
+ """Non-streaming model call β€” returns complete response."""
271
+ result = ""
272
+ for chunk in call_model(messages, max_new_tokens, image_url):
273
+ result = chunk
274
+ return result
code/model/loader.py ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Model loading and status management.
2
+
3
+ Supports two models:
4
+ - MiniCPM5-1B (text-only, fast)
5
+ - MiniCPM-V-4.6 (vision + text, image understanding)
6
+
7
+ Only one model is loaded at a time to conserve memory.
8
+ The model is loaded in a background thread on startup.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import gc
14
+ import logging
15
+ import threading
16
+ from typing import Any
17
+
18
+ from code.config.constants import DEFAULT_MODEL_KEY, MODEL_CONFIGS
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ # ─── Module-level state ─────────────────────────────────────────────────
23
+
24
+ _current_model_key: str = DEFAULT_MODEL_KEY
25
+ _model = None
26
+ _tokenizer_or_processor = None
27
+ _model_loaded = False
28
+ _model_loading = False
29
+ _load_error: str | None = None
30
+
31
+
32
+ def _unload_model() -> None:
33
+ """Unload current model and free memory."""
34
+ global _model, _tokenizer_or_processor, _model_loaded
35
+
36
+ if _model is not None:
37
+ del _model
38
+ _model = None
39
+ if _tokenizer_or_processor is not None:
40
+ del _tokenizer_or_processor
41
+ _tokenizer_or_processor = None
42
+
43
+ _model_loaded = False
44
+ gc.collect()
45
+
46
+ try:
47
+ import torch
48
+ if torch.cuda.is_available():
49
+ torch.cuda.empty_cache()
50
+ except ImportError:
51
+ pass
52
+
53
+
54
+ def load_model(model_key: str | None = None) -> None:
55
+ """Load a model by key. Unloads the previous model first."""
56
+ global _model, _tokenizer_or_processor, _model_loaded, _model_loading
57
+ global _load_error, _current_model_key
58
+
59
+ if model_key is None:
60
+ model_key = _current_model_key
61
+
62
+ if model_key not in MODEL_CONFIGS:
63
+ _load_error = f"Unknown model: {model_key}"
64
+ logger.error(_load_error)
65
+ return
66
+
67
+ # Skip if already loading or already loaded with same key
68
+ if _model_loading:
69
+ return
70
+ if _model_loaded and _current_model_key == model_key:
71
+ return
72
+
73
+ _model_loading = True
74
+ _load_error = None
75
+
76
+ # Unload previous model if switching
77
+ if _model_loaded and _current_model_key != model_key:
78
+ logger.info("Switching model from %s to %s", _current_model_key, model_key)
79
+ _unload_model()
80
+
81
+ _current_model_key = model_key
82
+ config = MODEL_CONFIGS[model_key]
83
+ model_id = config["id"]
84
+
85
+ try:
86
+ import torch
87
+
88
+ dtype = torch.float16 if torch.cuda.is_available() else torch.float32
89
+ device_map = "auto" if torch.cuda.is_available() else None
90
+
91
+ if config["type"] == "vlm":
92
+ _load_vlm_model(model_id, dtype, device_map)
93
+ else:
94
+ _load_text_model(model_id, dtype, device_map)
95
+
96
+ _model_loaded = True
97
+ logger.info("%s model loaded successfully.", config["name"])
98
+
99
+ except Exception as exc:
100
+ _load_error = str(exc)
101
+ logger.exception("Failed to load model %s: %s", model_id, exc)
102
+ finally:
103
+ _model_loading = False
104
+
105
+
106
+ def _load_text_model(model_id: str, dtype, device_map) -> None:
107
+ """Load a text-only model (AutoModelForCausalLM + AutoTokenizer)."""
108
+ global _model, _tokenizer_or_processor
109
+
110
+ from transformers import AutoModelForCausalLM, AutoTokenizer
111
+
112
+ logger.info("Loading %s (text model)...", model_id)
113
+
114
+ _tokenizer_or_processor = AutoTokenizer.from_pretrained(
115
+ model_id,
116
+ trust_remote_code=True,
117
+ )
118
+ _model = AutoModelForCausalLM.from_pretrained(
119
+ model_id,
120
+ torch_dtype=dtype,
121
+ device_map=device_map,
122
+ trust_remote_code=True,
123
+ low_cpu_mem_usage=True,
124
+ )
125
+
126
+ if device_map is None:
127
+ _model = _model.to("cpu")
128
+
129
+ _model.eval()
130
+
131
+
132
+ def _load_vlm_model(model_id: str, dtype, device_map) -> None:
133
+ """Load a vision-language model (AutoModelForImageTextToText + AutoProcessor)."""
134
+ global _model, _tokenizer_or_processor
135
+
136
+ try:
137
+ from transformers import AutoModelForImageTextToText, AutoProcessor
138
+ except ImportError:
139
+ # Fallback for older transformers
140
+ logger.warning("AutoModelForImageTextToText not found, trying AutoModel...")
141
+ from transformers import AutoModel as AutoModelForImageTextToText
142
+ from transformers import AutoProcessor
143
+
144
+ logger.info("Loading %s (VLM)...", model_id)
145
+
146
+ _tokenizer_or_processor = AutoProcessor.from_pretrained(
147
+ model_id,
148
+ trust_remote_code=True,
149
+ )
150
+ _model = AutoModelForImageTextToText.from_pretrained(
151
+ model_id,
152
+ torch_dtype=dtype,
153
+ device_map=device_map,
154
+ trust_remote_code=True,
155
+ low_cpu_mem_usage=True,
156
+ )
157
+
158
+ if device_map is None:
159
+ _model = _model.to("cpu")
160
+
161
+ _model.eval()
162
+
163
+
164
+ def start_background_load(model_key: str | None = None) -> threading.Thread:
165
+ """Start loading the model in a background daemon thread."""
166
+ thread = threading.Thread(target=load_model, args=(model_key,), daemon=True)
167
+ thread.start()
168
+ return thread
169
+
170
+
171
+ def switch_model(model_key: str) -> dict[str, Any]:
172
+ """Switch to a different model. Returns status immediately, loads in background."""
173
+ global _current_model_key
174
+
175
+ if model_key not in MODEL_CONFIGS:
176
+ return {"success": False, "message": f"Unknown model: {model_key}"}
177
+
178
+ if _current_model_key == model_key and _model_loaded:
179
+ return {"success": True, "message": f"Already using {MODEL_CONFIGS[model_key]['name']}"}
180
+
181
+ _current_model_key = model_key
182
+ _model_loaded = False
183
+
184
+ # Start loading in background
185
+ start_background_load(model_key)
186
+
187
+ config = MODEL_CONFIGS[model_key]
188
+ return {
189
+ "success": True,
190
+ "message": f"Switching to {config['name']}...",
191
+ "model_key": model_key,
192
+ "model_name": config["name"],
193
+ }
194
+
195
+
196
+ def get_model_status() -> dict[str, Any]:
197
+ """Return current model loading status."""
198
+ config = MODEL_CONFIGS.get(_current_model_key, {})
199
+ if _model_loaded:
200
+ return {
201
+ "status": "ready",
202
+ "message": f"{config.get('name', 'Model')} loaded and ready",
203
+ "model_key": _current_model_key,
204
+ "model_name": config.get("name", ""),
205
+ "model_type": config.get("type", "text"),
206
+ }
207
+ if _model_loading:
208
+ return {
209
+ "status": "loading",
210
+ "message": f"Loading {config.get('name', 'model')}... (this may take a few minutes)",
211
+ "model_key": _current_model_key,
212
+ "model_name": config.get("name", ""),
213
+ "model_type": config.get("type", "text"),
214
+ }
215
+ if _load_error:
216
+ return {
217
+ "status": "error",
218
+ "message": f"Model load error: {_load_error}",
219
+ "model_key": _current_model_key,
220
+ "model_name": config.get("name", ""),
221
+ "model_type": config.get("type", "text"),
222
+ }
223
+ return {
224
+ "status": "unknown",
225
+ "message": "Model not initialized",
226
+ "model_key": _current_model_key,
227
+ "model_name": config.get("name", ""),
228
+ "model_type": config.get("type", "text"),
229
+ }
230
+
231
+
232
+ def get_model():
233
+ """Return the loaded model instance (or None)."""
234
+ return _model
235
+
236
+
237
+ def get_tokenizer_or_processor():
238
+ """Return the loaded tokenizer or processor (or None)."""
239
+ return _tokenizer_or_processor
240
+
241
+
242
+ def is_model_loaded() -> bool:
243
+ """Return True if the model has been loaded successfully."""
244
+ return _model_loaded
245
+
246
+
247
+ def get_current_model_key() -> str:
248
+ """Return the key of the currently selected model."""
249
+ return _current_model_key
250
+
251
+
252
+ def get_current_model_type() -> str:
253
+ """Return 'text' or 'vlm' for the current model."""
254
+ return MODEL_CONFIGS.get(_current_model_key, {}).get("type", "text")
code/server/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """FastAPI / Gradio server routes."""
code/server/chat_helpers.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Chat helper functions β€” history conversion, prompt building, iteration context."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from code.config.constants import SYSTEM_PROMPT
8
+ from code.execution.code_extractor import strip_thinking_blocks
9
+
10
+
11
+ def chat_history_to_messages(history: list[dict[str, str]]) -> list[dict[str, Any]]:
12
+ """Convert chat history list to messages format for the model.
13
+
14
+ Prepends the system prompt and strips thinking blocks from
15
+ assistant messages.
16
+ """
17
+ messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
18
+ for item in history:
19
+ role = item.get("role")
20
+ content = str(item.get("content") or "").strip()
21
+ if role not in {"user", "assistant"} or not content:
22
+ continue
23
+ if role == "assistant":
24
+ content = strip_thinking_blocks(content)
25
+ messages.append({"role": role, "content": content})
26
+ return messages
27
+
28
+
29
+ def clip_context(text: str, limit: int = 4_000) -> str:
30
+ """Truncate text to a character limit with a note."""
31
+ if len(text) <= limit:
32
+ return text
33
+ return text[:limit] + f"\n... truncated {len(text) - limit} characters ..."
34
+
35
+
36
+ def iteration_context(execution_context: dict[str, Any] | None) -> str:
37
+ """Build a context string from previous execution results.
38
+
39
+ This allows the model to reference prior code, stdout, and stderr
40
+ when the user asks to iterate or debug.
41
+ """
42
+ if not execution_context or not execution_context.get("code"):
43
+ return ""
44
+
45
+ code = clip_context(str(execution_context.get("code") or ""), 6_000)
46
+ target = str(execution_context.get("target") or "code")
47
+ fence_lang = str(execution_context.get("fence_lang") or target)
48
+ status = str(execution_context.get("status") or "")
49
+ stdout = clip_context(str(execution_context.get("stdout") or ""), 2_000)
50
+ stderr = clip_context(str(execution_context.get("stderr") or ""), 2_000)
51
+
52
+ parts = [
53
+ "Previous generated code and run result are available for iteration.",
54
+ f"Previous target: {target}",
55
+ f"Previous status: {status}",
56
+ f"Previous code:\n```{fence_lang}\n{code}\n```",
57
+ ]
58
+ if stdout:
59
+ parts.append(f"Previous stdout:\n{stdout}")
60
+ if stderr:
61
+ parts.append(f"Previous stderr / traceback:\n{stderr}")
62
+ parts.append(
63
+ "If the user asks to revise, debug, extend, or explain the prior code, use this context."
64
+ )
65
+ return "\n\n".join(parts)
66
+
67
+
68
+ def targeted_prompt(
69
+ prompt: str,
70
+ target_language: str,
71
+ target_framework: str = "",
72
+ execution_context: dict[str, Any] | None = None,
73
+ search_context: str = "",
74
+ ) -> str:
75
+ """Build the full user prompt with language, framework, search, and iteration context."""
76
+ iter_ctx = iteration_context(execution_context)
77
+ context_block = f"\n\n{iter_ctx}" if iter_ctx else ""
78
+
79
+ search_block = ""
80
+ if search_context:
81
+ search_block = (
82
+ f"\n\n{search_context}\n\n"
83
+ "Use the above search results to inform your code generation if relevant."
84
+ )
85
+
86
+ framework_hint = f" using {target_framework}" if target_framework else ""
87
+
88
+ gradio_hint = ""
89
+ if target_framework == "Gradio":
90
+ gradio_hint = (
91
+ "\n\nIMPORTANT: This is a Gradio app. Create a complete Python script that:\n"
92
+ "- Imports gradio as gr\n"
93
+ "- Defines the UI using gr.Interface() or gr.Blocks()\n"
94
+ "- Includes all processing logic inline\n"
95
+ "- Calls .launch(server_name='0.0.0.0', server_port=7860) at the end\n"
96
+ "- Uses only standard library + gradio + common packages (PIL, matplotlib, numpy)\n"
97
+ "- Make the UI clean, modern, and functional"
98
+ )
99
+
100
+ return (
101
+ f"Target: {target_language}{framework_hint}. Generate a complete, runnable application. "
102
+ "If the user asks for a web app, include all HTML/CSS/JS. "
103
+ "If they ask for a backend, include the server code and any API definitions. "
104
+ "For single-file apps, use a single code block. For multi-file projects, use the @@FILE: format. "
105
+ "Make the code complete, working, and well-structured."
106
+ f"{gradio_hint}"
107
+ f"{search_block}"
108
+ f"{context_block}\n\n"
109
+ f"User request:\n{prompt}"
110
+ )
code/server/routes.py ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """FastAPI / Gradio Server routes.
2
+
3
+ Defines all HTTP and API endpoints:
4
+ - GET / β†’ serves the index.html frontend
5
+ - GET /api/model-status β†’ model loading status
6
+ - GET /images/{f} β†’ serve generated plot images
7
+ - GET /download/{f} β†’ serve project ZIP downloads
8
+ - API web_search β†’ Google search scraping
9
+ - API chat β†’ streaming chat with code execution
10
+ - API push_hf β†’ push to HuggingFace Hub
11
+ - API switch_model β†’ switch between loaded models
12
+ - API upload_image β†’ upload image for VLM inference
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import json
19
+ import logging
20
+ import os
21
+ import tempfile
22
+ from pathlib import Path
23
+ from typing import Any
24
+
25
+ from fastapi.responses import HTMLResponse, FileResponse
26
+ from gradio import Server
27
+
28
+ from code.config.constants import (
29
+ APP_TITLE,
30
+ DEFAULT_MODEL_KEY,
31
+ EXAMPLE_PROMPTS,
32
+ LANGUAGE_OPTIONS,
33
+ MODEL_CONFIGS,
34
+ MODEL_URL,
35
+ PY_TIMEOUT_S,
36
+ )
37
+ from code.execution.code_extractor import (
38
+ build_iframe,
39
+ extract_code,
40
+ extract_multi_file,
41
+ is_gradio_code,
42
+ normalize_language,
43
+ strip_thinking_blocks,
44
+ )
45
+ from code.execution.gradio_runner import run_gradio_app, stop_gradio_app
46
+ from code.execution.python_runner import run_python
47
+ from code.huggingface.push import create_project_zip, push_to_huggingface
48
+ from code.model.loader import (
49
+ get_model_status,
50
+ is_model_loaded,
51
+ get_current_model_key,
52
+ get_current_model_type,
53
+ switch_model,
54
+ )
55
+ from code.model.inference import call_model
56
+ from code.server.chat_helpers import chat_history_to_messages, targeted_prompt
57
+ from code.websearch.google_scraper import web_search_google, format_search_results
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+ # ─── Served Files Registry ──────────────────────────────────────────────
62
+
63
+ _served_files: dict[str, str] = {}
64
+
65
+ # ─── Uploaded Images Registry ───────────────────────────────────────────
66
+
67
+ _uploaded_images: dict[str, str] = {}
68
+
69
+ # ─── Server Instance ────────────────────────────────────────────────────
70
+
71
+ app = Server()
72
+
73
+
74
+ # ─── HTTP Routes ────────────────────────────────────────────────────────
75
+
76
+
77
+ @app.get("/", response_class=HTMLResponse)
78
+ async def homepage():
79
+ """Serve the index.html frontend with runtime config injected."""
80
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "index.html")
81
+ with open(html_path, "r", encoding="utf-8") as f:
82
+ content = f.read()
83
+
84
+ config = json.dumps({
85
+ "app_title": APP_TITLE,
86
+ "model_id": MODEL_CONFIGS[DEFAULT_MODEL_KEY]["id"],
87
+ "model_configs": {k: {"name": v["name"], "type": v["type"], "description": v["description"]} for k, v in MODEL_CONFIGS.items()},
88
+ "model_url": MODEL_URL,
89
+ "languages": LANGUAGE_OPTIONS,
90
+ "examples": [
91
+ {"label": label, "prompt": prompt, "language": lang, "framework": fw}
92
+ for label, prompt, lang, fw in EXAMPLE_PROMPTS
93
+ ],
94
+ "default_model": "minicpm5-1b",
95
+ })
96
+ content = content.replace("__RUNTIME_CONFIG__", config)
97
+ return content
98
+
99
+
100
+ @app.get("/api/model-status")
101
+ async def model_status_endpoint():
102
+ """Return the current model loading status."""
103
+ return get_model_status()
104
+
105
+
106
+ @app.get("/images/{filename}")
107
+ async def serve_image(filename: str):
108
+ """Serve a generated plot image by filename."""
109
+ path = _served_files.get(f"img:{filename}")
110
+ if path and os.path.exists(path):
111
+ return FileResponse(path, media_type="image/png")
112
+ return HTMLResponse("Not found", status_code=404)
113
+
114
+
115
+ @app.get("/download/{filename}")
116
+ async def serve_download(filename: str):
117
+ """Serve a project ZIP download by filename."""
118
+ path = _served_files.get(f"dl:{filename}")
119
+ if path and os.path.exists(path):
120
+ return FileResponse(path, filename=filename, media_type="application/octet-stream")
121
+ return HTMLResponse("Not found", status_code=404)
122
+
123
+
124
+ @app.get("/uploaded-images/{image_id}")
125
+ async def serve_uploaded_image(image_id: str):
126
+ """Serve an uploaded image by its ID."""
127
+ path = _uploaded_images.get(image_id)
128
+ if path and os.path.exists(path):
129
+ return FileResponse(path, media_type="image/png")
130
+ return HTMLResponse("Not found", status_code=404)
131
+
132
+
133
+ # ─── Gradio API Endpoints ──────────────────────────────────────────────
134
+
135
+
136
+ @app.api(name="switch_model", concurrency_limit=1)
137
+ def handle_switch_model(model_key: str) -> str:
138
+ """Switch to a different model."""
139
+ result = switch_model(model_key)
140
+ yield json.dumps(result)
141
+
142
+
143
+ @app.api(name="upload_image", concurrency_limit=4)
144
+ def handle_upload_image(image_data: str) -> str:
145
+ """Upload a base64-encoded image for VLM inference.
146
+
147
+ Returns an image ID that can be referenced in chat.
148
+ """
149
+ try:
150
+ if not image_data:
151
+ yield json.dumps({"success": False, "message": "No image data provided"})
152
+ return
153
+
154
+ # Handle data URI format: data:image/png;base64,...
155
+ if image_data.startswith("data:"):
156
+ # Extract the base64 part
157
+ parts = image_data.split(",", 1)
158
+ if len(parts) == 2:
159
+ image_data = parts[1]
160
+
161
+ # Decode base64
162
+ image_bytes = base64.b64decode(image_data)
163
+
164
+ # Save to temp file
165
+ img_dir = tempfile.mkdtemp(prefix="uploaded_img_")
166
+ image_id = f"img_{os.getpid()}_{int(os.urandom(4).hex(), 16)}"
167
+ img_path = os.path.join(img_dir, f"{image_id}.png")
168
+ Path(img_path).write_bytes(image_bytes)
169
+
170
+ # Register for serving
171
+ _uploaded_images[image_id] = img_path
172
+
173
+ # Create a URL for the image that the VLM can access
174
+ image_url = f"/uploaded-images/{image_id}"
175
+
176
+ # Also save as a file:// URL for local VLM access
177
+ file_url = f"file://{img_path}"
178
+
179
+ yield json.dumps({
180
+ "success": True,
181
+ "image_id": image_id,
182
+ "image_url": image_url,
183
+ "file_url": file_url,
184
+ "message": "Image uploaded successfully",
185
+ })
186
+
187
+ except Exception as exc:
188
+ logger.exception("Image upload failed")
189
+ yield json.dumps({
190
+ "success": False,
191
+ "message": f"Upload failed: {str(exc)}",
192
+ })
193
+
194
+
195
+ @app.api(name="web_search", concurrency_limit=4)
196
+ def handle_web_search(query: str) -> str:
197
+ """Search the web using Google scraping. No API key needed."""
198
+ query = (query or "").strip()
199
+ if not query:
200
+ yield json.dumps({"success": False, "results": [], "message": "Empty search query"})
201
+ return
202
+
203
+ try:
204
+ results = web_search_google(query, num_results=8)
205
+ formatted = format_search_results(results)
206
+
207
+ yield json.dumps({
208
+ "success": True,
209
+ "results": results,
210
+ "formatted": formatted,
211
+ "message": f"Found {len(results)} results",
212
+ })
213
+ except Exception as exc:
214
+ logger.exception("Web search failed")
215
+ yield json.dumps({
216
+ "success": False,
217
+ "results": [],
218
+ "message": f"Search failed: {str(exc)}",
219
+ })
220
+
221
+
222
+ @app.api(name="chat", concurrency_limit=2)
223
+ def handle_chat(
224
+ prompt: str,
225
+ target_language: str,
226
+ target_framework: str,
227
+ history_json: str,
228
+ exec_context_json: str,
229
+ search_enabled: str = "false",
230
+ image_url: str = "",
231
+ ) -> str:
232
+ """Stream chat responses with code execution. Yields JSON strings."""
233
+ history = json.loads(history_json) if history_json else []
234
+ execution_context = json.loads(exec_context_json) if exec_context_json else {}
235
+
236
+ prompt = (prompt or "").strip()
237
+ if not prompt:
238
+ yield json.dumps({
239
+ "type": "error",
240
+ "status_text": "Enter a prompt to get started.",
241
+ "status_state": "info",
242
+ "history": history,
243
+ "execution": execution_context,
244
+ })
245
+ return
246
+
247
+ # Check model status
248
+ model_status = get_model_status()
249
+ if model_status["status"] == "loading":
250
+ yield json.dumps({
251
+ "type": "error",
252
+ "status_text": model_status["message"],
253
+ "status_state": "working",
254
+ "history": history,
255
+ "execution": execution_context,
256
+ })
257
+ return
258
+ if model_status["status"] != "ready":
259
+ yield json.dumps({
260
+ "type": "error",
261
+ "status_text": model_status["message"],
262
+ "status_state": "error",
263
+ "history": history,
264
+ "execution": execution_context,
265
+ })
266
+ return
267
+
268
+ # Add user message and placeholder assistant message
269
+ history = list(history) + [
270
+ {"role": "user", "content": prompt},
271
+ {"role": "assistant", "content": ""},
272
+ ]
273
+ yield json.dumps({
274
+ "type": "status",
275
+ "status_text": "Thinking...",
276
+ "status_state": "working",
277
+ "history": history,
278
+ "execution": execution_context,
279
+ })
280
+
281
+ # Web search if enabled
282
+ search_context = ""
283
+ if search_enabled.lower() == "true":
284
+ yield json.dumps({
285
+ "type": "status",
286
+ "status_text": "Searching the web...",
287
+ "status_state": "working",
288
+ "history": history,
289
+ "execution": execution_context,
290
+ })
291
+ search_results = web_search_google(prompt, num_results=6)
292
+ if search_results:
293
+ search_context = format_search_results(search_results)
294
+ yield json.dumps({
295
+ "type": "search_results",
296
+ "status_text": f"Found {len(search_results)} results, generating code...",
297
+ "status_state": "working",
298
+ "history": history,
299
+ "execution": execution_context,
300
+ "search_results": search_results,
301
+ })
302
+
303
+ # Build messages for model
304
+ model_history = list(history[:-1])
305
+ model_history[-1] = {
306
+ "role": "user",
307
+ "content": targeted_prompt(
308
+ prompt, target_language, target_framework, execution_context, search_context
309
+ ),
310
+ }
311
+ messages = chat_history_to_messages(model_history)
312
+
313
+ # Determine image URL for VLM
314
+ vlm_image_url = image_url.strip() if image_url else None
315
+
316
+ final_response = ""
317
+ for partial in call_model(messages, image_url=vlm_image_url):
318
+ final_response = partial
319
+ # Strip thinking blocks so chat only shows clean output
320
+ clean_partial = strip_thinking_blocks(partial)
321
+ history[-1]["content"] = clean_partial
322
+ yield json.dumps({
323
+ "type": "streaming",
324
+ "status_text": "Generating...",
325
+ "status_state": "working",
326
+ "history": history,
327
+ "execution": execution_context,
328
+ })
329
+
330
+ if not final_response:
331
+ history[-1]["content"] = "The model did not return a response."
332
+ yield json.dumps({
333
+ "type": "error",
334
+ "status_text": "No model response.",
335
+ "status_state": "error",
336
+ "history": history,
337
+ "execution": execution_context,
338
+ })
339
+ return
340
+
341
+ # Extract code from response (use cleaned version)
342
+ clean_response = strip_thinking_blocks(final_response)
343
+ code, fence_lang = extract_code(clean_response)
344
+ target = normalize_language(target_language, fence_lang)
345
+
346
+ # Also try multi-file extraction
347
+ multi_files = extract_multi_file(clean_response)
348
+
349
+ if not code and not multi_files:
350
+ yield json.dumps({
351
+ "type": "complete",
352
+ "status_text": "Answered without running code.",
353
+ "status_state": "info",
354
+ "history": history,
355
+ "execution": execution_context,
356
+ })
357
+ return
358
+
359
+ yield json.dumps({
360
+ "type": "status",
361
+ "status_text": "Running...",
362
+ "status_state": "working",
363
+ "history": history,
364
+ "execution": execution_context,
365
+ })
366
+
367
+ # Execute code
368
+ stdout, stderr, image_path, status_text, status_state = "", "", None, "Preview ready", "success"
369
+ is_gradio = False
370
+ gradio_url = None
371
+
372
+ if target == "python" and code:
373
+ if is_gradio_code(code) or target_framework == "Gradio":
374
+ is_gradio = True
375
+ gradio_result = run_gradio_app(code)
376
+ if gradio_result["success"]:
377
+ gradio_url = gradio_result["url"]
378
+ status_text = f"Gradio app running at {gradio_url}"
379
+ status_state = "success"
380
+ stderr = f"Gradio app launched successfully at {gradio_url}"
381
+ else:
382
+ status_text = "Gradio launch failed"
383
+ status_state = "error"
384
+ stderr = gradio_result.get("stderr", gradio_result.get("message", "Launch failed"))
385
+ else:
386
+ result = run_python(code)
387
+ if result.timed_out:
388
+ stdout, stderr, image_path = result.stdout, result.stderr, result.image_path
389
+ status_text = f"Timed out after {PY_TIMEOUT_S}s"
390
+ status_state = "error"
391
+ elif result.returncode:
392
+ stdout, stderr, image_path = result.stdout, result.stderr, result.image_path
393
+ status_text = "Finished with errors"
394
+ status_state = "error"
395
+ else:
396
+ stdout, stderr, image_path = result.stdout, result.stderr, result.image_path
397
+ status_text = "Ran successfully"
398
+ status_state = "success"
399
+
400
+ # Register image for serving
401
+ image_url_out = None
402
+ if image_path:
403
+ filename = os.path.basename(image_path)
404
+ _served_files[f"img:{filename}"] = image_path
405
+ image_url_out = f"/images/{filename}"
406
+
407
+ # Register code for download
408
+ download_url = None
409
+ project_files = dict(multi_files) if multi_files else {}
410
+
411
+ # Rename main.py β†’ app.py for Python/Gradio projects (HF Spaces expects app.py)
412
+ if project_files and "main.py" in project_files and "app.py" not in project_files:
413
+ if target == "python" or is_gradio:
414
+ project_files["app.py"] = project_files.pop("main.py")
415
+
416
+ # If project_files is empty but we have single code, add it
417
+ if not project_files and code:
418
+ if target == "python":
419
+ fname = "app.py" if (is_gradio or is_gradio_code(code)) else "main.py"
420
+ elif target in {"web", "html", "javascript"}:
421
+ fname = "index.html"
422
+ else:
423
+ fname = f"main.{fence_lang or 'txt'}"
424
+ project_files = {fname: code}
425
+
426
+ if project_files:
427
+ project_name = "generated-project"
428
+ zip_path = create_project_zip(project_files, project_name)
429
+ zip_filename = f"{project_name}.zip"
430
+ _served_files[f"dl:{zip_filename}"] = zip_path
431
+ download_url = f"/download/{zip_filename}"
432
+ elif code:
433
+ ext = "py" if target == "python" else "html"
434
+ dl_filename = f"generated.{ext}"
435
+ dl_dir = tempfile.mkdtemp(prefix="fullstack_dl_")
436
+ dl_path = os.path.join(dl_dir, dl_filename)
437
+ Path(dl_path).write_text(code, encoding="utf-8")
438
+ _served_files[f"dl:{dl_filename}"] = dl_path
439
+ download_url = f"/download/{dl_filename}"
440
+
441
+ # Determine if this is web previewable
442
+ is_web = target in {"web", "javascript", "typescript", "html"} or (fence_lang or "") in {"html", "web"}
443
+ web_code = code if is_web else None
444
+
445
+ execution_context = {
446
+ "code": code,
447
+ "target": target,
448
+ "fence_lang": fence_lang or target,
449
+ "stdout": stdout,
450
+ "stderr": stderr,
451
+ "image_url": image_url_out,
452
+ "image_path": image_path,
453
+ "status": status_text,
454
+ "language": fence_lang or target,
455
+ "suggested_tab": "preview" if (image_path or is_web or is_gradio) else "console",
456
+ "download_url": download_url,
457
+ "project_files": project_files,
458
+ "is_web": is_web,
459
+ "web_code": web_code,
460
+ "is_gradio": is_gradio,
461
+ "gradio_url": gradio_url,
462
+ }
463
+
464
+ yield json.dumps({
465
+ "type": "complete",
466
+ "status_text": status_text,
467
+ "status_state": status_state,
468
+ "history": history,
469
+ "execution": execution_context,
470
+ })
471
+
472
+
473
+ @app.api(name="push_hf", concurrency_limit=1)
474
+ def handle_push_hf(
475
+ exec_context_json: str,
476
+ repo_name: str,
477
+ hf_token: str,
478
+ space_sdk: str = "auto",
479
+ is_space: str = "true",
480
+ ) -> str:
481
+ """Push generated project to HuggingFace Hub."""
482
+ try:
483
+ execution_context = json.loads(exec_context_json) if exec_context_json else {}
484
+ project_files = dict(execution_context.get("project_files", {}) or {})
485
+ code = execution_context.get("code", "")
486
+
487
+ # If project_files is empty but we have code, build files from code
488
+ if not project_files and code:
489
+ lang = execution_context.get("language", "python")
490
+ is_gradio = execution_context.get("is_gradio", False)
491
+
492
+ # Map language to entry file β€” JS/TS single-files get wrapped for Docker
493
+ if lang in ("javascript", "js", "typescript", "ts"):
494
+ # For single-file JS/TS code that is HTML (vanilla), keep as index.html
495
+ if "<!doctype" in code.lower() or "<html" in code.lower():
496
+ filename = "index.html"
497
+ else:
498
+ filename = "index.js"
499
+ elif lang in ("html", "web"):
500
+ filename = "index.html"
501
+ else:
502
+ ext_map = {
503
+ "python": "app.py", "py": "app.py",
504
+ }
505
+ filename = ext_map.get(lang, "app.py")
506
+
507
+ project_files = {filename: code}
508
+
509
+ # Auto-detect SDK for Gradio apps
510
+ if is_gradio or is_gradio_code(code):
511
+ space_sdk = "gradio"
512
+
513
+ # If still no files, try extracting from the raw response
514
+ if not project_files and code:
515
+ project_files = extract_multi_file(code)
516
+
517
+ if not project_files:
518
+ yield json.dumps({
519
+ "success": False,
520
+ "message": "No code to push. Generate some code first.",
521
+ "url": "",
522
+ })
523
+ return
524
+
525
+ # "auto" SDK means let push_to_huggingface decide
526
+ if space_sdk == "auto":
527
+ space_sdk = "static" # push_to_huggingface will auto-detect from files
528
+
529
+ project_name = repo_name.split("/")[-1] if "/" in repo_name else repo_name
530
+
531
+ result = push_to_huggingface(
532
+ files=project_files,
533
+ project_name=project_name,
534
+ repo_name=repo_name,
535
+ hf_token=hf_token,
536
+ space_sdk=space_sdk,
537
+ is_space=is_space.lower() == "true",
538
+ )
539
+
540
+ yield json.dumps(result)
541
+
542
+ except Exception as exc:
543
+ logger.exception("Push to HuggingFace failed")
544
+ yield json.dumps({
545
+ "success": False,
546
+ "message": f"Push failed: {str(exc)}",
547
+ "url": "",
548
+ })
549
+
550
+
551
+ def get_app() -> Server:
552
+ """Return the configured Gradio Server app instance."""
553
+ return app
code/websearch/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Web search (Google scraping, no API)."""
code/websearch/google_scraper.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Web search via scraping β€” no API key needed.
2
+
3
+ Strategy:
4
+ 1. Primary: DuckDuckGo HTML (more scraper-friendly, fewer captchas)
5
+ 2. Fallback: Google search with robust multi-selector parsing
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ import re
12
+ import urllib.parse
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # Common browser-like headers to avoid bot detection
17
+ _BROWSER_HEADERS = {
18
+ "User-Agent": (
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
20
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
21
+ "Chrome/125.0.0.0 Safari/537.36"
22
+ ),
23
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
24
+ "Accept-Language": "en-US,en;q=0.5",
25
+ "Accept-Encoding": "gzip, deflate",
26
+ "DNT": "1",
27
+ "Connection": "keep-alive",
28
+ "Upgrade-Insecure-Requests": "1",
29
+ }
30
+
31
+
32
+ def web_search_google(query: str, num_results: int = 8) -> list[dict[str, str]]:
33
+ """Search the web by scraping. No API key needed.
34
+
35
+ Tries DuckDuckGo first (more scraper-friendly),
36
+ then falls back to Google if DuckDuckGo returns nothing.
37
+
38
+ Returns a list of dicts with keys: title, url, snippet.
39
+ """
40
+ results = _search_duckduckgo(query, num_results)
41
+ if results:
42
+ return results
43
+
44
+ results = _search_google(query, num_results)
45
+ return results
46
+
47
+
48
+ def _search_duckduckgo(query: str, num_results: int) -> list[dict[str, str]]:
49
+ """Search DuckDuckGo HTML version β€” very scraper-friendly."""
50
+ try:
51
+ import requests
52
+ from bs4 import BeautifulSoup
53
+
54
+ encoded_query = urllib.parse.quote_plus(query)
55
+ url = f"https://html.duckduckgo.com/html/?q={encoded_query}"
56
+
57
+ headers = {**_BROWSER_HEADERS, "Referer": "https://duckduckgo.com/"}
58
+
59
+ resp = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
60
+ resp.raise_for_status()
61
+
62
+ soup = BeautifulSoup(resp.text, "html.parser")
63
+ results: list[dict[str, str]] = []
64
+
65
+ # DuckDuckGo HTML uses .result blocks
66
+ for result_div in soup.select(".result"):
67
+ title_el = result_div.select_one(".result__title a, .result__a")
68
+ snippet_el = result_div.select_one(".result__snippet")
69
+
70
+ if not title_el:
71
+ continue
72
+
73
+ title = title_el.get_text(strip=True)
74
+ # DDG uses redirect URLs like //duckduckgo.com/l/?uddg=...
75
+ href = title_el.get("href", "")
76
+
77
+ real_url = _extract_ddg_url(href)
78
+ if not real_url:
79
+ continue
80
+
81
+ # Skip internal URLs
82
+ if any(domain in real_url for domain in ["duckduckgo.com", "duck.co"]):
83
+ continue
84
+
85
+ snippet = snippet_el.get_text(strip=True) if snippet_el else ""
86
+
87
+ if title and real_url:
88
+ results.append({
89
+ "title": title,
90
+ "url": real_url,
91
+ "snippet": snippet,
92
+ })
93
+
94
+ if len(results) >= num_results:
95
+ break
96
+
97
+ logger.info("DuckDuckGo search for '%s' returned %d results", query, len(results))
98
+ return results
99
+
100
+ except ImportError:
101
+ logger.warning("requests or beautifulsoup4 not installed for web search")
102
+ return []
103
+ except Exception as exc:
104
+ logger.warning("DuckDuckGo search failed: %s", exc)
105
+ return []
106
+
107
+
108
+ def _extract_ddg_url(href: str) -> str | None:
109
+ """Extract the real URL from a DuckDuckGo redirect link."""
110
+ if not href:
111
+ return None
112
+
113
+ # Direct HTTP URL
114
+ if href.startswith("http"):
115
+ return href
116
+
117
+ # DDG redirect: //duckduckgo.com/l/?uddg=<encoded_url>&...
118
+ if "uddg=" in href:
119
+ parsed = urllib.parse.urlparse(href)
120
+ params = urllib.parse.parse_qs(parsed.query)
121
+ uddg = params.get("uddg", [])
122
+ if uddg:
123
+ return urllib.parse.unquote(uddg[0])
124
+
125
+ # Sometimes it's a relative redirect
126
+ if href.startswith("//"):
127
+ return "https:" + href
128
+
129
+ return None
130
+
131
+
132
+ def _search_google(query: str, num_results: int) -> list[dict[str, str]]:
133
+ """Search Google by scraping the results page. Fallback method."""
134
+ try:
135
+ import requests
136
+ from bs4 import BeautifulSoup
137
+
138
+ encoded_query = urllib.parse.quote_plus(query)
139
+ url = f"https://www.google.com/search?q={encoded_query}&num={num_results + 2}&hl=en"
140
+
141
+ headers = {**_BROWSER_HEADERS, "Referer": "https://www.google.com/"}
142
+
143
+ session = requests.Session()
144
+ # First get a cookie from Google
145
+ session.get("https://www.google.com/", headers=headers, timeout=5)
146
+
147
+ resp = session.get(url, headers=headers, timeout=10, allow_redirects=True)
148
+ resp.raise_for_status()
149
+
150
+ soup = BeautifulSoup(resp.text, "html.parser")
151
+ results: list[dict[str, str]] = []
152
+
153
+ # Strategy 1: Modern Google layout β€” div.g > div.yuRUbf (title+link) + div.VwiC3b (snippet)
154
+ for g_div in soup.select("div.g"):
155
+ title_el = g_div.select_one("h3")
156
+ link_el = g_div.select_one("a[href]")
157
+ snippet_el = g_div.select_one("div.VwiC3b, span.aCOpRe, div[data-sncf]")
158
+
159
+ if not title_el or not link_el:
160
+ continue
161
+
162
+ href = link_el.get("href", "")
163
+ real_url = _extract_google_url(href)
164
+ if not real_url:
165
+ continue
166
+
167
+ title = title_el.get_text(strip=True)
168
+ snippet = snippet_el.get_text(strip=True) if snippet_el else ""
169
+
170
+ if title and real_url:
171
+ results.append({
172
+ "title": title,
173
+ "url": real_url,
174
+ "snippet": snippet,
175
+ })
176
+
177
+ if len(results) >= num_results:
178
+ break
179
+
180
+ # Strategy 2: Fallback β€” look for any <a> containing an <h3>
181
+ if not results:
182
+ for a_tag in soup.find_all("a", href=True):
183
+ h3 = a_tag.find("h3")
184
+ if not h3:
185
+ continue
186
+
187
+ href = a_tag.get("href", "")
188
+ real_url = _extract_google_url(href)
189
+ if not real_url:
190
+ continue
191
+
192
+ title = h3.get_text(strip=True)
193
+ # Try to find a sibling or nearby snippet
194
+ snippet = ""
195
+ parent = a_tag.parent
196
+ if parent:
197
+ for _ in range(3):
198
+ parent = parent.parent if parent else None
199
+ if parent:
200
+ snippet_el = parent.select_one("div.VwiC3b, span.aCOpRe, span.st")
201
+ if snippet_el:
202
+ snippet = snippet_el.get_text(strip=True)
203
+
204
+ if title and real_url:
205
+ results.append({
206
+ "title": title,
207
+ "url": real_url,
208
+ "snippet": snippet,
209
+ })
210
+
211
+ if len(results) >= num_results:
212
+ break
213
+
214
+ # Strategy 3: Last resort β€” any <a data-ved> with external href
215
+ if not results:
216
+ for a_tag in soup.select("a[data-ved]"):
217
+ href = a_tag.get("href", "")
218
+ if not href.startswith("http"):
219
+ continue
220
+ if "google.com" in href:
221
+ continue
222
+
223
+ title_el = a_tag.select_one("h3, span")
224
+ title = title_el.get_text(strip=True) if title_el else a_tag.get_text(strip=True)[:100]
225
+
226
+ if title and href:
227
+ results.append({
228
+ "title": title,
229
+ "url": href,
230
+ "snippet": "",
231
+ })
232
+
233
+ if len(results) >= num_results:
234
+ break
235
+
236
+ logger.info("Google search for '%s' returned %d results", query, len(results))
237
+ return results
238
+
239
+ except ImportError:
240
+ logger.warning("requests or beautifulsoup4 not installed for web search")
241
+ return []
242
+ except Exception as exc:
243
+ logger.warning("Google search failed: %s", exc)
244
+ return []
245
+
246
+
247
+ def _extract_google_url(href: str) -> str | None:
248
+ """Extract the real URL from a Google search result link."""
249
+ if not href:
250
+ return None
251
+
252
+ # Google redirect: /url?q=<real_url>&...
253
+ if href.startswith("/url?q="):
254
+ parsed = urllib.parse.urlparse(href)
255
+ params = urllib.parse.parse_qs(parsed.query)
256
+ q = params.get("q", [])
257
+ if q:
258
+ real_url = q[0]
259
+ if real_url.startswith("http"):
260
+ return real_url
261
+
262
+ # Direct HTTP URL
263
+ if href.startswith("http"):
264
+ # Skip Google-internal URLs
265
+ if any(domain in href for domain in [
266
+ "google.com", "googleusercontent.com",
267
+ "youtube.com", "gstatic.com",
268
+ ]):
269
+ return None
270
+ return href
271
+
272
+ return None
273
+
274
+
275
+ def format_search_results(results: list[dict[str, str]]) -> str:
276
+ """Format search results into a text block for model context."""
277
+ if not results:
278
+ return "No search results found."
279
+
280
+ parts = ["Here are the web search results for reference:\n"]
281
+ for i, r in enumerate(results, 1):
282
+ parts.append(f"{i}. {r['title']}")
283
+ parts.append(f" URL: {r['url']}")
284
+ if r["snippet"]:
285
+ parts.append(f" {r['snippet']}")
286
+ parts.append("")
287
+
288
+ return "\n".join(parts)
index.html ADDED
@@ -0,0 +1,2525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>SoniCoder</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
+ .btn-thinking {
192
+ background: transparent;
193
+ border: 1px solid var(--border);
194
+ color: var(--purple);
195
+ font-family: var(--font-mono);
196
+ font-size: 11px;
197
+ padding: 5px 12px;
198
+ border-radius: var(--radius);
199
+ cursor: pointer;
200
+ transition: all var(--transition);
201
+ letter-spacing: 0.5px;
202
+ }
203
+ .btn-thinking:hover {
204
+ border-color: var(--purple);
205
+ background: rgba(168,85,247,0.08);
206
+ text-shadow: var(--glow-purple);
207
+ }
208
+ .btn-thinking.active {
209
+ border-color: var(--purple);
210
+ background: rgba(168,85,247,0.15);
211
+ color: var(--purple);
212
+ text-shadow: var(--glow-purple);
213
+ }
214
+
215
+ #model-select {
216
+ background: var(--bg-deep);
217
+ border: 1px solid var(--border);
218
+ color: var(--cyan);
219
+ font-family: var(--font-mono);
220
+ font-size: 11px;
221
+ padding: 5px 8px;
222
+ border-radius: var(--radius);
223
+ outline: none;
224
+ cursor: pointer;
225
+ transition: border-color var(--transition);
226
+ }
227
+ #model-select:focus { border-color: var(--border-focus); }
228
+ #model-select option { background: var(--bg-deep); color: var(--gray-light); }
229
+
230
+ #btn-attach-image {
231
+ background: transparent; border: 1px solid var(--border); color: var(--amber);
232
+ font-family: var(--font-mono); font-size: 14px; padding: 3px 8px;
233
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
234
+ }
235
+ #btn-attach-image:hover {
236
+ border-color: var(--amber); background: rgba(255,179,0,0.1);
237
+ }
238
+
239
+ /* ═══════════════════════════════════════════════════════
240
+ BANNER
241
+ ═══════════════════════════════════════════════════════ */
242
+ #playground-banner {
243
+ background: linear-gradient(90deg, rgba(168,85,247,0.08), rgba(57,255,20,0.05));
244
+ border-bottom: 1px solid var(--border);
245
+ color: var(--gray-mid);
246
+ font-size: 12px;
247
+ padding: 7px 18px;
248
+ text-align: center;
249
+ flex-shrink: 0;
250
+ }
251
+ #playground-banner strong { color: var(--gray-light); font-weight: 600; }
252
+ #playground-banner a { color: var(--purple); font-weight: 600; }
253
+
254
+ /* ═══════════════════════════════════════════════════════
255
+ MAIN LAYOUT
256
+ ═══════════════════════════════════════════════════════ */
257
+ #main {
258
+ display: flex;
259
+ flex: 1;
260
+ min-height: 0;
261
+ overflow: hidden;
262
+ }
263
+
264
+ /* ═══════════════════════════════════════════════════════
265
+ TERMINAL (LEFT PANEL)
266
+ ═══════════════════════════════════════════════════════ */
267
+ #terminal-panel {
268
+ display: flex;
269
+ flex-direction: column;
270
+ flex: 1;
271
+ min-width: 0;
272
+ border-right: 1px solid var(--border);
273
+ }
274
+
275
+ .panel-label {
276
+ padding: 6px 16px;
277
+ font-size: 10px;
278
+ letter-spacing: 2px;
279
+ color: var(--gray-dim);
280
+ border-bottom: 1px solid var(--border);
281
+ background: rgba(13,17,23,0.6);
282
+ text-transform: uppercase;
283
+ flex-shrink: 0;
284
+ }
285
+
286
+ #chat-messages {
287
+ flex: 1;
288
+ overflow-y: auto;
289
+ padding: 16px;
290
+ scroll-behavior: smooth;
291
+ }
292
+
293
+ /* Message styles */
294
+ .msg {
295
+ margin-bottom: 14px;
296
+ line-height: 1.65;
297
+ animation: msgFadeIn 0.25s ease;
298
+ }
299
+
300
+ @keyframes msgFadeIn {
301
+ from { opacity: 0; transform: translateY(6px); }
302
+ to { opacity: 1; transform: translateY(0); }
303
+ }
304
+
305
+ .msg-prefix { font-weight: 600; margin-right: 4px; }
306
+ .msg-user .msg-prefix { color: var(--green); text-shadow: var(--glow-green); }
307
+ .msg-user .msg-content { color: var(--green); text-shadow: var(--glow-green); }
308
+ .msg-assistant .msg-prefix { color: var(--cyan); text-shadow: var(--glow-cyan); float: left; }
309
+ .msg-assistant .msg-body { overflow: hidden; }
310
+ .msg-assistant .msg-content { color: var(--gray-light); }
311
+ .msg-system .msg-prefix { color: var(--amber); text-shadow: var(--glow-amber); }
312
+ .msg-system .msg-content { color: var(--amber); opacity: 0.85; }
313
+
314
+ /* Markdown elements */
315
+ .msg-content strong { color: #fff; font-weight: 600; }
316
+ .msg-content em { font-style: italic; color: var(--gray-mid); }
317
+ .msg-content code:not(pre code) {
318
+ background: var(--bg-code);
319
+ color: var(--code-text);
320
+ padding: 1px 5px;
321
+ border-radius: 3px;
322
+ font-size: 12px;
323
+ border: 1px solid var(--border);
324
+ }
325
+ .msg-content a { color: var(--cyan); }
326
+ .msg-content ul, .msg-content ol { margin: 6px 0 6px 20px; }
327
+ .msg-content li { margin-bottom: 3px; }
328
+ .msg-content h1, .msg-content h2, .msg-content h3 {
329
+ color: var(--cyan); margin: 10px 0 6px; font-size: 14px; text-shadow: var(--glow-cyan);
330
+ }
331
+ .msg-content h1 { font-size: 16px; }
332
+ .msg-content h2 { font-size: 15px; }
333
+ .msg-content p { margin: 4px 0; }
334
+
335
+ /* Code blocks */
336
+ .code-block-wrap {
337
+ margin: 8px 0;
338
+ border: 1px solid var(--border);
339
+ border-radius: var(--radius);
340
+ overflow: hidden;
341
+ background: var(--bg-code);
342
+ }
343
+ .code-block-header {
344
+ display: flex; align-items: center; justify-content: space-between;
345
+ padding: 4px 10px; background: rgba(30,42,58,0.5);
346
+ border-bottom: 1px solid var(--border); font-size: 11px;
347
+ }
348
+ .code-lang { color: var(--amber); text-transform: uppercase; letter-spacing: 1px; }
349
+ .btn-copy {
350
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
351
+ font-family: var(--font-mono); font-size: 10px; padding: 2px 8px;
352
+ border-radius: 3px; cursor: pointer; transition: all var(--transition);
353
+ }
354
+ .btn-copy:hover { border-color: var(--green); color: var(--green); }
355
+ .btn-copy.copied { border-color: var(--success); color: var(--success); }
356
+ .code-block-wrap pre {
357
+ margin: 0; padding: 10px 12px; overflow-x: auto;
358
+ font-size: 12px; line-height: 1.5; color: var(--code-text);
359
+ }
360
+ .code-block-wrap pre code { font-family: var(--font-mono); background: none; border: none; padding: 0; }
361
+
362
+ /* Thinking blocks */
363
+ .think-block {
364
+ margin: 8px 0; border: 1px solid rgba(255,179,0,0.15);
365
+ border-radius: var(--radius); background: rgba(255,179,0,0.03);
366
+ }
367
+ .think-summary {
368
+ display: block; width: 100%; background: transparent; border: none;
369
+ padding: 6px 10px; cursor: pointer; font-size: 12px;
370
+ font-family: var(--font-mono); text-align: left; color: var(--gray-dim);
371
+ user-select: none; transition: color var(--transition);
372
+ }
373
+ .think-summary:hover { color: var(--amber); }
374
+ .think-block .think-content {
375
+ padding: 6px 12px 10px; font-size: 12px; color: var(--gray-dim);
376
+ line-height: 1.55; border-top: 1px solid rgba(255,179,0,0.1);
377
+ }
378
+ .think-block:not(.open) .think-content { display: none; }
379
+
380
+ /* Hide thinking blocks entirely when toggle is off */
381
+ body.hide-thinking .think-block { display: none; }
382
+
383
+ /* ─── Search Source Badge (Grok-style) ────────────────────── */
384
+ .search-source-badge {
385
+ display: inline-flex;
386
+ align-items: center;
387
+ gap: 5px;
388
+ margin: 4px 0 2px;
389
+ padding: 4px 10px;
390
+ border-radius: 20px;
391
+ background: rgba(168, 85, 247, 0.08);
392
+ border: 1px solid rgba(168, 85, 247, 0.2);
393
+ cursor: pointer;
394
+ transition: all 0.2s ease;
395
+ font-size: 11px;
396
+ color: var(--purple);
397
+ user-select: none;
398
+ white-space: nowrap;
399
+ }
400
+ .search-source-badge:hover {
401
+ background: rgba(168, 85, 247, 0.15);
402
+ border-color: rgba(168, 85, 247, 0.4);
403
+ box-shadow: 0 0 8px rgba(168, 85, 247, 0.15);
404
+ }
405
+ .search-source-badge .badge-icon {
406
+ font-size: 13px;
407
+ line-height: 1;
408
+ }
409
+ .search-source-badge .badge-count {
410
+ font-weight: 600;
411
+ }
412
+ .search-source-badge .badge-arrow {
413
+ font-size: 9px;
414
+ transition: transform 0.2s ease;
415
+ opacity: 0.6;
416
+ }
417
+ .search-source-badge.open .badge-arrow {
418
+ transform: rotate(180deg);
419
+ }
420
+
421
+ .search-source-panel {
422
+ margin: 0 0 6px;
423
+ border-radius: 8px;
424
+ border: 1px solid rgba(168, 85, 247, 0.15);
425
+ background: rgba(168, 85, 247, 0.03);
426
+ max-height: 0;
427
+ overflow: hidden;
428
+ transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
429
+ opacity: 0;
430
+ }
431
+ .search-source-panel.open {
432
+ max-height: 600px;
433
+ overflow-y: auto;
434
+ opacity: 1;
435
+ padding: 8px 10px;
436
+ }
437
+ .search-source-panel::-webkit-scrollbar { width: 4px; }
438
+ .search-source-panel::-webkit-scrollbar-track { background: transparent; }
439
+ .search-source-panel::-webkit-scrollbar-thumb { background: var(--gray-dim); border-radius: 2px; }
440
+
441
+ .source-item {
442
+ display: flex;
443
+ gap: 8px;
444
+ padding: 6px 4px;
445
+ border-bottom: 1px solid rgba(168, 85, 247, 0.07);
446
+ align-items: flex-start;
447
+ }
448
+ .source-item:last-child { border-bottom: none; }
449
+ .source-favicon {
450
+ width: 16px; height: 16px;
451
+ border-radius: 3px;
452
+ flex-shrink: 0;
453
+ margin-top: 2px;
454
+ background: var(--bg-code);
455
+ border: 1px solid var(--border);
456
+ object-fit: contain;
457
+ }
458
+ .source-favicon-placeholder {
459
+ width: 16px; height: 16px;
460
+ border-radius: 3px;
461
+ flex-shrink: 0;
462
+ margin-top: 2px;
463
+ background: var(--bg-code);
464
+ border: 1px solid var(--border);
465
+ display: flex; align-items: center; justify-content: center;
466
+ font-size: 8px; color: var(--gray-dim);
467
+ }
468
+ .source-info { flex: 1; min-width: 0; }
469
+ .source-title {
470
+ font-size: 11px; font-weight: 600; color: var(--cyan);
471
+ text-decoration: none; display: block;
472
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
473
+ }
474
+ .source-title:hover { text-decoration: underline; }
475
+ .source-domain {
476
+ font-size: 9px; color: var(--green-dim);
477
+ display: block; margin-top: 1px;
478
+ }
479
+ .source-snippet {
480
+ font-size: 10px; color: var(--gray-mid);
481
+ line-height: 1.35;
482
+ margin-top: 2px;
483
+ display: -webkit-box;
484
+ -webkit-line-clamp: 2;
485
+ -webkit-box-orient: vertical;
486
+ overflow: hidden;
487
+ }
488
+
489
+ /* Streaming cursor */
490
+ .streaming-cursor::after {
491
+ content: '\u2588'; animation: blink 0.8s step-end infinite;
492
+ color: var(--green); margin-left: 2px;
493
+ }
494
+ @keyframes blink { 50% { opacity: 0; } }
495
+
496
+ /* ═══════════════════════════════════════════════════════
497
+ INPUT AREA
498
+ ═══════════════════════════════════════════════════════ */
499
+ #input-area {
500
+ flex-shrink: 0;
501
+ border-top: 1px solid var(--border);
502
+ background: var(--bg-panel);
503
+ padding: 10px 16px 8px;
504
+ }
505
+
506
+ /* Target selectors */
507
+ #target-selector {
508
+ display: flex;
509
+ gap: 10px;
510
+ margin-bottom: 6px;
511
+ align-items: center;
512
+ flex-wrap: wrap;
513
+ }
514
+
515
+ .selector-group {
516
+ display: flex;
517
+ align-items: center;
518
+ gap: 6px;
519
+ }
520
+
521
+ .selector-label {
522
+ font-size: 10px;
523
+ color: var(--gray-dim);
524
+ letter-spacing: 1px;
525
+ text-transform: uppercase;
526
+ }
527
+
528
+ #lang-select, #framework-select {
529
+ background: var(--bg-deep);
530
+ border: 1px solid var(--border);
531
+ color: var(--green);
532
+ font-family: var(--font-mono);
533
+ font-size: 11px;
534
+ padding: 3px 8px;
535
+ border-radius: var(--radius);
536
+ outline: none;
537
+ cursor: pointer;
538
+ transition: border-color var(--transition);
539
+ }
540
+ #lang-select:focus, #framework-select:focus {
541
+ border-color: var(--border-focus);
542
+ }
543
+ #lang-select option, #framework-select option {
544
+ background: var(--bg-deep);
545
+ color: var(--gray-light);
546
+ }
547
+
548
+ #input-row {
549
+ display: flex;
550
+ gap: 8px;
551
+ align-items: flex-end;
552
+ }
553
+
554
+ .input-prompt-symbol {
555
+ color: var(--green);
556
+ font-weight: 700;
557
+ font-size: 14px;
558
+ line-height: 36px;
559
+ text-shadow: var(--glow-green);
560
+ flex-shrink: 0;
561
+ }
562
+
563
+ #chat-input {
564
+ flex: 1;
565
+ background: var(--bg-deep);
566
+ border: 1px solid var(--border);
567
+ border-radius: var(--radius);
568
+ color: var(--green);
569
+ font-family: var(--font-mono);
570
+ font-size: 13px;
571
+ padding: 8px 12px;
572
+ resize: none;
573
+ outline: none;
574
+ min-height: 36px;
575
+ max-height: 120px;
576
+ line-height: 1.5;
577
+ transition: border-color var(--transition);
578
+ caret-color: var(--green);
579
+ text-shadow: var(--glow-green);
580
+ }
581
+ #chat-input::placeholder { color: var(--gray-dim); text-shadow: none; }
582
+ #chat-input:focus { border-color: var(--border-focus); }
583
+
584
+ #btn-web-search {
585
+ background: transparent; border: 1px solid var(--purple); color: var(--purple);
586
+ font-family: var(--font-mono); font-size: 11px; padding: 8px 10px;
587
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
588
+ letter-spacing: 1px; flex-shrink: 0; height: 36px;
589
+ display: flex; align-items: center; gap: 4px;
590
+ }
591
+ #btn-web-search:hover {
592
+ background: var(--purple); color: white;
593
+ box-shadow: 0 0 12px rgba(168,85,247,0.3);
594
+ text-shadow: none;
595
+ }
596
+
597
+ #btn-send, #btn-stop {
598
+ font-family: var(--font-mono); font-size: 12px; padding: 8px 14px;
599
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
600
+ letter-spacing: 1px; flex-shrink: 0; height: 36px;
601
+ display: flex; align-items: center; gap: 4px;
602
+ }
603
+
604
+ #btn-send {
605
+ background: transparent; border: 1px solid var(--green); color: var(--green);
606
+ }
607
+ #btn-send:hover:not(:disabled) {
608
+ background: var(--green); color: var(--bg-deep);
609
+ box-shadow: 0 0 12px rgba(57,255,20,0.3);
610
+ }
611
+ #btn-send:disabled { opacity: 0.3; cursor: not-allowed; }
612
+
613
+ #btn-stop {
614
+ background: transparent; border: 1px solid var(--red); color: var(--red); display: none;
615
+ }
616
+ #btn-stop:hover {
617
+ background: var(--red); color: var(--bg-deep);
618
+ box-shadow: 0 0 12px rgba(255,85,85,0.3);
619
+ }
620
+
621
+ /* Examples */
622
+ #examples-row {
623
+ display: flex; align-items: center; gap: 8px;
624
+ margin-top: 8px; flex-wrap: wrap;
625
+ }
626
+ .examples-label {
627
+ font-size: 10px; color: var(--gray-dim); letter-spacing: 1px;
628
+ text-transform: uppercase; flex-shrink: 0;
629
+ }
630
+ .example-chip {
631
+ background: rgba(30,42,58,0.4); border: 1px solid var(--border);
632
+ border-radius: 12px; padding: 3px 10px; font-family: var(--font-mono);
633
+ font-size: 11px; color: var(--gray-mid); cursor: pointer;
634
+ transition: all var(--transition); white-space: nowrap;
635
+ }
636
+ .example-chip:hover {
637
+ border-color: var(--purple); color: var(--purple);
638
+ background: rgba(168,85,247,0.05); text-shadow: var(--glow-purple);
639
+ }
640
+
641
+ /* ═══════════════════════════════════════════════════════
642
+ OUTPUT PANEL (RIGHT)
643
+ ═══════════════════════════════════════════════════════ */
644
+ #output-panel {
645
+ display: flex; flex-direction: column; width: 45%; min-width: 340px;
646
+ max-width: 55%; min-height: 0; background: var(--bg-panel);
647
+ }
648
+
649
+ #output-tabs {
650
+ display: flex; border-bottom: 1px solid var(--border);
651
+ background: rgba(13,17,23,0.6); flex-shrink: 0;
652
+ }
653
+
654
+ .output-tab {
655
+ flex: 1; background: transparent; border: none;
656
+ border-bottom: 2px solid transparent; color: var(--gray-dim);
657
+ font-family: var(--font-mono); font-size: 11px; padding: 8px 12px;
658
+ cursor: pointer; transition: all var(--transition);
659
+ letter-spacing: 1px; text-transform: uppercase;
660
+ }
661
+ .output-tab:hover { color: var(--gray-mid); }
662
+ .output-tab.active {
663
+ color: var(--cyan); border-bottom-color: var(--cyan);
664
+ text-shadow: var(--glow-cyan);
665
+ }
666
+
667
+ #output-content {
668
+ flex: 1; min-height: 0; overflow: hidden; position: relative;
669
+ }
670
+
671
+ /* Tab panes */
672
+ .tab-pane { display: none; height: 100%; min-height: 0; }
673
+ .tab-pane.active { display: flex; flex-direction: column; }
674
+
675
+ /* Preview tab */
676
+ #pane-preview {
677
+ align-items: stretch; justify-content: stretch;
678
+ position: relative; min-height: 0; overflow: hidden;
679
+ }
680
+
681
+ .preview-placeholder {
682
+ align-self: center; margin: auto; text-align: center;
683
+ color: var(--gray-dim); padding: 40px 20px;
684
+ }
685
+ .preview-placeholder .ascii-art {
686
+ font-size: 11px; line-height: 1.3; margin-bottom: 16px; color: var(--border-focus);
687
+ }
688
+ .preview-placeholder .placeholder-text { font-size: 12px; letter-spacing: 0.5px; }
689
+
690
+ #preview-image {
691
+ display: none; max-width: 100%; max-height: 100%;
692
+ object-fit: contain; padding: 12px;
693
+ }
694
+
695
+ #preview-iframe {
696
+ display: none; position: absolute; inset: 0; width: 100%; height: 100%;
697
+ min-height: 0; border: none; background: #fff;
698
+ }
699
+
700
+ #btn-fullscreen {
701
+ display: none; position: absolute; top: 8px; right: 8px;
702
+ background: rgba(13,17,23,0.8); border: 1px solid var(--border);
703
+ color: var(--gray-mid); font-family: var(--font-mono); font-size: 11px;
704
+ padding: 4px 10px; border-radius: var(--radius); cursor: pointer;
705
+ z-index: 5; transition: all var(--transition);
706
+ }
707
+ #btn-fullscreen:hover { border-color: var(--cyan); color: var(--cyan); }
708
+
709
+ /* Console tab */
710
+ #pane-console { padding: 12px 16px; gap: 12px; overflow-y: auto; }
711
+
712
+ .console-section { margin-bottom: 8px; }
713
+ .console-label {
714
+ font-size: 10px; letter-spacing: 2px; color: var(--gray-dim);
715
+ margin-bottom: 4px; text-transform: uppercase;
716
+ }
717
+ .console-output {
718
+ background: var(--bg-deep); border: 1px solid var(--border);
719
+ border-radius: var(--radius); padding: 10px 12px; font-size: 12px;
720
+ line-height: 1.5; white-space: pre-wrap; word-break: break-word;
721
+ min-height: 40px; max-height: 280px; overflow-y: auto;
722
+ }
723
+ #console-stdout { color: var(--success); }
724
+ #console-stderr { color: var(--red); }
725
+
726
+ /* Code tab */
727
+ #pane-code { padding: 0; }
728
+
729
+ .code-tab-header {
730
+ display: flex; align-items: center; justify-content: space-between;
731
+ padding: 8px 12px; border-bottom: 1px solid var(--border);
732
+ background: rgba(30,42,58,0.3); flex-shrink: 0;
733
+ }
734
+ .code-tab-lang {
735
+ font-size: 11px; color: var(--amber); letter-spacing: 1px; text-transform: uppercase;
736
+ }
737
+ .code-tab-actions { display: flex; gap: 8px; }
738
+ .code-tab-btn {
739
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
740
+ font-family: var(--font-mono); font-size: 10px; padding: 3px 8px;
741
+ border-radius: 3px; cursor: pointer; text-decoration: none;
742
+ transition: all var(--transition); display: inline-flex;
743
+ align-items: center; gap: 4px;
744
+ }
745
+ .code-tab-btn:hover { border-color: var(--cyan); color: var(--cyan); text-decoration: none; }
746
+
747
+ #code-display {
748
+ flex: 1; overflow: auto; padding: 12px; background: var(--bg-code);
749
+ }
750
+ #code-display pre { margin: 0; font-size: 12px; line-height: 1.5; color: var(--code-text); }
751
+
752
+ .code-placeholder {
753
+ display: flex; align-items: center; justify-content: center;
754
+ height: 100%; color: var(--gray-dim); font-size: 12px;
755
+ }
756
+
757
+ /* ═══════════════════════════════════════════════════════
758
+ SEARCH TAB
759
+ ═══════════════════════════════════════════════════════ */
760
+ #pane-search { padding: 12px 16px; gap: 12px; overflow-y: auto; }
761
+
762
+ .search-bar {
763
+ display: flex;
764
+ gap: 8px;
765
+ margin-bottom: 10px;
766
+ }
767
+ #search-input {
768
+ flex: 1;
769
+ background: var(--bg-deep);
770
+ border: 1px solid var(--border);
771
+ color: var(--green);
772
+ font-family: var(--font-mono);
773
+ font-size: 12px;
774
+ padding: 6px 10px;
775
+ border-radius: var(--radius);
776
+ outline: none;
777
+ transition: border-color var(--transition);
778
+ }
779
+ #search-input:focus { border-color: var(--border-focus); }
780
+ #search-input::placeholder { color: var(--gray-dim); }
781
+
782
+ #btn-search-go {
783
+ background: transparent;
784
+ border: 1px solid var(--purple);
785
+ color: var(--purple);
786
+ font-family: var(--font-mono);
787
+ font-size: 11px;
788
+ padding: 6px 12px;
789
+ border-radius: var(--radius);
790
+ cursor: pointer;
791
+ transition: all var(--transition);
792
+ letter-spacing: 1px;
793
+ }
794
+ #btn-search-go:hover {
795
+ background: var(--purple);
796
+ color: white;
797
+ text-shadow: none;
798
+ }
799
+
800
+ .search-result-item {
801
+ padding: 8px 0;
802
+ border-bottom: 1px solid var(--border);
803
+ }
804
+ .search-result-item:last-child { border-bottom: none; }
805
+ .search-result-title {
806
+ color: var(--cyan);
807
+ font-size: 12px;
808
+ font-weight: 600;
809
+ text-decoration: none;
810
+ display: block;
811
+ margin-bottom: 2px;
812
+ }
813
+ .search-result-title:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
814
+ .search-result-url {
815
+ color: var(--green-dim);
816
+ font-size: 10px;
817
+ display: block;
818
+ margin-bottom: 2px;
819
+ word-break: break-all;
820
+ }
821
+ .search-result-snippet {
822
+ color: var(--gray-mid);
823
+ font-size: 11px;
824
+ line-height: 1.4;
825
+ }
826
+ .search-results-empty {
827
+ color: var(--gray-dim);
828
+ font-size: 12px;
829
+ text-align: center;
830
+ padding: 40px 20px;
831
+ }
832
+
833
+ /* Gradio preview */
834
+ .gradio-badge {
835
+ display: inline-flex;
836
+ align-items: center;
837
+ gap: 4px;
838
+ padding: 3px 8px;
839
+ background: rgba(168,85,247,0.15);
840
+ border: 1px solid var(--purple);
841
+ border-radius: 10px;
842
+ font-size: 10px;
843
+ color: var(--purple);
844
+ letter-spacing: 0.5px;
845
+ }
846
+
847
+ #gradio-iframe {
848
+ display: none;
849
+ position: absolute;
850
+ inset: 0;
851
+ width: 100%;
852
+ height: 100%;
853
+ min-height: 0;
854
+ border: none;
855
+ }
856
+
857
+ /* ═══════════════════════════════════════════════════════
858
+ DEPLOY TAB
859
+ ═══════════════════════════════════════════════════════ */
860
+ #pane-deploy { padding: 16px; gap: 14px; overflow-y: auto; }
861
+
862
+ .deploy-section {
863
+ border: 1px solid var(--border);
864
+ border-radius: var(--radius);
865
+ padding: 14px;
866
+ background: var(--bg-code);
867
+ }
868
+
869
+ .deploy-title {
870
+ font-size: 12px;
871
+ font-weight: 600;
872
+ color: var(--purple);
873
+ text-shadow: var(--glow-purple);
874
+ margin-bottom: 10px;
875
+ letter-spacing: 1px;
876
+ text-transform: uppercase;
877
+ }
878
+
879
+ .deploy-field {
880
+ margin-bottom: 10px;
881
+ }
882
+
883
+ .deploy-field label {
884
+ display: block;
885
+ font-size: 10px;
886
+ color: var(--gray-dim);
887
+ letter-spacing: 1px;
888
+ text-transform: uppercase;
889
+ margin-bottom: 4px;
890
+ }
891
+
892
+ .deploy-field input, .deploy-field select {
893
+ width: 100%;
894
+ background: var(--bg-deep);
895
+ border: 1px solid var(--border);
896
+ color: var(--green);
897
+ font-family: var(--font-mono);
898
+ font-size: 12px;
899
+ padding: 6px 10px;
900
+ border-radius: var(--radius);
901
+ outline: none;
902
+ transition: border-color var(--transition);
903
+ }
904
+ .deploy-field input:focus, .deploy-field select:focus {
905
+ border-color: var(--border-focus);
906
+ }
907
+ .deploy-field input::placeholder {
908
+ color: var(--gray-dim);
909
+ }
910
+ .deploy-field select option {
911
+ background: var(--bg-deep);
912
+ color: var(--gray-light);
913
+ }
914
+
915
+ .deploy-hint {
916
+ font-size: 10px;
917
+ color: var(--gray-dim);
918
+ margin-top: 3px;
919
+ }
920
+
921
+ #btn-push-hf {
922
+ width: 100%;
923
+ background: linear-gradient(135deg, rgba(168,85,247,0.2), rgba(57,255,20,0.1));
924
+ border: 1px solid var(--purple);
925
+ color: var(--purple);
926
+ font-family: var(--font-mono);
927
+ font-size: 12px;
928
+ padding: 8px 14px;
929
+ border-radius: var(--radius);
930
+ cursor: pointer;
931
+ transition: all var(--transition);
932
+ letter-spacing: 1px;
933
+ margin-top: 6px;
934
+ }
935
+ #btn-push-hf:hover:not(:disabled) {
936
+ background: var(--purple);
937
+ color: white;
938
+ box-shadow: 0 0 12px rgba(168,85,247,0.4);
939
+ text-shadow: none;
940
+ }
941
+ #btn-push-hf:disabled {
942
+ opacity: 0.4;
943
+ cursor: not-allowed;
944
+ }
945
+
946
+ .deploy-status {
947
+ margin-top: 10px;
948
+ padding: 8px 12px;
949
+ border-radius: var(--radius);
950
+ font-size: 11px;
951
+ display: none;
952
+ }
953
+ .deploy-status.success {
954
+ display: block;
955
+ background: rgba(80,250,123,0.1);
956
+ border: 1px solid var(--success);
957
+ color: var(--success);
958
+ }
959
+ .deploy-status.error {
960
+ display: block;
961
+ background: rgba(255,85,85,0.1);
962
+ border: 1px solid var(--red);
963
+ color: var(--red);
964
+ }
965
+ .deploy-status.working {
966
+ display: block;
967
+ background: rgba(255,179,0,0.1);
968
+ border: 1px solid var(--amber);
969
+ color: var(--amber);
970
+ }
971
+
972
+ .deploy-status a {
973
+ color: var(--cyan);
974
+ font-weight: 600;
975
+ }
976
+
977
+ /* Project files list */
978
+ .project-files {
979
+ margin-top: 10px;
980
+ }
981
+
982
+ .file-item {
983
+ display: flex;
984
+ align-items: center;
985
+ gap: 6px;
986
+ padding: 4px 0;
987
+ font-size: 11px;
988
+ color: var(--gray-mid);
989
+ border-bottom: 1px solid rgba(30,42,58,0.5);
990
+ }
991
+ .file-item:last-child { border-bottom: none; }
992
+ .file-icon { color: var(--amber); }
993
+ .file-name { color: var(--cyan); }
994
+
995
+ /* ═══════════════════════════════════════════════════════
996
+ STATUS BAR
997
+ ═══════════════════════════════════════════════════════ */
998
+ #status-bar {
999
+ display: flex; align-items: center; gap: 8px; padding: 5px 16px;
1000
+ border-top: 1px solid var(--border); background: var(--bg-panel);
1001
+ font-size: 11px; flex-shrink: 0;
1002
+ }
1003
+
1004
+ .status-indicator {
1005
+ display: inline-flex; align-items: center; gap: 6px;
1006
+ }
1007
+ .status-dot { font-size: 10px; line-height: 1; }
1008
+ #status-text { letter-spacing: 1px; text-transform: uppercase; }
1009
+
1010
+ .status-idle { color: var(--gray-dim); }
1011
+ .status-working { color: var(--amber); text-shadow: var(--glow-amber); }
1012
+ .status-success { color: var(--success); text-shadow: 0 0 8px rgba(80,250,123,0.3); }
1013
+ .status-error { color: var(--red); text-shadow: 0 0 8px rgba(255,85,85,0.3); }
1014
+ .status-info { color: var(--cyan); text-shadow: var(--glow-cyan); }
1015
+
1016
+ @keyframes spin { to { transform: rotate(360deg); } }
1017
+ .status-working .status-dot { display: inline-block; animation: spin 1s linear infinite; }
1018
+
1019
+ /* ═══════════════════════════════════════════════════════
1020
+ FULLSCREEN OVERLAY
1021
+ ═══════════════════════════════════════════════════════ */
1022
+ #fullscreen-overlay {
1023
+ display: none; position: fixed; inset: 0; z-index: 1000;
1024
+ background: var(--bg-deep); flex-direction: column;
1025
+ }
1026
+ #fullscreen-overlay.active { display: flex; }
1027
+
1028
+ #fullscreen-bar {
1029
+ display: flex; align-items: center; justify-content: space-between;
1030
+ padding: 8px 16px; border-bottom: 1px solid var(--border); background: var(--bg-panel);
1031
+ }
1032
+ #fullscreen-bar span { color: var(--cyan); font-size: 12px; letter-spacing: 1px; }
1033
+ #btn-exit-fullscreen {
1034
+ background: transparent; border: 1px solid var(--border); color: var(--gray-mid);
1035
+ font-family: var(--font-mono); font-size: 11px; padding: 4px 12px;
1036
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
1037
+ }
1038
+ #btn-exit-fullscreen:hover { border-color: var(--red); color: var(--red); }
1039
+ #fullscreen-iframe { flex: 1; border: none; background: #fff; }
1040
+
1041
+ /* ═══════════════════════════════════════════════════════
1042
+ RESPONSIVE
1043
+ ═══════════════════════════════════════════════════════ */
1044
+ @media (max-width: 900px) {
1045
+ #main { flex-direction: column; }
1046
+ #terminal-panel { border-right: none; border-bottom: 1px solid var(--border); max-height: 55vh; }
1047
+ #output-panel { width: 100%; max-width: 100%; min-width: 0; flex: 1; }
1048
+ .header-ascii { font-size: 10px; }
1049
+ #chat-input { font-size: 12px; }
1050
+ #preview-iframe { min-height: 400px; }
1051
+ }
1052
+
1053
+ @media (max-width: 600px) {
1054
+ #header { padding: 8px 12px; gap: 8px; }
1055
+ .header-ascii { display: none; }
1056
+ .header-subtitle { display: none; }
1057
+ .pill { font-size: 10px; padding: 3px 8px; }
1058
+ #chat-messages { padding: 10px; }
1059
+ #input-area { padding: 8px 10px 6px; }
1060
+ #examples-row { display: none; }
1061
+ #target-selector { gap: 6px; }
1062
+ }
1063
+ </style>
1064
+ </head>
1065
+ <body>
1066
+ <div id="app">
1067
+ <!-- Header -->
1068
+ <header id="header">
1069
+ <div class="header-title">
1070
+ <div class="header-ascii">&#9556;&#9552;&#9552;&#9552; FULLSTACK CODE BUILDER &#9552;&#9552;&#9552;&#9562;</div>
1071
+ <div class="header-subtitle">Local AI App Generator | <span id="header-model-name">MiniCPM5-1B</span></div>
1072
+ </div>
1073
+ <div class="header-actions">
1074
+ <a class="pill" id="model-pill" href="#" target="_blank" rel="noopener">
1075
+ <span class="dot loading" id="model-dot"></span>
1076
+ <span id="model-pill-text">MiniCPM5-1B</span>
1077
+ </a>
1078
+ <select id="model-select" onchange="onModelChange()" title="Switch AI model">
1079
+ <option value="minicpm5-1b">MiniCPM5-1B (text)</option>
1080
+ <option value="minicpm-v-4.6">MiniCPM-V-4.6 (vision)</option>
1081
+ </select>
1082
+ <button id="btn-thinking" class="btn-thinking active" onclick="toggleThinking()" title="Show/hide thinking blocks">🧠 Think</button>
1083
+ <button id="btn-new-chat" onclick="newChat()" title="Start a new chat session">[NEW]</button>
1084
+ </div>
1085
+ </header>
1086
+
1087
+ <div id="playground-banner">
1088
+ 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 &mdash; no external APIs. Generate fullstack apps in any language and deploy to HuggingFace.
1089
+ </div>
1090
+
1091
+ <!-- Main Layout -->
1092
+ <div id="main">
1093
+ <!-- Terminal Panel -->
1094
+ <div id="terminal-panel">
1095
+ <div class="panel-label">Terminal</div>
1096
+ <div id="chat-messages"></div>
1097
+ <div id="input-area">
1098
+ <div id="target-selector">
1099
+ <div class="selector-group">
1100
+ <span class="selector-label">Lang:</span>
1101
+ <select id="lang-select" onchange="onLanguageChange()"></select>
1102
+ </div>
1103
+ <div class="selector-group">
1104
+ <span class="selector-label">Framework:</span>
1105
+ <select id="framework-select"></select>
1106
+ </div>
1107
+ <div class="selector-group" id="image-attach-group" style="display:none;">
1108
+ <input type="file" id="image-upload" accept="image/*" style="display:none" onchange="onImageUpload(event)">
1109
+ <button id="btn-attach-image" onclick="document.getElementById('image-upload').click()" title="Attach image (VLM only)">πŸ“·</button>
1110
+ <span id="image-attach-name" style="font-size:10px;color:var(--gray-dim);max-width:100px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"></span>
1111
+ <button id="btn-remove-image" onclick="removeImage()" title="Remove image" style="display:none;font-size:10px;color:var(--red);background:none;border:none;cursor:pointer;">βœ•</button>
1112
+ </div>
1113
+ </div>
1114
+ <div id="input-row">
1115
+ <span class="input-prompt-symbol">&#10095;</span>
1116
+ <textarea id="chat-input" rows="1" placeholder="Describe the app you want to build..." spellcheck="false"></textarea>
1117
+ <button id="btn-web-search" onclick="searchAndGenerate()" title="Search web + Generate">&#128269;</button>
1118
+ <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">&#10148;</button>
1119
+ <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
1120
+ </div>
1121
+ <div id="examples-row"></div>
1122
+ </div>
1123
+ </div>
1124
+
1125
+ <!-- Output Panel -->
1126
+ <div id="output-panel">
1127
+ <div id="output-tabs">
1128
+ <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
1129
+ <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
1130
+ <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
1131
+ <button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button>
1132
+ <button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
1133
+ </div>
1134
+ <div id="output-content">
1135
+ <!-- Preview Pane -->
1136
+ <div class="tab-pane active" id="pane-preview">
1137
+ <div class="preview-placeholder" id="preview-placeholder">
1138
+ <div class="ascii-art">
1139
+ &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
1140
+ &#9474; &#9585;&#9473;&#9473;&#9473;&#9586; &#9474;
1141
+ &#9474; &#9474; &#9654; &#9474; OUTPUT &#9474;
1142
+ &#9474; &#9589;&#9473;&#9473;&#9473;&#9588; &#9474;
1143
+ &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</div>
1144
+ <div class="placeholder-text">Generate code to see output here</div>
1145
+ </div>
1146
+ <img id="preview-image" alt="Generated output">
1147
+ <iframe id="preview-iframe" sandbox="allow-scripts"></iframe>
1148
+ <button id="btn-fullscreen" onclick="openFullscreen()">&#10570; FULLSCREEN</button>
1149
+ </div>
1150
+
1151
+ <!-- Console Pane -->
1152
+ <div class="tab-pane" id="pane-console">
1153
+ <div class="console-section">
1154
+ <div class="console-label">stdout:</div>
1155
+ <div class="console-output" id="console-stdout">No output yet.</div>
1156
+ </div>
1157
+ <div class="console-section">
1158
+ <div class="console-label">stderr:</div>
1159
+ <div class="console-output" id="console-stderr">No errors.</div>
1160
+ </div>
1161
+ </div>
1162
+
1163
+ <!-- Code Pane -->
1164
+ <div class="tab-pane" id="pane-code">
1165
+ <div class="code-tab-header">
1166
+ <span class="code-tab-lang" id="code-tab-lang">&mdash;</span>
1167
+ <div class="code-tab-actions">
1168
+ <button class="code-tab-btn" id="btn-copy-code" onclick="copyCode()">&#128203; Copy</button>
1169
+ <a class="code-tab-btn" id="btn-download" href="#" style="display:none;">&#11015; Download</a>
1170
+ </div>
1171
+ </div>
1172
+ <div id="code-display">
1173
+ <div class="code-placeholder">No code generated yet.</div>
1174
+ </div>
1175
+ </div>
1176
+
1177
+ <!-- Search Pane -->
1178
+ <div class="tab-pane" id="pane-search">
1179
+ <div class="search-bar">
1180
+ <input type="text" id="search-input" placeholder="Search the web... (Google, no API needed)" spellcheck="false">
1181
+ <button id="btn-search-go" onclick="doWebSearch()">&#128269; Search</button>
1182
+ </div>
1183
+ <div id="search-results">
1184
+ <div class="search-results-empty">Search the web for documentation, examples, and references to use in your code.</div>
1185
+ </div>
1186
+ </div>
1187
+
1188
+ <!-- Deploy Pane -->
1189
+ <div class="tab-pane" id="pane-deploy">
1190
+ <div class="deploy-section">
1191
+ <div class="deploy-title">&#128640; Deploy to HuggingFace</div>
1192
+ <div class="deploy-field">
1193
+ <label for="hf-repo-name">Repository Name</label>
1194
+ <input type="text" id="hf-repo-name" placeholder="username/my-app" autocomplete="off">
1195
+ <div class="deploy-hint">Format: username/repo-name or just repo-name</div>
1196
+ </div>
1197
+ <div class="deploy-field">
1198
+ <label for="hf-token">HuggingFace Token</label>
1199
+ <input type="password" id="hf-token" placeholder="hf_xxxxxxxxxxxxxxxxxxxxx" autocomplete="off">
1200
+ <div class="deploy-hint">Get your token at <a href="https://huggingface.co/settings/tokens" target="_blank">huggingface.co/settings/tokens</a></div>
1201
+ </div>
1202
+ <div class="deploy-field">
1203
+ <label for="hf-space-sdk">Space SDK</label>
1204
+ <select id="hf-space-sdk">
1205
+ <option value="auto">Auto-detect</option>
1206
+ <option value="docker">Docker (React/Next/Vue/Express/Node)</option>
1207
+ <option value="static">Static (HTML/CSS/JS)</option>
1208
+ <option value="gradio">Gradio (Python)</option>
1209
+ <option value="streamlit">Streamlit (Python)</option>
1210
+ </select>
1211
+ <div class="deploy-hint">JS frameworks auto-use Docker with Dockerfile build</div>
1212
+ </div>
1213
+ <button id="btn-push-hf" onclick="pushToHuggingFace()" disabled>&#128640; Push to HuggingFace</button>
1214
+ <div class="deploy-status" id="deploy-status"></div>
1215
+ <div class="project-files" id="project-files"></div>
1216
+ </div>
1217
+ </div>
1218
+ </div>
1219
+ </div>
1220
+ </div>
1221
+
1222
+ <!-- Status Bar -->
1223
+ <div id="status-bar">
1224
+ <div class="status-indicator status-idle" id="status-indicator">
1225
+ <span class="status-dot">&#9679;</span>
1226
+ <span id="status-text">LOADING MODEL...</span>
1227
+ </div>
1228
+ </div>
1229
+ </div>
1230
+
1231
+ <!-- Fullscreen Overlay -->
1232
+ <div id="fullscreen-overlay">
1233
+ <div id="fullscreen-bar">
1234
+ <span>WEB PREVIEW</span>
1235
+ <button id="btn-exit-fullscreen" onclick="closeFullscreen()">[&#10005; CLOSE]</button>
1236
+ </div>
1237
+ <iframe id="fullscreen-iframe" sandbox="allow-scripts"></iframe>
1238
+ </div>
1239
+
1240
+ <script>
1241
+ // ═══════════════════════════════════════════════════════
1242
+ // CONFIG
1243
+ // ═══════════════════════════════════════════════════════
1244
+ const CONFIG = __RUNTIME_CONFIG__;
1245
+
1246
+ // ═══════════════════════════════════════════════════════
1247
+ // STATE
1248
+ // ═══════════════════════════════════════════════════════
1249
+ const state = {
1250
+ history: [],
1251
+ executionContext: {},
1252
+ targetLanguage: 'Python',
1253
+ targetFramework: 'Flask',
1254
+ isGenerating: false,
1255
+ currentEventSource: null,
1256
+ activeTab: 'preview',
1257
+ lastExecution: null,
1258
+ lastCode: '',
1259
+ lastCodeLang: '',
1260
+ pendingWebPreviewCode: '',
1261
+ loadedWebPreviewCode: '',
1262
+ scheduledWebPreviewCode: '',
1263
+ reasoningExpanded: false,
1264
+ lastReasoningPressAt: 0,
1265
+ modelReady: false,
1266
+ searchEnabled: false,
1267
+ lastSearchResults: [],
1268
+ currentSearchResults: [],
1269
+ searchPanelExpanded: false,
1270
+ showThinking: true,
1271
+ currentModelKey: 'minicpm5-1b',
1272
+ currentModelType: 'text',
1273
+ uploadedImageFileUrl: '',
1274
+ uploadedImageName: '',
1275
+ };
1276
+
1277
+ // ═══════════════════════════════════════════════════════
1278
+ // INITIALIZATION
1279
+ // ═══════════════════════════════════════════════════════
1280
+ document.addEventListener('DOMContentLoaded', () => {
1281
+ document.title = CONFIG.app_title || 'SoniCoder';
1282
+
1283
+ try {
1284
+ if (CONFIG.model_url) {
1285
+ document.getElementById('model-pill').href = CONFIG.model_url;
1286
+ document.getElementById('banner-model-link').href = CONFIG.model_url;
1287
+ }
1288
+ const modelId = typeof CONFIG.model_id === 'string' ? CONFIG.model_id : (CONFIG.model_configs ? Object.values(CONFIG.model_configs)[0]?.name || 'AI Model' : 'AI Model');
1289
+ document.getElementById('model-pill-text').textContent = modelId.split('/').pop();
1290
+ } catch (e) { console.warn('Model pill setup error:', e); }
1291
+
1292
+ // Populate language/framework selects
1293
+ populateLanguageSelects();
1294
+
1295
+ // Render examples
1296
+ renderExamples();
1297
+
1298
+ // Welcome message
1299
+ addSystemMessage('Welcome to SoniCoder. The model is loading locally (no API keys needed). Select a language and framework, then describe the app you want to build.');
1300
+
1301
+ // Input auto-resize & keybinding
1302
+ const input = document.getElementById('chat-input');
1303
+ input.addEventListener('input', autoResize);
1304
+ input.addEventListener('keydown', (e) => {
1305
+ if (e.key === 'Enter' && e.shiftKey) {
1306
+ e.preventDefault();
1307
+ handleSend();
1308
+ }
1309
+ });
1310
+
1311
+ // Search input Enter key
1312
+ document.getElementById('search-input')?.addEventListener('keydown', (e) => {
1313
+ if (e.key === 'Enter') { e.preventDefault(); doWebSearch(); }
1314
+ });
1315
+
1316
+ document.addEventListener('pointerdown', handleReasoningPress, true);
1317
+ document.addEventListener('mousedown', handleReasoningPress, true);
1318
+ document.addEventListener('keydown', handleReasoningKeydown, true);
1319
+ document.addEventListener('keydown', handleFullscreenKeydown);
1320
+ observePreviewSize();
1321
+
1322
+ // Poll model status
1323
+ pollModelStatus();
1324
+ });
1325
+
1326
+ function autoResize() {
1327
+ const el = document.getElementById('chat-input');
1328
+ el.style.height = 'auto';
1329
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
1330
+ }
1331
+
1332
+ // ═══════════════════════════════════════════════════════
1333
+ // MODEL STATUS POLLING
1334
+ // ═══════════════════════════════════════════════════════
1335
+ async function pollModelStatus() {
1336
+ try {
1337
+ const resp = await fetch('/api/model-status');
1338
+ const data = await resp.json();
1339
+
1340
+ const dot = document.getElementById('model-dot');
1341
+ const statusText = document.getElementById('status-text');
1342
+ const indicator = document.getElementById('status-indicator');
1343
+
1344
+ if (data.status === 'ready') {
1345
+ state.modelReady = true;
1346
+ dot.className = 'dot';
1347
+ statusText.textContent = 'MODEL READY';
1348
+ indicator.className = 'status-indicator status-success';
1349
+ document.getElementById('btn-push-hf').disabled = false;
1350
+ // Update model info from server response
1351
+ if (data.model_key) state.currentModelKey = data.model_key;
1352
+ if (data.model_type) state.currentModelType = data.model_type;
1353
+ if (data.model_name) {
1354
+ document.getElementById('model-pill-text').textContent = data.model_name;
1355
+ document.getElementById('header-model-name').textContent = data.model_name;
1356
+ }
1357
+ // Show/hide image upload based on model type
1358
+ const imageGroup = document.getElementById('image-attach-group');
1359
+ if (state.currentModelType === 'vlm') {
1360
+ imageGroup.style.display = 'flex';
1361
+ } else {
1362
+ imageGroup.style.display = 'none';
1363
+ }
1364
+ // Sync model selector
1365
+ const modelSelect = document.getElementById('model-select');
1366
+ if (modelSelect && data.model_key) modelSelect.value = data.model_key;
1367
+ setTimeout(() => {
1368
+ if (!state.isGenerating) {
1369
+ indicator.className = 'status-indicator status-idle';
1370
+ statusText.textContent = 'IDLE';
1371
+ }
1372
+ }, 3000);
1373
+ return;
1374
+ } else if (data.status === 'loading') {
1375
+ dot.className = 'dot loading';
1376
+ statusText.textContent = 'LOADING MODEL...';
1377
+ indicator.className = 'status-indicator status-working';
1378
+ } else {
1379
+ dot.className = 'dot error';
1380
+ statusText.textContent = 'MODEL ERROR';
1381
+ indicator.className = 'status-indicator status-error';
1382
+ }
1383
+
1384
+ setTimeout(pollModelStatus, 3000);
1385
+ } catch (err) {
1386
+ console.error('Model status poll error:', err);
1387
+ setTimeout(pollModelStatus, 5000);
1388
+ }
1389
+ }
1390
+
1391
+ // ═══════════════════════════════════════════════════════
1392
+ // LANGUAGE / FRAMEWORK SELECTS
1393
+ // ═══════════════════════════════════════════════════════
1394
+ function populateLanguageSelects() {
1395
+ const langSelect = document.getElementById('lang-select');
1396
+ const fwSelect = document.getElementById('framework-select');
1397
+ if (!langSelect || !fwSelect) return;
1398
+
1399
+ // Fallback languages if CONFIG not loaded
1400
+ const languages = CONFIG.languages || [
1401
+ ["Python", ["Gradio", "Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]],
1402
+ ["JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]],
1403
+ ["TypeScript", ["React", "Next.js", "Express.js", "NestJS"]],
1404
+ ["HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]],
1405
+ ["Java", ["Spring Boot", "Maven", "Gradle"]],
1406
+ ["Go", ["Gin", "Fiber", "Echo", "Plain Go"]],
1407
+ ["Rust", ["Actix", "Axum", "Rocket"]],
1408
+ ["PHP", ["Laravel", "Symfony", "Plain PHP"]],
1409
+ ["Ruby", ["Rails", "Sinatra"]],
1410
+ ["C#", ["ASP.NET", "Blazor"]],
1411
+ ["Swift", ["Vapor", "SwiftUI"]],
1412
+ ["Kotlin", ["Ktor", "Spring Boot"]],
1413
+ ];
1414
+
1415
+ languages.forEach((entry) => {
1416
+ const lang = Array.isArray(entry) ? entry[0] : entry;
1417
+ const opt = document.createElement('option');
1418
+ opt.value = lang;
1419
+ opt.textContent = lang;
1420
+ if (lang === 'Python') opt.selected = true;
1421
+ langSelect.appendChild(opt);
1422
+ });
1423
+
1424
+ onLanguageChange();
1425
+ }
1426
+
1427
+ function onLanguageChange() {
1428
+ const langSelect = document.getElementById('lang-select');
1429
+ const fwSelect = document.getElementById('framework-select');
1430
+ const selectedLang = langSelect.value;
1431
+
1432
+ state.targetLanguage = selectedLang;
1433
+
1434
+ // Update frameworks
1435
+ fwSelect.innerHTML = '';
1436
+ const languages = CONFIG.languages || [];
1437
+ const entry = languages.find((e) => (Array.isArray(e) ? e[0] : e) === selectedLang);
1438
+ if (entry && Array.isArray(entry) && entry[1]) {
1439
+ entry[1].forEach((fw) => {
1440
+ const opt = document.createElement('option');
1441
+ opt.value = fw;
1442
+ opt.textContent = fw;
1443
+ fwSelect.appendChild(opt);
1444
+ });
1445
+ state.targetFramework = entry[1][0];
1446
+ }
1447
+
1448
+ fwSelect.onchange = () => {
1449
+ state.targetFramework = fwSelect.value;
1450
+ autoSelectSDK(selectedLang, fwSelect.value);
1451
+ };
1452
+
1453
+ // Auto-select SDK based on language/framework
1454
+ autoSelectSDK(selectedLang, fwSelect.value);
1455
+ }
1456
+
1457
+ function autoSelectSDK(lang, framework) {
1458
+ const sdkSelect = document.getElementById('hf-space-sdk');
1459
+ if (!sdkSelect) return;
1460
+
1461
+ const jsLangs = ['JavaScript', 'TypeScript'];
1462
+ const jsFrameworks = ['React', 'Next.js', 'Vue.js', 'Express.js', 'Node.js', 'NestJS'];
1463
+ const pythonFrameworks = ['Gradio', 'Streamlit', 'Flask', 'Django', 'FastAPI'];
1464
+
1465
+ if (jsLangs.includes(lang) || jsFrameworks.includes(framework)) {
1466
+ // JS frameworks need Docker
1467
+ if (jsFrameworks.includes(framework) || jsLangs.includes(lang)) {
1468
+ sdkSelect.value = 'docker';
1469
+ }
1470
+ } else if (framework === 'Gradio') {
1471
+ sdkSelect.value = 'gradio';
1472
+ } else if (framework === 'Streamlit') {
1473
+ sdkSelect.value = 'streamlit';
1474
+ } else if (lang === 'Python' && pythonFrameworks.includes(framework)) {
1475
+ sdkSelect.value = 'gradio';
1476
+ } else if (lang === 'HTML/CSS/JS' || framework === 'Vanilla' || framework === 'Tailwind CSS' || framework === 'Bootstrap') {
1477
+ sdkSelect.value = 'static';
1478
+ } else {
1479
+ sdkSelect.value = 'auto';
1480
+ }
1481
+ }
1482
+
1483
+ // ═══════════════════════════════════════════════════════
1484
+ // EXAMPLES
1485
+ // ═══════════════════════════════════════════════════════
1486
+ function renderExamples() {
1487
+ const row = document.getElementById('examples-row');
1488
+ if (!CONFIG.examples || CONFIG.examples.length === 0) {
1489
+ row.style.display = 'none';
1490
+ return;
1491
+ }
1492
+ row.innerHTML = '<span class="examples-label">Try:</span>';
1493
+ CONFIG.examples.forEach((ex) => {
1494
+ const chip = document.createElement('button');
1495
+ chip.className = 'example-chip';
1496
+ chip.textContent = ex.label;
1497
+ chip.title = ex.prompt;
1498
+ chip.addEventListener('click', () => {
1499
+ if (state.isGenerating) return;
1500
+ resetConversation();
1501
+ if (ex.language) {
1502
+ document.getElementById('lang-select').value = ex.language;
1503
+ onLanguageChange();
1504
+ }
1505
+ if (ex.framework) {
1506
+ document.getElementById('framework-select').value = ex.framework;
1507
+ state.targetFramework = ex.framework;
1508
+ }
1509
+ sendMessage(ex.prompt);
1510
+ });
1511
+ row.appendChild(chip);
1512
+ });
1513
+ }
1514
+
1515
+ // ═══════════════════════════════════════════════════════
1516
+ // CHAT MESSAGES
1517
+ // ═══════════════════════════════════════════════════════
1518
+ function addSystemMessage(text) {
1519
+ const container = document.getElementById('chat-messages');
1520
+ const div = document.createElement('div');
1521
+ div.className = 'msg msg-system';
1522
+ div.innerHTML = `<span class="msg-prefix">system&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1523
+ container.appendChild(div);
1524
+ scrollToBottom();
1525
+ }
1526
+
1527
+ function addUserMessage(text) {
1528
+ const container = document.getElementById('chat-messages');
1529
+ const div = document.createElement('div');
1530
+ div.className = 'msg msg-user';
1531
+ div.innerHTML = `<span class="msg-prefix">user&gt;</span><span class="msg-content">${escapeHtml(text)}</span>`;
1532
+ container.appendChild(div);
1533
+ scrollToBottom();
1534
+ }
1535
+
1536
+ function addAssistantMessage() {
1537
+ const container = document.getElementById('chat-messages');
1538
+ const div = document.createElement('div');
1539
+ div.className = 'msg msg-assistant';
1540
+ div.id = 'current-assistant-msg';
1541
+ div.innerHTML = `<span class="msg-prefix">ai&gt;</span><div class="msg-body"><div class="search-source-container"></div><span class="msg-content streaming-cursor"></span></div>`;
1542
+ container.appendChild(div);
1543
+ state.reasoningExpanded = false;
1544
+ state.currentSearchResults = [];
1545
+ state.searchPanelExpanded = false;
1546
+ scrollToBottom();
1547
+ return div;
1548
+ }
1549
+
1550
+ function updateAssistantMessage(content, isStreaming) {
1551
+ const div = document.getElementById('current-assistant-msg');
1552
+ if (!div) return;
1553
+ const contentEl = div.querySelector('.msg-content');
1554
+ const keepReasoningExpanded = state.reasoningExpanded || Boolean(contentEl.querySelector('.think-block.open'));
1555
+ state.reasoningExpanded = keepReasoningExpanded;
1556
+ contentEl.innerHTML = parseMarkdown(content);
1557
+ contentEl.querySelectorAll('.think-block').forEach((block) => {
1558
+ setReasoningBlockOpen(block, keepReasoningExpanded);
1559
+ });
1560
+ if (isStreaming) {
1561
+ contentEl.classList.add('streaming-cursor');
1562
+ } else {
1563
+ contentEl.classList.remove('streaming-cursor');
1564
+ }
1565
+ scrollToBottom();
1566
+ }
1567
+
1568
+ function finalizeAssistantMessage() {
1569
+ const div = document.getElementById('current-assistant-msg');
1570
+ if (div) {
1571
+ div.id = '';
1572
+ const contentEl = div.querySelector('.msg-content');
1573
+ if (contentEl) contentEl.classList.remove('streaming-cursor');
1574
+ }
1575
+ }
1576
+
1577
+ function scrollToBottom() {
1578
+ const container = document.getElementById('chat-messages');
1579
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
1580
+ }
1581
+
1582
+ // ═══════════════════════════════════════════════════════
1583
+ // MARKDOWN PARSER
1584
+ // ═══════════════════════════════════════════════════════
1585
+ function parseMarkdown(text) {
1586
+ if (!text) return '';
1587
+
1588
+ const thinkBlocks = [];
1589
+ text = text.replace(/<think>([\s\S]*?)<\/think>/g, (_, content) => {
1590
+ const idx = thinkBlocks.length;
1591
+ thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (click to expand)'));
1592
+ return `@@THINKBLOCK_${idx}@@`;
1593
+ });
1594
+ text = text.replace(/<think>([\s\S]*)$/g, (_, content) => {
1595
+ const idx = thinkBlocks.length;
1596
+ thinkBlocks.push(renderThinkBlock(content, '\ud83d\udcad Reasoning (thinking...)'));
1597
+ return `@@THINKBLOCK_${idx}@@`;
1598
+ });
1599
+
1600
+ const codeBlocks = [];
1601
+ text = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
1602
+ const idx = codeBlocks.length;
1603
+ codeBlocks.push({ lang: lang || 'text', code: code.trimEnd() });
1604
+ return `@@CODEBLOCK_${idx}@@`;
1605
+ });
1606
+
1607
+ text = escapeHtml(text);
1608
+
1609
+ text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
1610
+ text = text.replace(/(?<!\*)\*([^*]+?)\*(?!\*)/g, '<em>$1</em>');
1611
+ text = text.replace(/`([^`]+?)`/g, '<code>$1</code>');
1612
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
1613
+
1614
+ text = text.replace(/^### (.+)$/gm, '<h3>$1</h3>');
1615
+ text = text.replace(/^## (.+)$/gm, '<h2>$1</h2>');
1616
+ text = text.replace(/^# (.+)$/gm, '<h1>$1</h1>');
1617
+
1618
+ text = text.replace(/^(?:[-*]) (.+)$/gm, '<li>$1</li>');
1619
+ text = text.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
1620
+ text = text.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
1621
+ text = text.replace(/(<li>(?:(?!<\/?[uo]l>).)*<\/li>(?:\s*<li>(?:(?!<\/?[uo]l>).)*<\/li>)*)/g, (match) => {
1622
+ if (!match.includes('<ul>') && !match.includes('</ul>')) return '<ol>' + match + '</ol>';
1623
+ return match;
1624
+ });
1625
+
1626
+ text = text.replace(/@@CODEBLOCK_(\d+)@@/g, (_, idx) => {
1627
+ const block = codeBlocks[parseInt(idx)];
1628
+ const escapedCode = escapeHtml(block.code);
1629
+ const id = `code-${Date.now()}-${idx}`;
1630
+ 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>`;
1631
+ });
1632
+ text = text.replace(/@@THINKBLOCK_(\d+)@@/g, (_, idx) => thinkBlocks[parseInt(idx)]);
1633
+
1634
+ text = text.replace(/\n\n/g, '</p><p>');
1635
+ text = text.replace(/\n/g, '<br>');
1636
+ text = '<p>' + text + '</p>';
1637
+ text = text.replace(/<p>\s*<\/p>/g, '');
1638
+ text = text.replace(/<p>(<(?:div|ul|ol|h[1-3]))/g, '$1');
1639
+ text = text.replace(/(<\/(?:div|ul|ol|h[1-3])>)<\/p>/g, '$1');
1640
+
1641
+ return text;
1642
+ }
1643
+
1644
+ function renderThinkBlock(content, summary) {
1645
+ const escapedContent = escapeHtml(content.trim()).replace(/\n/g, '<br>');
1646
+ const openClass = state.reasoningExpanded ? ' open' : '';
1647
+ const expanded = state.reasoningExpanded ? 'true' : 'false';
1648
+ return `<div class="think-block${openClass}"><button type="button" class="think-summary" aria-expanded="${expanded}">${summary}</button><div class="think-content">${escapedContent}</div></div>`;
1649
+ }
1650
+
1651
+ function handleReasoningPress(event) { updateReasoningFromEvent(event); }
1652
+ function handleReasoningKeydown(event) {
1653
+ if (event.key !== 'Enter' && event.key !== ' ') return;
1654
+ updateReasoningFromEvent(event);
1655
+ }
1656
+
1657
+ function updateReasoningFromEvent(event) {
1658
+ if (event.type === 'mousedown' && Date.now() - state.lastReasoningPressAt < 500) return;
1659
+ const target = event.target;
1660
+ if (!target || !target.closest) return;
1661
+ const button = target.closest('.think-summary');
1662
+ if (!button) return;
1663
+ const block = button.closest('.think-block');
1664
+ if (!block) return;
1665
+ event.preventDefault();
1666
+ event.stopPropagation();
1667
+ if (event.stopImmediatePropagation) event.stopImmediatePropagation();
1668
+ state.lastReasoningPressAt = Date.now();
1669
+ const nextOpen = !block.classList.contains('open');
1670
+ state.reasoningExpanded = nextOpen;
1671
+ const scope = block.closest('.msg-content') || document;
1672
+ scope.querySelectorAll('.think-block').forEach((trace) => {
1673
+ setReasoningBlockOpen(trace, nextOpen);
1674
+ });
1675
+ }
1676
+
1677
+ function setReasoningBlockOpen(block, open) {
1678
+ block.classList.toggle('open', open);
1679
+ const button = block.querySelector('.think-summary');
1680
+ if (button) button.setAttribute('aria-expanded', open ? 'true' : 'false');
1681
+ }
1682
+
1683
+ function escapeHtml(text) {
1684
+ const div = document.createElement('div');
1685
+ div.textContent = text;
1686
+ return div.innerHTML;
1687
+ }
1688
+
1689
+ // ══════���════════════════════════════════════════════════
1690
+ // COPY FUNCTIONS
1691
+ // ═══════════════════════════════════════════════════════
1692
+ function copyBlock(button, codeId) {
1693
+ const codeEl = document.getElementById(codeId);
1694
+ if (!codeEl) return;
1695
+ navigator.clipboard.writeText(codeEl.textContent).then(() => {
1696
+ button.textContent = '\u2713 Copied!';
1697
+ button.classList.add('copied');
1698
+ setTimeout(() => { button.textContent = '\ud83d\udccb Copy'; button.classList.remove('copied'); }, 2000);
1699
+ });
1700
+ }
1701
+
1702
+ function copyCode() {
1703
+ if (!state.lastCode) return;
1704
+ const btn = document.getElementById('btn-copy-code');
1705
+ navigator.clipboard.writeText(state.lastCode).then(() => {
1706
+ btn.textContent = '\u2713 Copied!';
1707
+ setTimeout(() => { btn.textContent = '\ud83d\udccb Copy'; }, 2000);
1708
+ });
1709
+ }
1710
+
1711
+ // ═══════════════════════════════════════════════════════
1712
+ // STATUS BAR
1713
+ // ═══════════════════════════════════════════════════════
1714
+ function renderStatus(text, statusState) {
1715
+ const indicator = document.getElementById('status-indicator');
1716
+ const textEl = document.getElementById('status-text');
1717
+ const dotEl = indicator.querySelector('.status-dot');
1718
+
1719
+ indicator.className = 'status-indicator';
1720
+ switch (statusState) {
1721
+ case 'working': indicator.classList.add('status-working'); dotEl.textContent = '\u25d0'; break;
1722
+ case 'success': indicator.classList.add('status-success'); dotEl.textContent = '\u2713'; break;
1723
+ case 'error': indicator.classList.add('status-error'); dotEl.textContent = '\u2717'; break;
1724
+ case 'info': indicator.classList.add('status-info'); dotEl.textContent = '\u2139'; break;
1725
+ default: indicator.classList.add('status-idle'); dotEl.textContent = '\u25cf';
1726
+ }
1727
+ textEl.textContent = text || 'IDLE';
1728
+ }
1729
+
1730
+ // ═══════════════════════════════════════════════════════
1731
+ // OUTPUT PANEL
1732
+ // ═══════════════════════════════════════════════════════
1733
+ function switchTab(tab, { forcePreviewReload = false } = {}) {
1734
+ const wasPreview = state.activeTab === 'preview';
1735
+ state.activeTab = tab;
1736
+ document.querySelectorAll('.output-tab').forEach((btn) => {
1737
+ btn.classList.toggle('active', btn.dataset.tab === tab);
1738
+ });
1739
+ document.querySelectorAll('.tab-pane').forEach((pane) => {
1740
+ pane.classList.toggle('active', pane.id === `pane-${tab}`);
1741
+ });
1742
+ if (tab === 'preview') {
1743
+ ensureWebPreviewLoaded({ forceReload: forcePreviewReload || !wasPreview });
1744
+ }
1745
+ }
1746
+
1747
+ function renderExecution(execution) {
1748
+ if (!execution) return;
1749
+ state.lastExecution = execution;
1750
+
1751
+ // Console
1752
+ document.getElementById('console-stdout').textContent = execution.stdout || 'No output.';
1753
+ document.getElementById('console-stderr').textContent = execution.stderr || 'No errors.';
1754
+
1755
+ // Code
1756
+ if (execution.code) {
1757
+ state.lastCode = execution.code;
1758
+ state.lastCodeLang = execution.language || 'code';
1759
+ document.getElementById('code-tab-lang').textContent = state.lastCodeLang;
1760
+ document.getElementById('code-display').innerHTML = `<pre>${escapeHtml(execution.code)}</pre>`;
1761
+ }
1762
+
1763
+ // Download
1764
+ const dlBtn = document.getElementById('btn-download');
1765
+ if (execution.download_url) {
1766
+ dlBtn.href = execution.download_url;
1767
+ dlBtn.style.display = 'inline-flex';
1768
+ dlBtn.setAttribute('download', '');
1769
+ } else {
1770
+ dlBtn.style.display = 'none';
1771
+ }
1772
+
1773
+ // Preview
1774
+ const placeholder = document.getElementById('preview-placeholder');
1775
+ const img = document.getElementById('preview-image');
1776
+ const iframe = getPreviewIframe();
1777
+ const fsBtn = document.getElementById('btn-fullscreen');
1778
+
1779
+ if (execution.image_url) {
1780
+ placeholder.style.display = 'none';
1781
+ iframe.style.display = 'none';
1782
+ fsBtn.style.display = 'none';
1783
+ img.src = execution.image_url;
1784
+ img.style.display = 'block';
1785
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
1786
+ switchTab('preview');
1787
+ }
1788
+ } else if (execution.is_web && execution.code) {
1789
+ placeholder.style.display = 'none';
1790
+ img.style.display = 'none';
1791
+ iframe.style.display = 'block';
1792
+ fsBtn.style.display = 'block';
1793
+ state.pendingWebPreviewCode = execution.code;
1794
+ state.loadedWebPreviewCode = '';
1795
+ state.scheduledWebPreviewCode = '';
1796
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
1797
+ switchTab('preview', { forcePreviewReload: true });
1798
+ } else {
1799
+ iframe.srcdoc = '';
1800
+ }
1801
+ } else if (execution.is_gradio && execution.gradio_url) {
1802
+ // Gradio app handling
1803
+ placeholder.style.display = 'none';
1804
+ img.style.display = 'none';
1805
+ iframe.style.display = 'block';
1806
+ fsBtn.style.display = 'block';
1807
+ // Show Gradio badge
1808
+ const badge = document.createElement('span');
1809
+ badge.className = 'gradio-badge';
1810
+ badge.innerHTML = '\u26a1 Gradio';
1811
+ const codeTabHeader = document.querySelector('.code-tab-header');
1812
+ if (codeTabHeader) codeTabHeader.prepend(badge);
1813
+ state.pendingWebPreviewCode = `<html><body style="margin:0;display:flex;align-items:center;justify-content:center;height:100vh;font-family:monospace;background:#0d1117;color:#a855f7;"><div style="text-align:center"><h2>\u26a1 Gradio App Running</h2><p>App is running at: <a href="${execution.gradio_url}" target="_blank" style="color:#00d4ff">${execution.gradio_url}</a></p><p style="color:#8b949e;font-size:12px">Open in a new tab to interact with the Gradio interface</p></div></body></html>`;
1814
+ state.loadedWebPreviewCode = '';
1815
+ state.scheduledWebPreviewCode = '';
1816
+ switchTab('preview', { forcePreviewReload: true });
1817
+ } else {
1818
+ if (execution.stdout || execution.stderr) {
1819
+ const suggested = execution.suggested_tab || 'console';
1820
+ if (state.activeTab !== 'deploy') switchTab(suggested);
1821
+ }
1822
+ }
1823
+
1824
+ // Deploy tab - project files
1825
+ renderProjectFiles(execution.project_files || {});
1826
+
1827
+ // Enable deploy button
1828
+ document.getElementById('btn-push-hf').disabled = !execution.code;
1829
+ }
1830
+
1831
+ function renderProjectFiles(files) {
1832
+ const container = document.getElementById('project-files');
1833
+ if (!files || Object.keys(files).length === 0) {
1834
+ container.innerHTML = '';
1835
+ return;
1836
+ }
1837
+
1838
+ let html = '<div style="margin-top: 12px; font-size: 10px; color: var(--gray-dim); letter-spacing: 1px; text-transform: uppercase;">Project Files:</div>';
1839
+ for (const [filepath, content] of Object.entries(files)) {
1840
+ const ext = filepath.split('.').pop();
1841
+ const icon = getFileIcon(ext);
1842
+ 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>`;
1843
+ }
1844
+ container.innerHTML = html;
1845
+ }
1846
+
1847
+ function getFileIcon(ext) {
1848
+ const icons = {
1849
+ 'py': '\ud83d\udc0d', 'js': '\u26a1', 'ts': '\ud83d\udde1\ufe0f', 'html': '\ud83c\udf10',
1850
+ 'css': '\ud83c\udfa8', 'json': '\ud83d\udcc4', 'md': '\ud83d\udcd3', 'yml': '\u2699\ufe0f',
1851
+ 'yaml': '\u2699\ufe0f', 'java': '\u2615', 'go': '\ud83e\udd85', 'rs': '\ud83e\udd80',
1852
+ 'php': '\ud83d\udc18', 'rb': '\ud83d\udc8e', 'swift': '\ud83e\udd85', 'kt': '\ud83c\udf0a',
1853
+ };
1854
+ return icons[ext] || '\ud83d\udcc1';
1855
+ }
1856
+
1857
+ function resetOutput() {
1858
+ const iframe = getPreviewIframe();
1859
+ document.getElementById('preview-placeholder').style.display = '';
1860
+ document.getElementById('preview-image').style.display = 'none';
1861
+ iframe.style.display = 'none';
1862
+ iframe.srcdoc = '';
1863
+ document.getElementById('btn-fullscreen').style.display = 'none';
1864
+ document.getElementById('console-stdout').textContent = 'No output.';
1865
+ document.getElementById('console-stderr').textContent = 'No errors.';
1866
+ document.getElementById('code-display').innerHTML = '<div class="code-placeholder">No code generated yet.</div>';
1867
+ document.getElementById('code-tab-lang').textContent = '\u2014';
1868
+ document.getElementById('btn-download').style.display = 'none';
1869
+ document.getElementById('project-files').innerHTML = '';
1870
+ document.getElementById('deploy-status').className = 'deploy-status';
1871
+ document.getElementById('deploy-status').style.display = 'none';
1872
+ state.lastExecution = null;
1873
+ state.lastCode = '';
1874
+ state.lastCodeLang = '';
1875
+ state.pendingWebPreviewCode = '';
1876
+ state.loadedWebPreviewCode = '';
1877
+ state.scheduledWebPreviewCode = '';
1878
+ }
1879
+
1880
+ // ═══════════════════════════════════════════════════════
1881
+ // FULLSCREEN
1882
+ // ═══════════════════════════════════════════════════════
1883
+ function getPreviewIframe() { return document.getElementById('preview-iframe'); }
1884
+
1885
+ function recreatePreviewIframe() {
1886
+ const oldFrame = getPreviewIframe();
1887
+ const freshFrame = document.createElement('iframe');
1888
+ freshFrame.id = 'preview-iframe';
1889
+ freshFrame.setAttribute('sandbox', 'allow-scripts');
1890
+ freshFrame.style.display = oldFrame.style.display || 'block';
1891
+ oldFrame.replaceWith(freshFrame);
1892
+ return freshFrame;
1893
+ }
1894
+
1895
+ function ensureWebPreviewLoaded({ forceReload = false } = {}) {
1896
+ const iframe = getPreviewIframe();
1897
+ if (!state.pendingWebPreviewCode || state.activeTab !== 'preview' || iframe.style.display === 'none') return;
1898
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) {
1899
+ schedulePreviewResize(iframe);
1900
+ return;
1901
+ }
1902
+ if (!forceReload && state.scheduledWebPreviewCode === state.pendingWebPreviewCode) return;
1903
+
1904
+ state.scheduledWebPreviewCode = state.pendingWebPreviewCode;
1905
+ iframe.srcdoc = '';
1906
+ const loadWhenLaidOut = () => {
1907
+ if (state.activeTab !== 'preview' || !state.pendingWebPreviewCode) {
1908
+ state.scheduledWebPreviewCode = '';
1909
+ return;
1910
+ }
1911
+ if (!forceReload && state.loadedWebPreviewCode === state.pendingWebPreviewCode) return;
1912
+ const visibleFrame = getPreviewIframe();
1913
+ const rect = visibleFrame.getBoundingClientRect();
1914
+ if (rect.width < 10 || rect.height < 10) {
1915
+ state.scheduledWebPreviewCode = '';
1916
+ setTimeout(() => ensureWebPreviewLoaded({ forceReload }), 50);
1917
+ return;
1918
+ }
1919
+ const freshFrame = recreatePreviewIframe();
1920
+ freshFrame.srcdoc = state.pendingWebPreviewCode;
1921
+ state.loadedWebPreviewCode = state.pendingWebPreviewCode;
1922
+ state.scheduledWebPreviewCode = '';
1923
+ schedulePreviewResize(freshFrame);
1924
+ };
1925
+ requestAnimationFrame(() => requestAnimationFrame(loadWhenLaidOut));
1926
+ setTimeout(loadWhenLaidOut, 75);
1927
+ }
1928
+
1929
+ function schedulePreviewResize(iframe) {
1930
+ const dispatchResize = () => {
1931
+ try { iframe.contentWindow?.dispatchEvent(new Event('resize')); } catch (_err) {}
1932
+ };
1933
+ requestAnimationFrame(() => requestAnimationFrame(dispatchResize));
1934
+ setTimeout(dispatchResize, 100);
1935
+ setTimeout(dispatchResize, 350);
1936
+ }
1937
+
1938
+ function observePreviewSize() {
1939
+ const previewPane = document.getElementById('pane-preview');
1940
+ if (!previewPane) return;
1941
+ window.addEventListener('resize', () => {
1942
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1943
+ schedulePreviewResize(getPreviewIframe());
1944
+ }
1945
+ });
1946
+ if (typeof ResizeObserver === 'undefined') return;
1947
+ const observer = new ResizeObserver(() => {
1948
+ if (state.activeTab === 'preview' && state.loadedWebPreviewCode) {
1949
+ schedulePreviewResize(getPreviewIframe());
1950
+ }
1951
+ });
1952
+ observer.observe(previewPane);
1953
+ }
1954
+
1955
+ function openFullscreen() {
1956
+ const overlay = document.getElementById('fullscreen-overlay');
1957
+ const iframe = document.getElementById('fullscreen-iframe');
1958
+ if (state.lastExecution && state.lastExecution.is_web && state.lastExecution.code) {
1959
+ iframe.srcdoc = state.lastExecution.code;
1960
+ }
1961
+ overlay.classList.add('active');
1962
+ }
1963
+
1964
+ function closeFullscreen() {
1965
+ document.getElementById('fullscreen-overlay').classList.remove('active');
1966
+ document.getElementById('fullscreen-iframe').srcdoc = '';
1967
+ }
1968
+
1969
+ function handleFullscreenKeydown(event) {
1970
+ if (event.key !== 'Escape') return;
1971
+ const overlay = document.getElementById('fullscreen-overlay');
1972
+ if (!overlay.classList.contains('active')) return;
1973
+ event.preventDefault();
1974
+ closeFullscreen();
1975
+ }
1976
+
1977
+ // ═══════════════════════════════════════════════════════
1978
+ // SEND / RECEIVE
1979
+ // ═══════════════════════════════════════════════════════
1980
+ function handleSend() {
1981
+ const input = document.getElementById('chat-input');
1982
+ const prompt = input.value.trim();
1983
+ if (!prompt || state.isGenerating) return;
1984
+ input.value = '';
1985
+ autoResize();
1986
+ sendMessage(prompt);
1987
+ }
1988
+
1989
+ async function sendMessage(prompt) {
1990
+ if (state.isGenerating) return;
1991
+
1992
+ if (!state.modelReady) {
1993
+ addSystemMessage('The model is still loading. Please wait...');
1994
+ return;
1995
+ }
1996
+
1997
+ state.isGenerating = true;
1998
+ toggleInputState(true);
1999
+ addUserMessage(prompt);
2000
+ addAssistantMessage();
2001
+ renderStatus('Thinking...', 'working');
2002
+
2003
+ const historyJSON = JSON.stringify(state.history);
2004
+ const execContextJSON = JSON.stringify(state.executionContext);
2005
+ const framework = document.getElementById('framework-select')?.value || state.targetFramework;
2006
+
2007
+ try {
2008
+ const resp = await fetch('/gradio_api/call/chat', {
2009
+ method: 'POST',
2010
+ headers: { 'Content-Type': 'application/json' },
2011
+ body: JSON.stringify({
2012
+ data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON, state.searchEnabled ? 'true' : 'false', state.uploadedImageFileUrl || '']
2013
+ })
2014
+ });
2015
+
2016
+ if (!resp.ok) throw new Error(`API error: ${resp.status} ${resp.statusText}`);
2017
+
2018
+ const { event_id } = await resp.json();
2019
+ const eventSource = new EventSource(`/gradio_api/call/chat/${event_id}`);
2020
+ state.currentEventSource = eventSource;
2021
+
2022
+ eventSource.addEventListener('generating', (e) => {
2023
+ try {
2024
+ const dataArray = JSON.parse(e.data);
2025
+ const payload = JSON.parse(dataArray[0]);
2026
+ handlePayload(payload, true);
2027
+ } catch (err) { console.error('Parse error (generating):', err); }
2028
+ });
2029
+
2030
+ eventSource.addEventListener('complete', (e) => {
2031
+ try {
2032
+ const dataArray = JSON.parse(e.data);
2033
+ const payload = JSON.parse(dataArray[0]);
2034
+ handlePayload(payload, false);
2035
+ } catch (err) { console.error('Parse error (complete):', err); }
2036
+ eventSource.close();
2037
+ onGenerationEnd();
2038
+ });
2039
+
2040
+ eventSource.addEventListener('error', (e) => {
2041
+ let errorMsg = 'An error occurred during generation.';
2042
+ if (e.data) errorMsg = e.data;
2043
+ console.error('SSE error:', errorMsg);
2044
+ finalizeAssistantMessage();
2045
+ addSystemMessage(`Error: ${errorMsg}`);
2046
+ renderStatus('Error', 'error');
2047
+ eventSource.close();
2048
+ onGenerationEnd();
2049
+ });
2050
+
2051
+ } catch (err) {
2052
+ console.error('Send error:', err);
2053
+ finalizeAssistantMessage();
2054
+ addSystemMessage(`Error: ${err.message}`);
2055
+ renderStatus('Error', 'error');
2056
+ onGenerationEnd();
2057
+ }
2058
+ }
2059
+
2060
+ function handlePayload(payload, isStreaming) {
2061
+ if (payload.status_text) renderStatus(payload.status_text, payload.status_state || 'working');
2062
+
2063
+ // Handle search results (show inline source badge)
2064
+ if (payload.type === 'search_results' && payload.search_results) {
2065
+ state.currentSearchResults = payload.search_results;
2066
+ state.lastSearchResults = payload.search_results;
2067
+ renderSearchSourceBadge(payload.search_results, false);
2068
+ // Also render in the Search tab
2069
+ renderSearchResults(payload.search_results);
2070
+ }
2071
+
2072
+ if (payload.history) {
2073
+ state.history = payload.history;
2074
+ const lastMsg = payload.history[payload.history.length - 1];
2075
+ if (lastMsg && lastMsg.role === 'assistant') {
2076
+ updateAssistantMessage(lastMsg.content, isStreaming);
2077
+ }
2078
+ }
2079
+
2080
+ if (payload.execution) {
2081
+ renderExecution(payload.execution);
2082
+ if (payload.execution) state.executionContext = payload.execution;
2083
+ }
2084
+
2085
+ if (payload.type === 'complete') {
2086
+ finalizeAssistantMessage();
2087
+ renderStatus('Done', 'success');
2088
+ setTimeout(() => { if (!state.isGenerating) renderStatus('Idle', 'idle'); }, 3000);
2089
+ }
2090
+
2091
+ if (payload.type === 'error') {
2092
+ finalizeAssistantMessage();
2093
+ addSystemMessage(`Error: ${payload.status_text || 'Unknown error'}`);
2094
+ renderStatus('Error', 'error');
2095
+ }
2096
+
2097
+ if (payload.execution && payload.execution.suggested_tab) {
2098
+ switchTab(payload.execution.suggested_tab);
2099
+ }
2100
+ }
2101
+
2102
+ function onGenerationEnd() {
2103
+ state.isGenerating = false;
2104
+ state.currentEventSource = null;
2105
+ toggleInputState(false);
2106
+ }
2107
+
2108
+ function toggleInputState(generating) {
2109
+ const sendBtn = document.getElementById('btn-send');
2110
+ const stopBtn = document.getElementById('btn-stop');
2111
+ const input = document.getElementById('chat-input');
2112
+
2113
+ if (generating) {
2114
+ sendBtn.style.display = 'none';
2115
+ stopBtn.style.display = 'flex';
2116
+ input.disabled = true;
2117
+ input.placeholder = 'Generating...';
2118
+ } else {
2119
+ sendBtn.style.display = 'flex';
2120
+ stopBtn.style.display = 'none';
2121
+ sendBtn.disabled = false;
2122
+ input.disabled = false;
2123
+ input.placeholder = 'Describe the app you want to build...';
2124
+ input.focus();
2125
+ }
2126
+ }
2127
+
2128
+ function stopGeneration() {
2129
+ if (state.currentEventSource) {
2130
+ state.currentEventSource.close();
2131
+ state.currentEventSource = null;
2132
+ }
2133
+ finalizeAssistantMessage();
2134
+ addSystemMessage('Generation stopped by user.');
2135
+ renderStatus('Stopped', 'info');
2136
+ onGenerationEnd();
2137
+ }
2138
+
2139
+ function resetConversation(announcement) {
2140
+ state.history = [];
2141
+ state.executionContext = {};
2142
+ state.lastExecution = null;
2143
+ state.lastCode = '';
2144
+ state.lastCodeLang = '';
2145
+ state.reasoningExpanded = false;
2146
+
2147
+ if (state.currentEventSource) {
2148
+ state.currentEventSource.close();
2149
+ state.currentEventSource = null;
2150
+ }
2151
+ state.isGenerating = false;
2152
+ toggleInputState(false);
2153
+
2154
+ document.getElementById('chat-messages').innerHTML = '';
2155
+ resetOutput();
2156
+ switchTab('preview');
2157
+ renderStatus('Idle', 'idle');
2158
+ if (announcement) addSystemMessage(announcement);
2159
+ }
2160
+
2161
+ function newChat() {
2162
+ resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'SoniCoder'}.`);
2163
+ }
2164
+
2165
+ function toggleThinking() {
2166
+ state.showThinking = !state.showThinking;
2167
+ const btn = document.getElementById('btn-thinking');
2168
+ if (state.showThinking) {
2169
+ btn.classList.add('active');
2170
+ document.body.classList.remove('hide-thinking');
2171
+ btn.textContent = '🧠 Think';
2172
+ } else {
2173
+ btn.classList.remove('active');
2174
+ document.body.classList.add('hide-thinking');
2175
+ btn.textContent = '🧠 Think';
2176
+ }
2177
+ }
2178
+
2179
+ // ═══════════════════════════════════════════════════════
2180
+ // MODEL SWITCHING
2181
+ // ═══════════════════════════════════════════════════════
2182
+ async function onModelChange() {
2183
+ const select = document.getElementById('model-select');
2184
+ const modelKey = select.value;
2185
+ if (modelKey === state.currentModelKey) return;
2186
+
2187
+ const isVLM = modelKey === 'minicpm-v-4.6';
2188
+ const imageGroup = document.getElementById('image-attach-group');
2189
+ if (isVLM) {
2190
+ imageGroup.style.display = 'flex';
2191
+ } else {
2192
+ imageGroup.style.display = 'none';
2193
+ removeImage();
2194
+ }
2195
+
2196
+ addSystemMessage(`Switching model to ${select.options[select.selectedIndex].text}...`);
2197
+ renderStatus('Switching model...', 'working');
2198
+
2199
+ try {
2200
+ const resp = await fetch('/gradio_api/call/switch_model', {
2201
+ method: 'POST',
2202
+ headers: { 'Content-Type': 'application/json' },
2203
+ body: JSON.stringify({ data: [modelKey] })
2204
+ });
2205
+ const { event_id } = await resp.json();
2206
+ const eventSource = new EventSource(`/gradio_api/call/switch_model/${event_id}`);
2207
+
2208
+ eventSource.addEventListener('complete', (e) => {
2209
+ const dataArray = JSON.parse(e.data);
2210
+ const result = JSON.parse(dataArray[0]);
2211
+ if (result.success) {
2212
+ state.currentModelKey = modelKey;
2213
+ state.currentModelType = isVLM ? 'vlm' : 'text';
2214
+ const name = isVLM ? 'MiniCPM-V-4.6' : 'MiniCPM5-1B';
2215
+ document.getElementById('model-pill-text').textContent = name;
2216
+ document.getElementById('header-model-name').textContent = name;
2217
+ addSystemMessage(`Switched to ${name}. Model is loading in background...`);
2218
+ // Poll for model ready
2219
+ pollModelStatus();
2220
+ } else {
2221
+ addSystemMessage(`Failed to switch: ${result.message}`);
2222
+ select.value = state.currentModelKey;
2223
+ }
2224
+ eventSource.close();
2225
+ });
2226
+ eventSource.addEventListener('error', () => {
2227
+ addSystemMessage('Model switch failed');
2228
+ select.value = state.currentModelKey;
2229
+ eventSource.close();
2230
+ });
2231
+ } catch (err) {
2232
+ addSystemMessage(`Switch error: ${err.message}`);
2233
+ select.value = state.currentModelKey;
2234
+ }
2235
+ }
2236
+
2237
+ function pollModelStatus() {
2238
+ const interval = setInterval(async () => {
2239
+ try {
2240
+ const resp = await fetch('/api/model-status');
2241
+ const status = await resp.json();
2242
+ if (status.status === 'ready') {
2243
+ state.modelReady = true;
2244
+ const dot = document.getElementById('model-dot');
2245
+ dot.className = 'dot';
2246
+ dot.style.background = 'var(--success)';
2247
+ dot.style.boxShadow = '0 0 6px var(--success)';
2248
+ renderStatus('Ready', 'success');
2249
+ setTimeout(() => renderStatus('Idle', 'idle'), 2000);
2250
+ clearInterval(interval);
2251
+ } else if (status.status === 'error') {
2252
+ state.modelReady = false;
2253
+ renderStatus('Model error', 'error');
2254
+ clearInterval(interval);
2255
+ }
2256
+ } catch { clearInterval(interval); }
2257
+ }, 2000);
2258
+ }
2259
+
2260
+ // ═══════════════════════════════════════════════════════
2261
+ // IMAGE UPLOAD (VLM)
2262
+ // ═══════════════════════════════════════════════════════
2263
+ async function onImageUpload(event) {
2264
+ const file = event.target.files[0];
2265
+ if (!file) return;
2266
+
2267
+ state.uploadedImageName = file.name;
2268
+ document.getElementById('image-attach-name').textContent = file.name;
2269
+ document.getElementById('btn-remove-image').style.display = 'inline';
2270
+
2271
+ // Convert to base64
2272
+ const reader = new FileReader();
2273
+ reader.onload = async function(e) {
2274
+ const base64Data = e.target.result;
2275
+
2276
+ // Upload to server
2277
+ try {
2278
+ const resp = await fetch('/gradio_api/call/upload_image', {
2279
+ method: 'POST',
2280
+ headers: { 'Content-Type': 'application/json' },
2281
+ body: JSON.stringify({ data: [base64Data] })
2282
+ });
2283
+ const { event_id } = await resp.json();
2284
+ const eventSource = new EventSource(`/gradio_api/call/upload_image/${event_id}`);
2285
+
2286
+ eventSource.addEventListener('complete', (ev) => {
2287
+ const dataArray = JSON.parse(ev.data);
2288
+ const result = JSON.parse(dataArray[0]);
2289
+ if (result.success) {
2290
+ state.uploadedImageFileUrl = result.file_url;
2291
+ document.getElementById('image-attach-name').textContent = 'βœ“ ' + file.name;
2292
+ } else {
2293
+ document.getElementById('image-attach-name').textContent = 'βœ— Upload failed';
2294
+ }
2295
+ eventSource.close();
2296
+ });
2297
+ eventSource.addEventListener('error', () => {
2298
+ document.getElementById('image-attach-name').textContent = 'βœ— Upload error';
2299
+ eventSource.close();
2300
+ });
2301
+ } catch (err) {
2302
+ document.getElementById('image-attach-name').textContent = 'βœ— Error';
2303
+ }
2304
+ };
2305
+ reader.readAsDataURL(file);
2306
+ }
2307
+
2308
+ function removeImage() {
2309
+ state.uploadedImageFileUrl = '';
2310
+ state.uploadedImageName = '';
2311
+ document.getElementById('image-upload').value = '';
2312
+ document.getElementById('image-attach-name').textContent = '';
2313
+ document.getElementById('btn-remove-image').style.display = 'none';
2314
+ }
2315
+
2316
+ // ═══════════════════════════════════════════════════════
2317
+ // WEB SEARCH
2318
+ // ═══════════════════════════════════════════════════════
2319
+ async function doWebSearch() {
2320
+ const query = document.getElementById('search-input').value.trim();
2321
+ if (!query) return;
2322
+
2323
+ const resultsContainer = document.getElementById('search-results');
2324
+ resultsContainer.innerHTML = '<div class="search-results-empty" style="color:var(--amber);">Searching...</div>';
2325
+
2326
+ try {
2327
+ const resp = await fetch('/gradio_api/call/web_search', {
2328
+ method: 'POST',
2329
+ headers: { 'Content-Type': 'application/json' },
2330
+ body: JSON.stringify({ data: [query] })
2331
+ });
2332
+ const { event_id } = await resp.json();
2333
+ const eventSource = new EventSource(`/gradio_api/call/web_search/${event_id}`);
2334
+
2335
+ eventSource.addEventListener('complete', (e) => {
2336
+ const dataArray = JSON.parse(e.data);
2337
+ const result = JSON.parse(dataArray[0]);
2338
+ if (result.success) {
2339
+ state.lastSearchResults = result.results;
2340
+ renderSearchResults(result.results);
2341
+ } else {
2342
+ resultsContainer.innerHTML = `<div class="search-results-empty">${escapeHtml(result.message)}</div>`;
2343
+ }
2344
+ eventSource.close();
2345
+ });
2346
+ eventSource.addEventListener('error', (e) => {
2347
+ resultsContainer.innerHTML = '<div class="search-results-empty">Search failed</div>';
2348
+ eventSource.close();
2349
+ });
2350
+ } catch (err) {
2351
+ resultsContainer.innerHTML = `<div class="search-results-empty">Error: ${err.message}</div>`;
2352
+ }
2353
+ }
2354
+
2355
+ function searchAndGenerate() {
2356
+ const input = document.getElementById('chat-input');
2357
+ const prompt = input.value.trim();
2358
+ if (!prompt || state.isGenerating) return;
2359
+ input.value = '';
2360
+ autoResize();
2361
+ state.searchEnabled = true;
2362
+ sendMessage(prompt);
2363
+ // Reset after sending
2364
+ state.searchEnabled = false;
2365
+ }
2366
+
2367
+ function renderSearchResults(results) {
2368
+ const container = document.getElementById('search-results');
2369
+ if (!results || results.length === 0) {
2370
+ container.innerHTML = '<div class="search-results-empty">No results found.</div>';
2371
+ return;
2372
+ }
2373
+ let html = '';
2374
+ results.forEach((r) => {
2375
+ html += `<div class="search-result-item">
2376
+ <a class="search-result-title" href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>
2377
+ <span class="search-result-url">${escapeHtml(r.url)}</span>
2378
+ <div class="search-result-snippet">${escapeHtml(r.snippet)}</div>
2379
+ </div>`;
2380
+ });
2381
+ container.innerHTML = html;
2382
+ }
2383
+
2384
+ // ═══════════════════════════════════════════════════════
2385
+ // SEARCH SOURCE BADGE (Grok-style inline in chat)
2386
+ // ═══════════════════════════════════════════════════════
2387
+ function renderSearchSourceBadge(results, expanded) {
2388
+ const div = document.getElementById('current-assistant-msg');
2389
+ if (!div) return;
2390
+ const container = div.querySelector('.search-source-container');
2391
+ if (!container || !results || results.length === 0) return;
2392
+
2393
+ const count = results.length;
2394
+ const openClass = expanded ? ' open' : '';
2395
+
2396
+ // Build source items HTML
2397
+ let sourceItems = '';
2398
+ results.forEach((r) => {
2399
+ let domain = '';
2400
+ try { domain = new URL(r.url).hostname.replace('www.', ''); } catch(e) { domain = r.host_name || r.url; }
2401
+ const faviconUrl = r.favicon || `https://www.google.com/s2/favicons?domain=${domain}&sz=16`;
2402
+ sourceItems += `<div class="source-item">
2403
+ <img class="source-favicon" src="${escapeHtml(faviconUrl)}" alt="" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';">
2404
+ <div class="source-favicon-placeholder" style="display:none;">${escapeHtml(domain.charAt(0).toUpperCase())}</div>
2405
+ <div class="source-info">
2406
+ <a class="source-title" href="${escapeHtml(r.url)}" target="_blank" rel="noopener" onclick="event.stopPropagation();">${escapeHtml(r.title)}</a>
2407
+ <span class="source-domain">${escapeHtml(domain)}</span>
2408
+ <div class="source-snippet">${escapeHtml(r.snippet)}</div>
2409
+ </div>
2410
+ </div>`;
2411
+ });
2412
+
2413
+ container.innerHTML = `
2414
+ <div class="search-source-badge${openClass}" onclick="toggleSearchPanel(this)">
2415
+ <span class="badge-icon">&#128269;</span>
2416
+ <span>Searched</span>
2417
+ <span class="badge-count">${count} source${count !== 1 ? 's' : ''}</span>
2418
+ <span class="badge-arrow">&#9660;</span>
2419
+ </div>
2420
+ <div class="search-source-panel${openClass}">
2421
+ ${sourceItems}
2422
+ </div>
2423
+ `;
2424
+
2425
+ scrollToBottom();
2426
+ }
2427
+
2428
+ function toggleSearchPanel(badge) {
2429
+ const panel = badge.nextElementSibling;
2430
+ const isOpen = badge.classList.contains('open');
2431
+ badge.classList.toggle('open', !isOpen);
2432
+ panel.classList.toggle('open', !isOpen);
2433
+ state.searchPanelExpanded = !isOpen;
2434
+ scrollToBottom();
2435
+ }
2436
+
2437
+ // ═══════════════════════════════════════════════════════
2438
+ // HUGGINGFACE PUSH
2439
+ // ══════════════════════════��════════════════════════════
2440
+ async function pushToHuggingFace() {
2441
+ const repoName = document.getElementById('hf-repo-name').value.trim();
2442
+ const hfToken = document.getElementById('hf-token').value.trim();
2443
+ const spaceSdk = document.getElementById('hf-space-sdk').value;
2444
+ const statusEl = document.getElementById('deploy-status');
2445
+
2446
+ if (!repoName) {
2447
+ statusEl.className = 'deploy-status error';
2448
+ statusEl.textContent = 'Please enter a repository name.';
2449
+ statusEl.style.display = 'block';
2450
+ return;
2451
+ }
2452
+ if (!hfToken) {
2453
+ statusEl.className = 'deploy-status error';
2454
+ statusEl.textContent = 'Please enter your HuggingFace token.';
2455
+ statusEl.style.display = 'block';
2456
+ return;
2457
+ }
2458
+
2459
+ if (!state.executionContext || !state.executionContext.code) {
2460
+ statusEl.className = 'deploy-status error';
2461
+ statusEl.textContent = 'No code to push. Generate some code first.';
2462
+ statusEl.style.display = 'block';
2463
+ return;
2464
+ }
2465
+
2466
+ statusEl.className = 'deploy-status working';
2467
+ statusEl.textContent = 'Pushing to HuggingFace...';
2468
+ statusEl.style.display = 'block';
2469
+
2470
+ const btn = document.getElementById('btn-push-hf');
2471
+ btn.disabled = true;
2472
+
2473
+ try {
2474
+ const execContextJSON = JSON.stringify(state.executionContext);
2475
+
2476
+ const resp = await fetch('/gradio_api/call/push_hf', {
2477
+ method: 'POST',
2478
+ headers: { 'Content-Type': 'application/json' },
2479
+ body: JSON.stringify({
2480
+ data: [execContextJSON, repoName, hfToken, spaceSdk, 'true']
2481
+ })
2482
+ });
2483
+
2484
+ if (!resp.ok) throw new Error(`API error: ${resp.status}`);
2485
+
2486
+ const { event_id } = await resp.json();
2487
+
2488
+ const eventSource = new EventSource(`/gradio_api/call/push_hf/${event_id}`);
2489
+
2490
+ eventSource.addEventListener('complete', (e) => {
2491
+ try {
2492
+ const dataArray = JSON.parse(e.data);
2493
+ const result = JSON.parse(dataArray[0]);
2494
+
2495
+ if (result.success) {
2496
+ statusEl.className = 'deploy-status success';
2497
+ statusEl.innerHTML = `\u2713 ${result.message}<br><a href="${result.url}" target="_blank" rel="noopener">${result.url} \u2197</a>`;
2498
+ } else {
2499
+ statusEl.className = 'deploy-status error';
2500
+ statusEl.textContent = `\u2717 ${result.message}`;
2501
+ }
2502
+ } catch (err) {
2503
+ statusEl.className = 'deploy-status error';
2504
+ statusEl.textContent = `Parse error: ${err.message}`;
2505
+ }
2506
+ eventSource.close();
2507
+ btn.disabled = false;
2508
+ });
2509
+
2510
+ eventSource.addEventListener('error', (e) => {
2511
+ statusEl.className = 'deploy-status error';
2512
+ statusEl.textContent = `Push failed: ${e.data || 'Unknown error'}`;
2513
+ eventSource.close();
2514
+ btn.disabled = false;
2515
+ });
2516
+
2517
+ } catch (err) {
2518
+ statusEl.className = 'deploy-status error';
2519
+ statusEl.textContent = `Push failed: ${err.message}`;
2520
+ btn.disabled = false;
2521
+ }
2522
+ }
2523
+ </script>
2524
+ </body>
2525
+ </html>
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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
7
+ requests>=2.31.0
8
+ beautifulsoup4>=4.12.0
9
+ Pillow>=10.0
10
+ torchvision>=0.16.0