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

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. app.py +408 -59
  2. index.html +227 -12
  3. requirements.txt +3 -0
app.py CHANGED
@@ -3,6 +3,8 @@
3
  Uses MiniCPM5-1B for local inference (no external APIs).
4
  Supports generating fullstack applications in any language.
5
  Can push generated projects to HuggingFace Hub.
 
 
6
  """
7
 
8
  from __future__ import annotations
@@ -19,6 +21,7 @@ import tempfile
19
  import textwrap
20
  import threading
21
  import time
 
22
  import zipfile
23
  from collections.abc import Iterator
24
  from dataclasses import dataclass, field
@@ -35,6 +38,7 @@ MODEL_URL = "https://huggingface.co/openbmb/MiniCPM5-1B"
35
  DEFAULT_TEMPERATURE = 0.6
36
  DEFAULT_MAX_TOKENS = 4096
37
  PY_TIMEOUT_S = 15
 
38
  PY_MEM_LIMIT_MB = 1024
39
  MAX_STDIO_CHARS = 16_000
40
  OUTPUT_PNG = "output.png"
@@ -49,7 +53,7 @@ logging.basicConfig(level=logging.INFO)
49
  # ─── Supported Languages & Frameworks ───────────────────────────────────
50
 
51
  LANGUAGE_OPTIONS = [
52
- ("Python", ["Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]),
53
  ("JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]),
54
  ("TypeScript", ["React", "Next.js", "Express.js", "NestJS"]),
55
  ("HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]),
@@ -72,6 +76,7 @@ When the user asks you to build an application:
72
  2. Include all necessary files for the project to run
73
  3. Add proper error handling and comments
74
  4. For web apps, make the UI responsive and modern
 
75
 
76
  FILE OUTPUT FORMAT - IMPORTANT:
77
  When generating multi-file projects, wrap each file in this format:
@@ -90,11 +95,31 @@ etc.
90
 
91
  When generating web apps with HTML/CSS/JS, return a single self-contained HTML document with all CSS and JavaScript inline. Make the page fully responsive: html/body at margin:0 and 100% width/height, use flexbox/grid layouts, and size any canvas to its container.
92
 
93
- For Python, prefer standard library or common packages. Do not use network calls, subprocesses, shell commands, or long-running loops in demo code.
 
 
 
 
 
 
 
 
94
  """
95
 
96
  # Curated starter prompts
97
  EXAMPLE_PROMPTS: list[tuple[str, str, str, str]] = [
 
 
 
 
 
 
 
 
 
 
 
 
98
  (
99
  "🌐 React Todo App",
100
  "Build a React todo application with add, delete, mark complete, and filter functionality. Use modern hooks and a clean responsive UI.",
@@ -119,21 +144,130 @@ EXAMPLE_PROMPTS: list[tuple[str, str, str, str]] = [
119
  "HTML/CSS/JS",
120
  "Vanilla",
121
  ),
122
- (
123
- "🚀 Express API",
124
- "Build an Express.js REST API with user authentication endpoints (register, login, profile), JWT token handling, and input validation.",
125
- "JavaScript",
126
- "Express.js",
127
- ),
128
- (
129
- "✅ Todo App",
130
- "Create a self-contained HTML/CSS/JavaScript todo app: add tasks, mark them complete, delete them, filter by all/active/completed, and show a live count of remaining tasks, with a clean, modern, responsive UI.",
131
- "HTML/CSS/JS",
132
- "Vanilla",
133
- ),
134
  ]
135
 
136
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
  # ─── Model Loading ──────────────────────────────────────────────────────
138
 
139
  _model = None
@@ -358,6 +492,11 @@ def _normalize_language(target_language: str | None, fence_lang: str | None) ->
358
  return lang
359
 
360
 
 
 
 
 
 
361
  # ─── Python Execution ───────────────────────────────────────────────────
362
 
363
  @dataclass
@@ -502,6 +641,122 @@ def run_python(code: str) -> PythonExecutionResult:
502
  )
503
 
504
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
505
  # ─── Web Document ───────────────────────────────────────────────────────
506
 
507
  def _web_document(code: str, fence_lang: str | None) -> str:
@@ -532,10 +787,7 @@ def build_iframe(code: str, fence_lang: str | None = None) -> str:
532
  # ─── Project Packaging ──────────────────────────────────────────────────
533
 
534
  def create_project_zip(files: dict[str, str], project_name: str) -> str:
535
- """Create a ZIP file from extracted project files.
536
-
537
- Returns path to the created ZIP file.
538
- """
539
  zip_dir = tempfile.mkdtemp(prefix="fullstack_project_")
540
  zip_path = os.path.join(zip_dir, f"{project_name}.zip")
541
 
@@ -546,23 +798,6 @@ def create_project_zip(files: dict[str, str], project_name: str) -> str:
546
  return zip_path
547
 
548
 
549
- def create_project_directory(files: dict[str, str], project_name: str) -> str:
550
- """Create a directory with project files for HuggingFace push.
551
-
552
- Returns path to the created project directory.
553
- """
554
- proj_dir = tempfile.mkdtemp(prefix="fullstack_deploy_")
555
- target_dir = os.path.join(proj_dir, project_name)
556
- os.makedirs(target_dir, exist_ok=True)
557
-
558
- for filepath, content in files.items():
559
- full_path = os.path.join(target_dir, filepath)
560
- os.makedirs(os.path.dirname(full_path), exist_ok=True)
561
- Path(full_path).write_text(content, encoding="utf-8")
562
-
563
- return target_dir
564
-
565
-
566
  # ─── HuggingFace Hub Push ───────────────────────────────────────────────
567
 
568
  def push_to_huggingface(
@@ -573,35 +808,20 @@ def push_to_huggingface(
573
  space_sdk: str = "static",
574
  is_space: bool = True,
575
  ) -> dict[str, Any]:
576
- """Push generated project to HuggingFace Hub.
577
-
578
- Args:
579
- files: Dict of {filepath: content}
580
- project_name: Name of the project
581
- repo_name: HuggingFace repo name (e.g., "username/my-app")
582
- hf_token: HuggingFace API token
583
- space_sdk: SDK for Space ("gradio", "streamlit", "static", "docker")
584
- is_space: Whether to create a Space (True) or Model repo (False)
585
-
586
- Returns:
587
- Dict with status and URL info
588
- """
589
  try:
590
  from huggingface_hub import HfApi, create_repo
591
 
592
  api = HfApi(token=hf_token)
593
 
594
- # Parse username/repo
595
  if "/" in repo_name:
596
  namespace, name = repo_name.split("/", 1)
597
  else:
598
- # Use the authenticated user's namespace
599
  user_info = api.whoami()
600
  namespace = user_info["name"]
601
  name = repo_name
602
  repo_name = f"{namespace}/{name}"
603
 
604
- # Create repo
605
  try:
606
  if is_space:
607
  create_repo(
@@ -621,7 +841,6 @@ def push_to_huggingface(
621
  except Exception as e:
622
  logger.warning("Repo creation warning: %s", e)
623
 
624
- # Upload files
625
  with tempfile.TemporaryDirectory(prefix="hf_push_") as tmp_dir:
626
  for filepath, content in files.items():
627
  full_path = os.path.join(tmp_dir, filepath)
@@ -646,6 +865,26 @@ Generated by Fullstack Code Builder using {MODEL_ID}.
646
  """
647
  Path(readme_path).write_text(readme_content, encoding="utf-8")
648
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
649
  api.upload_folder(
650
  folder_path=tmp_dir,
651
  repo_id=repo_name,
@@ -725,18 +964,37 @@ def _targeted_prompt(
725
  target_language: str,
726
  target_framework: str = "",
727
  execution_context: dict[str, Any] | None = None,
 
728
  ) -> str:
729
  iteration_context = _iteration_context(execution_context)
730
  context_block = f"\n\n{iteration_context}" if iteration_context else ""
731
 
 
 
 
 
732
  framework_hint = f" using {target_framework}" if target_framework else ""
733
 
 
 
 
 
 
 
 
 
 
 
 
 
734
  return (
735
  f"Target: {target_language}{framework_hint}. Generate a complete, runnable application. "
736
  "If the user asks for a web app, include all HTML/CSS/JS. "
737
  "If they ask for a backend, include the server code and any API definitions. "
738
  "For single-file apps, use a single code block. For multi-file projects, use the @@FILE: format. "
739
  "Make the code complete, working, and well-structured."
 
 
740
  f"{context_block}\n\n"
741
  f"User request:\n{prompt}"
742
  )
@@ -747,7 +1005,28 @@ def _targeted_prompt(
747
  def _run_extracted_code(
748
  code: str,
749
  target: str,
 
750
  ) -> tuple[str, str, str | None, str, str]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  if target == "python":
752
  result = run_python(code)
753
  if result.timed_out:
@@ -809,6 +1088,33 @@ async def serve_download(filename: str):
809
  return HTMLResponse("Not found", status_code=404)
810
 
811
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
812
  @app.api(name="chat", concurrency_limit=2)
813
  def handle_chat(
814
  prompt: str,
@@ -816,6 +1122,7 @@ def handle_chat(
816
  target_framework: str,
817
  history_json: str,
818
  exec_context_json: str,
 
819
  ) -> str:
820
  """Stream chat responses with code execution. Yields JSON strings."""
821
  history = json.loads(history_json) if history_json else []
@@ -866,11 +1173,33 @@ def handle_chat(
866
  "execution": execution_context,
867
  })
868
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  # Build messages for model
870
  model_history = list(history[:-1])
871
  model_history[-1] = {
872
  "role": "user",
873
- "content": _targeted_prompt(prompt, target_language, target_framework, execution_context),
874
  }
875
  messages = _chat_history_to_messages(model_history)
876
 
@@ -922,10 +1251,26 @@ def handle_chat(
922
  "execution": execution_context,
923
  })
924
 
925
- # Execute if Python
926
  stdout, stderr, image_path, status_text, status_state = "", "", None, "Preview ready", "success"
 
 
 
927
  if target == "python" and code:
928
- stdout, stderr, image_path, status_text, status_state = _run_extracted_code(code, target)
 
 
 
 
 
 
 
 
 
 
 
 
 
929
 
930
  # Register image for serving
931
  image_url = None
@@ -939,7 +1284,6 @@ def handle_chat(
939
  project_files = multi_files if multi_files else {}
940
 
941
  if project_files:
942
- # Multi-file project: create ZIP
943
  project_name = "generated-project"
944
  zip_path = create_project_zip(project_files, project_name)
945
  zip_filename = f"{project_name}.zip"
@@ -968,11 +1312,13 @@ def handle_chat(
968
  "image_path": image_path,
969
  "status": status_text,
970
  "language": fence_lang or target,
971
- "suggested_tab": "preview" if (image_path or is_web) else "console",
972
  "download_url": download_url,
973
  "project_files": project_files,
974
  "is_web": is_web,
975
  "web_code": web_code,
 
 
976
  }
977
 
978
  yield json.dumps({
@@ -998,7 +1344,6 @@ def handle_push_hf(
998
  project_files = execution_context.get("project_files", {})
999
 
1000
  if not project_files:
1001
- # If no multi-file, try single code
1002
  code = execution_context.get("code", "")
1003
  if not code:
1004
  yield json.dumps({
@@ -1009,6 +1354,7 @@ def handle_push_hf(
1009
  return
1010
 
1011
  lang = execution_context.get("language", "python")
 
1012
  ext_map = {
1013
  "python": "app.py", "py": "app.py",
1014
  "javascript": "index.js", "js": "index.js",
@@ -1018,7 +1364,10 @@ def handle_push_hf(
1018
  filename = ext_map.get(lang, "app.py")
1019
  project_files = {filename: code}
1020
 
1021
- # Extract project name from repo_name
 
 
 
1022
  project_name = repo_name.split("/")[-1] if "/" in repo_name else repo_name
1023
 
1024
  result = push_to_huggingface(
 
3
  Uses MiniCPM5-1B for local inference (no external APIs).
4
  Supports generating fullstack applications in any language.
5
  Can push generated projects to HuggingFace Hub.
6
+ Web search via Google scraping (no API keys needed).
7
+ Gradio app support for Python.
8
  """
9
 
10
  from __future__ import annotations
 
21
  import textwrap
22
  import threading
23
  import time
24
+ import urllib.parse
25
  import zipfile
26
  from collections.abc import Iterator
27
  from dataclasses import dataclass, field
 
38
  DEFAULT_TEMPERATURE = 0.6
39
  DEFAULT_MAX_TOKENS = 4096
40
  PY_TIMEOUT_S = 15
41
+ GRADIO_TIMEOUT_S = 30
42
  PY_MEM_LIMIT_MB = 1024
43
  MAX_STDIO_CHARS = 16_000
44
  OUTPUT_PNG = "output.png"
 
53
  # ─── Supported Languages & Frameworks ───────────────────────────────────
54
 
55
  LANGUAGE_OPTIONS = [
56
+ ("Python", ["Gradio", "Flask", "Django", "FastAPI", "Streamlit", "Plain Python"]),
57
  ("JavaScript", ["React", "Vue.js", "Next.js", "Express.js", "Node.js", "Vanilla JS"]),
58
  ("TypeScript", ["React", "Next.js", "Express.js", "NestJS"]),
59
  ("HTML/CSS/JS", ["Tailwind CSS", "Bootstrap", "Vanilla"]),
 
76
  2. Include all necessary files for the project to run
77
  3. Add proper error handling and comments
78
  4. For web apps, make the UI responsive and modern
79
+ 5. For Gradio apps, use gradio library and create a complete working app with gr.Interface or gr.Blocks
80
 
81
  FILE OUTPUT FORMAT - IMPORTANT:
82
  When generating multi-file projects, wrap each file in this format:
 
95
 
96
  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.
97
 
98
+ When generating Gradio apps, create a complete app.py with:
99
+ - import gradio as gr
100
+ - Define the interface using gr.Interface() or gr.Blocks()
101
+ - Call iface.launch(server_name="0.0.0.0", server_port=7860) at the end
102
+ - Include all necessary processing logic inline
103
+
104
+ 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).
105
+
106
+ 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.
107
  """
108
 
109
  # Curated starter prompts
110
  EXAMPLE_PROMPTS: list[tuple[str, str, str, str]] = [
111
+ (
112
+ "🎨 Gradio Image Filter",
113
+ "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.",
114
+ "Python",
115
+ "Gradio",
116
+ ),
117
+ (
118
+ "🤖 Gradio Chat App",
119
+ "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.",
120
+ "Python",
121
+ "Gradio",
122
+ ),
123
  (
124
  "🌐 React Todo App",
125
  "Build a React todo application with add, delete, mark complete, and filter functionality. Use modern hooks and a clean responsive UI.",
 
144
  "HTML/CSS/JS",
145
  "Vanilla",
146
  ),
 
 
 
 
 
 
 
 
 
 
 
 
147
  ]
148
 
149
 
150
+ # ─── Web Search (Google Scraping — No API) ──────────────────────────────
151
+
152
+ def web_search_google(query: str, num_results: int = 8) -> list[dict[str, str]]:
153
+ """Search Google by scraping the results page. No API key needed.
154
+
155
+ Returns a list of dicts with keys: title, url, snippet.
156
+ Uses requests with a browser-like User-Agent to avoid captchas.
157
+ """
158
+ try:
159
+ import requests
160
+ from bs4 import BeautifulSoup
161
+
162
+ encoded_query = urllib.parse.quote_plus(query)
163
+ url = f"https://www.google.com/search?q={encoded_query}&num={num_results + 2}&hl=en"
164
+
165
+ headers = {
166
+ "User-Agent": (
167
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
168
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
169
+ "Chrome/120.0.0.0 Safari/537.36"
170
+ ),
171
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
172
+ "Accept-Language": "en-US,en;q=0.5",
173
+ "Accept-Encoding": "gzip, deflate",
174
+ "DNT": "1",
175
+ "Connection": "keep-alive",
176
+ "Upgrade-Insecure-Requests": "1",
177
+ }
178
+
179
+ resp = requests.get(url, headers=headers, timeout=10, allow_redirects=True)
180
+ resp.raise_for_status()
181
+
182
+ soup = BeautifulSoup(resp.text, "html.parser")
183
+ results: list[dict[str, str]] = []
184
+
185
+ # Parse Google search results
186
+ # Google uses various class names; we try multiple selectors
187
+ for g_div in soup.select("div.g, div[data-sokoban-container], div.yuRUbf"):
188
+ title_el = g_div.select_one("h3")
189
+ link_el = g_div.select_one("a[href]")
190
+ snippet_el = g_div.select_one("div.VwiC3b, span.aCOpRe, div[data-sncf]")
191
+
192
+ if not title_el or not link_el:
193
+ continue
194
+
195
+ href = link_el.get("href", "")
196
+ # Google sometimes prefixes URLs; extract the real URL
197
+ if href.startswith("/url?q="):
198
+ real_url = urllib.parse.parse_qs(urllib.parse.urlparse(href).query).get("q", [href])[0]
199
+ elif href.startswith("http"):
200
+ real_url = href
201
+ else:
202
+ continue
203
+
204
+ # Skip Google-internal URLs
205
+ if "google.com" in real_url or "googleusercontent.com" in real_url:
206
+ continue
207
+
208
+ title = title_el.get_text(strip=True)
209
+ snippet = snippet_el.get_text(strip=True) if snippet_el else ""
210
+
211
+ if title and real_url:
212
+ results.append({
213
+ "title": title,
214
+ "url": real_url,
215
+ "snippet": snippet,
216
+ })
217
+
218
+ if len(results) >= num_results:
219
+ break
220
+
221
+ # Fallback: try parsing from <a> tags with data-ved attribute
222
+ if not results:
223
+ for a_tag in soup.select("a[data-ved]"):
224
+ href = a_tag.get("href", "")
225
+ if not href.startswith("http"):
226
+ continue
227
+ if "google.com" in href:
228
+ continue
229
+
230
+ title_el = a_tag.select_one("h3, span")
231
+ title = title_el.get_text(strip=True) if title_el else a_tag.get_text(strip=True)[:100]
232
+ snippet = ""
233
+
234
+ if title and href:
235
+ results.append({
236
+ "title": title,
237
+ "url": href,
238
+ "snippet": snippet,
239
+ })
240
+
241
+ if len(results) >= num_results:
242
+ break
243
+
244
+ logger.info("Web search for '%s' returned %d results", query, len(results))
245
+ return results
246
+
247
+ except ImportError:
248
+ logger.warning("requests or beautifulsoup4 not installed for web search")
249
+ return []
250
+ except Exception as exc:
251
+ logger.exception("Web search failed: %s", exc)
252
+ return []
253
+
254
+
255
+ def format_search_results(results: list[dict[str, str]]) -> str:
256
+ """Format search results into a text block for model context."""
257
+ if not results:
258
+ return "No search results found."
259
+
260
+ parts = ["Here are the web search results for reference:\n"]
261
+ for i, r in enumerate(results, 1):
262
+ parts.append(f"{i}. {r['title']}")
263
+ parts.append(f" URL: {r['url']}")
264
+ if r["snippet"]:
265
+ parts.append(f" {r['snippet']}")
266
+ parts.append("")
267
+
268
+ return "\n".join(parts)
269
+
270
+
271
  # ─── Model Loading ──────────────────────────────────────────────────────
272
 
273
  _model = None
 
492
  return lang
493
 
494
 
495
+ def _is_gradio_code(code: str) -> bool:
496
+ """Detect if Python code is a Gradio app."""
497
+ return bool(re.search(r"import\s+gradio|from\s+gradio\s+import|gr\.\s*(Interface|Blocks|TabbedInterface|ChatInterface|App)", code))
498
+
499
+
500
  # ─── Python Execution ───────────────────────────────────────────────────
501
 
502
  @dataclass
 
641
  )
642
 
643
 
644
+ # ─── Gradio App Runner ─────────────────────────────────────────────────
645
+
646
+ # Registry for running Gradio subprocesses
647
+ _running_gradio_procs: dict[str, subprocess.Popen] = {}
648
+
649
+
650
+ def run_gradio_app(code: str, port: int = 7861) -> dict[str, Any]:
651
+ """Launch a Gradio app as a subprocess and return its URL.
652
+
653
+ The Gradio app is run on the specified port. We modify the code
654
+ to ensure it launches on the correct port and is accessible.
655
+ """
656
+ # Kill any previously running Gradio app
657
+ for pid, proc in list(_running_gradio_procs.items()):
658
+ try:
659
+ proc.terminate()
660
+ proc.wait(timeout=3)
661
+ except Exception:
662
+ try:
663
+ proc.kill()
664
+ except Exception:
665
+ pass
666
+ _running_gradio_procs.clear()
667
+
668
+ # Patch the code: ensure launch uses correct server_name and server_port
669
+ patched_code = code
670
+
671
+ # Replace .launch() with correct params
672
+ patched_code = re.sub(
673
+ r"(\w+)\.launch\([^)]*\)",
674
+ f'\\1.launch(server_name="0.0.0.0", server_port={port}, share=False)',
675
+ patched_code,
676
+ )
677
+
678
+ # If no .launch() found, add one
679
+ if ".launch(" not in patched_code:
680
+ # Add launch at the end if missing
681
+ patched_code += f'\n\nif __name__ == "__main__":\n iface.launch(server_name="0.0.0.0", server_port={port}, share=False)\n'
682
+
683
+ with tempfile.TemporaryDirectory(prefix="gradio_app_") as tmp:
684
+ app_path = Path(tmp) / "gradio_app.py"
685
+ app_path.write_text(patched_code, encoding="utf-8")
686
+
687
+ env = {
688
+ **os.environ,
689
+ "PYTHONUNBUFFERED": "1",
690
+ "GRADIO_SERVER_NAME": "0.0.0.0",
691
+ "GRADIO_SERVER_PORT": str(port),
692
+ }
693
+
694
+ try:
695
+ proc = subprocess.Popen(
696
+ [sys.executable, str(app_path)],
697
+ cwd=tmp,
698
+ env=env,
699
+ stdout=subprocess.PIPE,
700
+ stderr=subprocess.PIPE,
701
+ text=True,
702
+ )
703
+
704
+ proc_id = f"gradio_{port}"
705
+ _running_gradio_procs[proc_id] = proc
706
+
707
+ # Wait a bit for the server to start
708
+ import time as _time
709
+ _time.sleep(3)
710
+
711
+ # Check if process is still running
712
+ poll = proc.poll()
713
+ if poll is not None:
714
+ stdout = proc.stdout.read() if proc.stdout else ""
715
+ stderr = proc.stderr.read() if proc.stderr else ""
716
+ return {
717
+ "success": False,
718
+ "url": "",
719
+ "message": f"Gradio app exited with code {poll}",
720
+ "stdout": stdout[-2000:] if stdout else "",
721
+ "stderr": stderr[-2000:] if stderr else "",
722
+ }
723
+
724
+ gradio_url = f"http://localhost:{port}"
725
+ return {
726
+ "success": True,
727
+ "url": gradio_url,
728
+ "message": f"Gradio app running at {gradio_url}",
729
+ "port": port,
730
+ }
731
+
732
+ except Exception as exc:
733
+ logger.exception("Failed to launch Gradio app")
734
+ return {
735
+ "success": False,
736
+ "url": "",
737
+ "message": f"Failed to launch: {exc}",
738
+ }
739
+
740
+
741
+ def stop_gradio_app() -> dict[str, Any]:
742
+ """Stop any running Gradio app subprocess."""
743
+ stopped = 0
744
+ for pid, proc in list(_running_gradio_procs.items()):
745
+ try:
746
+ proc.terminate()
747
+ proc.wait(timeout=3)
748
+ stopped += 1
749
+ except Exception:
750
+ try:
751
+ proc.kill()
752
+ stopped += 1
753
+ except Exception:
754
+ pass
755
+ _running_gradio_procs.clear()
756
+
757
+ return {"success": True, "message": f"Stopped {stopped} Gradio app(s)"}
758
+
759
+
760
  # ─── Web Document ───────────────────────────────────────────────────────
761
 
762
  def _web_document(code: str, fence_lang: str | None) -> str:
 
787
  # ─── Project Packaging ──────────────────────────────────────────────────
788
 
789
  def create_project_zip(files: dict[str, str], project_name: str) -> str:
790
+ """Create a ZIP file from extracted project files."""
 
 
 
791
  zip_dir = tempfile.mkdtemp(prefix="fullstack_project_")
792
  zip_path = os.path.join(zip_dir, f"{project_name}.zip")
793
 
 
798
  return zip_path
799
 
800
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
801
  # ─── HuggingFace Hub Push ───────────────────────────────────────────────
802
 
803
  def push_to_huggingface(
 
808
  space_sdk: str = "static",
809
  is_space: bool = True,
810
  ) -> dict[str, Any]:
811
+ """Push generated project to HuggingFace Hub."""
 
 
 
 
 
 
 
 
 
 
 
 
812
  try:
813
  from huggingface_hub import HfApi, create_repo
814
 
815
  api = HfApi(token=hf_token)
816
 
 
817
  if "/" in repo_name:
818
  namespace, name = repo_name.split("/", 1)
819
  else:
 
820
  user_info = api.whoami()
821
  namespace = user_info["name"]
822
  name = repo_name
823
  repo_name = f"{namespace}/{name}"
824
 
 
825
  try:
826
  if is_space:
827
  create_repo(
 
841
  except Exception as e:
842
  logger.warning("Repo creation warning: %s", e)
843
 
 
844
  with tempfile.TemporaryDirectory(prefix="hf_push_") as tmp_dir:
845
  for filepath, content in files.items():
846
  full_path = os.path.join(tmp_dir, filepath)
 
865
  """
866
  Path(readme_path).write_text(readme_content, encoding="utf-8")
867
 
868
+ # Add requirements.txt for Python/Gradio projects
869
+ req_path = os.path.join(tmp_dir, "requirements.txt")
870
+ if not os.path.exists(req_path):
871
+ has_python = any(
872
+ f.endswith(".py") for f in files.keys()
873
+ )
874
+ if has_python:
875
+ reqs = ["gradio>=4.0.0"]
876
+ # Detect common dependencies
877
+ all_code = "\n".join(files.values())
878
+ if "matplotlib" in all_code:
879
+ reqs.append("matplotlib>=3.8")
880
+ if "PIL" in all_code or "Pillow" in all_code:
881
+ reqs.append("Pillow>=10.0")
882
+ if "numpy" in all_code:
883
+ reqs.append("numpy>=1.24")
884
+ if "pandas" in all_code:
885
+ reqs.append("pandas>=2.0")
886
+ Path(req_path).write_text("\n".join(reqs) + "\n", encoding="utf-8")
887
+
888
  api.upload_folder(
889
  folder_path=tmp_dir,
890
  repo_id=repo_name,
 
964
  target_language: str,
965
  target_framework: str = "",
966
  execution_context: dict[str, Any] | None = None,
967
+ search_context: str = "",
968
  ) -> str:
969
  iteration_context = _iteration_context(execution_context)
970
  context_block = f"\n\n{iteration_context}" if iteration_context else ""
971
 
972
+ search_block = ""
973
+ if search_context:
974
+ search_block = f"\n\n{search_context}\n\nUse the above search results to inform your code generation if relevant."
975
+
976
  framework_hint = f" using {target_framework}" if target_framework else ""
977
 
978
+ gradio_hint = ""
979
+ if target_framework == "Gradio":
980
+ gradio_hint = (
981
+ "\n\nIMPORTANT: This is a Gradio app. Create a complete Python script that:\n"
982
+ "- Imports gradio as gr\n"
983
+ "- Defines the UI using gr.Interface() or gr.Blocks()\n"
984
+ "- Includes all processing logic inline\n"
985
+ "- Calls .launch(server_name='0.0.0.0', server_port=7860) at the end\n"
986
+ "- Uses only standard library + gradio + common packages (PIL, matplotlib, numpy)\n"
987
+ "- Make the UI clean, modern, and functional"
988
+ )
989
+
990
  return (
991
  f"Target: {target_language}{framework_hint}. Generate a complete, runnable application. "
992
  "If the user asks for a web app, include all HTML/CSS/JS. "
993
  "If they ask for a backend, include the server code and any API definitions. "
994
  "For single-file apps, use a single code block. For multi-file projects, use the @@FILE: format. "
995
  "Make the code complete, working, and well-structured."
996
+ f"{gradio_hint}"
997
+ f"{search_block}"
998
  f"{context_block}\n\n"
999
  f"User request:\n{prompt}"
1000
  )
 
1005
  def _run_extracted_code(
1006
  code: str,
1007
  target: str,
1008
+ framework: str = "",
1009
  ) -> tuple[str, str, str | None, str, str]:
1010
+ """Run extracted code. For Gradio apps, launch as a subprocess server."""
1011
+ if target == "python" and _is_gradio_code(code):
1012
+ result = run_gradio_app(code)
1013
+ if result["success"]:
1014
+ return (
1015
+ result.get("stdout", ""),
1016
+ f"Gradio app running at {result['url']}",
1017
+ None,
1018
+ f"Gradio running at {result['url']}",
1019
+ "success",
1020
+ )
1021
+ else:
1022
+ return (
1023
+ result.get("stdout", ""),
1024
+ result.get("stderr", result.get("message", "Gradio launch failed")),
1025
+ None,
1026
+ "Gradio launch failed",
1027
+ "error",
1028
+ )
1029
+
1030
  if target == "python":
1031
  result = run_python(code)
1032
  if result.timed_out:
 
1088
  return HTMLResponse("Not found", status_code=404)
1089
 
1090
 
1091
+ @app.api(name="web_search", concurrency_limit=4)
1092
+ def handle_web_search(query: str) -> str:
1093
+ """Search the web using Google scraping. No API key needed."""
1094
+ query = (query or "").strip()
1095
+ if not query:
1096
+ yield json.dumps({"success": False, "results": [], "message": "Empty search query"})
1097
+ return
1098
+
1099
+ try:
1100
+ results = web_search_google(query, num_results=8)
1101
+ formatted = format_search_results(results)
1102
+
1103
+ yield json.dumps({
1104
+ "success": True,
1105
+ "results": results,
1106
+ "formatted": formatted,
1107
+ "message": f"Found {len(results)} results",
1108
+ })
1109
+ except Exception as exc:
1110
+ logger.exception("Web search failed")
1111
+ yield json.dumps({
1112
+ "success": False,
1113
+ "results": [],
1114
+ "message": f"Search failed: {str(exc)}",
1115
+ })
1116
+
1117
+
1118
  @app.api(name="chat", concurrency_limit=2)
1119
  def handle_chat(
1120
  prompt: str,
 
1122
  target_framework: str,
1123
  history_json: str,
1124
  exec_context_json: str,
1125
+ search_enabled: str = "false",
1126
  ) -> str:
1127
  """Stream chat responses with code execution. Yields JSON strings."""
1128
  history = json.loads(history_json) if history_json else []
 
1173
  "execution": execution_context,
1174
  })
1175
 
1176
+ # Web search if enabled
1177
+ search_context = ""
1178
+ if search_enabled.lower() == "true":
1179
+ yield json.dumps({
1180
+ "type": "status",
1181
+ "status_text": "Searching the web...",
1182
+ "status_state": "working",
1183
+ "history": history,
1184
+ "execution": execution_context,
1185
+ })
1186
+ search_results = web_search_google(prompt, num_results=6)
1187
+ if search_results:
1188
+ search_context = format_search_results(search_results)
1189
+ yield json.dumps({
1190
+ "type": "search_results",
1191
+ "status_text": f"Found {len(search_results)} results, generating code...",
1192
+ "status_state": "working",
1193
+ "history": history,
1194
+ "execution": execution_context,
1195
+ "search_results": search_results,
1196
+ })
1197
+
1198
  # Build messages for model
1199
  model_history = list(history[:-1])
1200
  model_history[-1] = {
1201
  "role": "user",
1202
+ "content": _targeted_prompt(prompt, target_language, target_framework, execution_context, search_context),
1203
  }
1204
  messages = _chat_history_to_messages(model_history)
1205
 
 
1251
  "execution": execution_context,
1252
  })
1253
 
1254
+ # Execute code
1255
  stdout, stderr, image_path, status_text, status_state = "", "", None, "Preview ready", "success"
1256
+ is_gradio = False
1257
+ gradio_url = None
1258
+
1259
  if target == "python" and code:
1260
+ if _is_gradio_code(code) or target_framework == "Gradio":
1261
+ is_gradio = True
1262
+ gradio_result = run_gradio_app(code)
1263
+ if gradio_result["success"]:
1264
+ gradio_url = gradio_result["url"]
1265
+ status_text = f"Gradio app running at {gradio_url}"
1266
+ status_state = "success"
1267
+ stderr = f"Gradio app launched successfully at {gradio_url}"
1268
+ else:
1269
+ status_text = "Gradio launch failed"
1270
+ status_state = "error"
1271
+ stderr = gradio_result.get("stderr", gradio_result.get("message", "Launch failed"))
1272
+ else:
1273
+ stdout, stderr, image_path, status_text, status_state = _run_extracted_code(code, target, target_framework)
1274
 
1275
  # Register image for serving
1276
  image_url = None
 
1284
  project_files = multi_files if multi_files else {}
1285
 
1286
  if project_files:
 
1287
  project_name = "generated-project"
1288
  zip_path = create_project_zip(project_files, project_name)
1289
  zip_filename = f"{project_name}.zip"
 
1312
  "image_path": image_path,
1313
  "status": status_text,
1314
  "language": fence_lang or target,
1315
+ "suggested_tab": "preview" if (image_path or is_web or is_gradio) else "console",
1316
  "download_url": download_url,
1317
  "project_files": project_files,
1318
  "is_web": is_web,
1319
  "web_code": web_code,
1320
+ "is_gradio": is_gradio,
1321
+ "gradio_url": gradio_url,
1322
  }
1323
 
1324
  yield json.dumps({
 
1344
  project_files = execution_context.get("project_files", {})
1345
 
1346
  if not project_files:
 
1347
  code = execution_context.get("code", "")
1348
  if not code:
1349
  yield json.dumps({
 
1354
  return
1355
 
1356
  lang = execution_context.get("language", "python")
1357
+ is_gradio = execution_context.get("is_gradio", False)
1358
  ext_map = {
1359
  "python": "app.py", "py": "app.py",
1360
  "javascript": "index.js", "js": "index.js",
 
1364
  filename = ext_map.get(lang, "app.py")
1365
  project_files = {filename: code}
1366
 
1367
+ # Auto-detect SDK for Gradio apps
1368
+ if is_gradio or _is_gradio_code(code):
1369
+ space_sdk = "gradio"
1370
+
1371
  project_name = repo_name.split("/")[-1] if "/" in repo_name else repo_name
1372
 
1373
  result = push_to_huggingface(
index.html CHANGED
@@ -423,6 +423,19 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
423
  #chat-input::placeholder { color: var(--gray-dim); text-shadow: none; }
424
  #chat-input:focus { border-color: var(--border-focus); }
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  #btn-send, #btn-stop {
427
  font-family: var(--font-mono); font-size: 12px; padding: 8px 14px;
428
  border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
@@ -583,6 +596,106 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
583
  height: 100%; color: var(--gray-dim); font-size: 12px;
584
  }
585
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
586
  /* ═══════════════════════════════════════════════════════
587
  DEPLOY TAB
588
  ═══════════════════════════════════════════════════════ */
@@ -796,7 +909,7 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
796
  <!-- Header -->
797
  <header id="header">
798
  <div class="header-title">
799
- <div class="header-ascii">╔═══ FULLSTACK CODE BUILDER ═══╗</div>
800
  <div class="header-subtitle">Local AI App Generator | MiniCPM5-1B</div>
801
  </div>
802
  <div class="header-actions">
@@ -809,7 +922,7 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
809
  </header>
810
 
811
  <div id="playground-banner">
812
- Powered by <a id="banner-model-link" href="https://huggingface.co/openbmb/MiniCPM5-1B" target="_blank" rel="noopener"><strong>MiniCPM5-1B</strong></a> running locally no external APIs. Generate fullstack apps in any language and deploy to HuggingFace.
813
  </div>
814
 
815
  <!-- Main Layout -->
@@ -832,6 +945,7 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
832
  <div id="input-row">
833
  <span class="input-prompt-symbol">&#10095;</span>
834
  <textarea id="chat-input" rows="1" placeholder="Describe the app you want to build..." spellcheck="false"></textarea>
 
835
  <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">&#10148;</button>
836
  <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
837
  </div>
@@ -845,6 +959,7 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
845
  <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
846
  <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
847
  <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
 
848
  <button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
849
  </div>
850
  <div id="output-content">
@@ -852,11 +967,11 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
852
  <div class="tab-pane active" id="pane-preview">
853
  <div class="preview-placeholder" id="preview-placeholder">
854
  <div class="ascii-art">
855
- ┌──────────────────────┐
856
- ╭━━━╮
857
- OUTPUT
858
- ╰━━━╯
859
- └──────────────────────┘</div>
860
  <div class="placeholder-text">Generate code to see output here</div>
861
  </div>
862
  <img id="preview-image" alt="Generated output">
@@ -890,6 +1005,17 @@ a:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
890
  </div>
891
  </div>
892
 
 
 
 
 
 
 
 
 
 
 
 
893
  <!-- Deploy Pane -->
894
  <div class="tab-pane" id="pane-deploy">
895
  <div class="deploy-section">
@@ -966,6 +1092,8 @@ const state = {
966
  reasoningExpanded: false,
967
  lastReasoningPressAt: 0,
968
  modelReady: false,
 
 
969
  };
970
 
971
  // ═══════════════════════════════════════════════════════
@@ -1001,6 +1129,11 @@ document.addEventListener('DOMContentLoaded', () => {
1001
  }
1002
  });
1003
 
 
 
 
 
 
1004
  document.addEventListener('pointerdown', handleReasoningPress, true);
1005
  document.addEventListener('mousedown', handleReasoningPress, true);
1006
  document.addEventListener('keydown', handleReasoningKeydown, true);
@@ -1035,7 +1168,6 @@ async function pollModelStatus() {
1035
  statusText.textContent = 'MODEL READY';
1036
  indicator.className = 'status-indicator status-success';
1037
  document.getElementById('btn-push-hf').disabled = false;
1038
- // Update after 3 seconds to idle
1039
  setTimeout(() => {
1040
  if (!state.isGenerating) {
1041
  indicator.className = 'status-indicator status-idle';
@@ -1053,7 +1185,6 @@ async function pollModelStatus() {
1053
  indicator.className = 'status-indicator status-error';
1054
  }
1055
 
1056
- // Poll again in 3 seconds
1057
  setTimeout(pollModelStatus, 3000);
1058
  } catch (err) {
1059
  console.error('Model status poll error:', err);
@@ -1406,7 +1537,7 @@ function renderExecution(execution) {
1406
  fsBtn.style.display = 'none';
1407
  img.src = execution.image_url;
1408
  img.style.display = 'block';
1409
- if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'deploy') {
1410
  switchTab('preview');
1411
  }
1412
  } else if (execution.is_web && execution.code) {
@@ -1417,11 +1548,27 @@ function renderExecution(execution) {
1417
  state.pendingWebPreviewCode = execution.code;
1418
  state.loadedWebPreviewCode = '';
1419
  state.scheduledWebPreviewCode = '';
1420
- if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'deploy') {
1421
  switchTab('preview', { forcePreviewReload: true });
1422
  } else {
1423
  iframe.srcdoc = '';
1424
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1425
  } else {
1426
  if (execution.stdout || execution.stderr) {
1427
  const suggested = execution.suggested_tab || 'console';
@@ -1617,7 +1764,7 @@ async function sendMessage(prompt) {
1617
  method: 'POST',
1618
  headers: { 'Content-Type': 'application/json' },
1619
  body: JSON.stringify({
1620
- data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON]
1621
  })
1622
  });
1623
 
@@ -1761,6 +1908,74 @@ function newChat() {
1761
  resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'Fullstack Code Builder'}.`);
1762
  }
1763
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1764
  // ═══════════════════════════════════════════════════════
1765
  // HUGGINGFACE PUSH
1766
  // ═══════════════════════════════════════════════════════
 
423
  #chat-input::placeholder { color: var(--gray-dim); text-shadow: none; }
424
  #chat-input:focus { border-color: var(--border-focus); }
425
 
426
+ #btn-web-search {
427
+ background: transparent; border: 1px solid var(--purple); color: var(--purple);
428
+ font-family: var(--font-mono); font-size: 11px; padding: 8px 10px;
429
+ border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
430
+ letter-spacing: 1px; flex-shrink: 0; height: 36px;
431
+ display: flex; align-items: center; gap: 4px;
432
+ }
433
+ #btn-web-search:hover {
434
+ background: var(--purple); color: white;
435
+ box-shadow: 0 0 12px rgba(168,85,247,0.3);
436
+ text-shadow: none;
437
+ }
438
+
439
  #btn-send, #btn-stop {
440
  font-family: var(--font-mono); font-size: 12px; padding: 8px 14px;
441
  border-radius: var(--radius); cursor: pointer; transition: all var(--transition);
 
596
  height: 100%; color: var(--gray-dim); font-size: 12px;
597
  }
598
 
599
+ /* ═══════════════════════════════════════════════════════
600
+ SEARCH TAB
601
+ ═══════════════════════════════════════════════════════ */
602
+ #pane-search { padding: 12px 16px; gap: 12px; overflow-y: auto; }
603
+
604
+ .search-bar {
605
+ display: flex;
606
+ gap: 8px;
607
+ margin-bottom: 10px;
608
+ }
609
+ #search-input {
610
+ flex: 1;
611
+ background: var(--bg-deep);
612
+ border: 1px solid var(--border);
613
+ color: var(--green);
614
+ font-family: var(--font-mono);
615
+ font-size: 12px;
616
+ padding: 6px 10px;
617
+ border-radius: var(--radius);
618
+ outline: none;
619
+ transition: border-color var(--transition);
620
+ }
621
+ #search-input:focus { border-color: var(--border-focus); }
622
+ #search-input::placeholder { color: var(--gray-dim); }
623
+
624
+ #btn-search-go {
625
+ background: transparent;
626
+ border: 1px solid var(--purple);
627
+ color: var(--purple);
628
+ font-family: var(--font-mono);
629
+ font-size: 11px;
630
+ padding: 6px 12px;
631
+ border-radius: var(--radius);
632
+ cursor: pointer;
633
+ transition: all var(--transition);
634
+ letter-spacing: 1px;
635
+ }
636
+ #btn-search-go:hover {
637
+ background: var(--purple);
638
+ color: white;
639
+ text-shadow: none;
640
+ }
641
+
642
+ .search-result-item {
643
+ padding: 8px 0;
644
+ border-bottom: 1px solid var(--border);
645
+ }
646
+ .search-result-item:last-child { border-bottom: none; }
647
+ .search-result-title {
648
+ color: var(--cyan);
649
+ font-size: 12px;
650
+ font-weight: 600;
651
+ text-decoration: none;
652
+ display: block;
653
+ margin-bottom: 2px;
654
+ }
655
+ .search-result-title:hover { text-decoration: underline; text-shadow: var(--glow-cyan); }
656
+ .search-result-url {
657
+ color: var(--green-dim);
658
+ font-size: 10px;
659
+ display: block;
660
+ margin-bottom: 2px;
661
+ word-break: break-all;
662
+ }
663
+ .search-result-snippet {
664
+ color: var(--gray-mid);
665
+ font-size: 11px;
666
+ line-height: 1.4;
667
+ }
668
+ .search-results-empty {
669
+ color: var(--gray-dim);
670
+ font-size: 12px;
671
+ text-align: center;
672
+ padding: 40px 20px;
673
+ }
674
+
675
+ /* Gradio preview */
676
+ .gradio-badge {
677
+ display: inline-flex;
678
+ align-items: center;
679
+ gap: 4px;
680
+ padding: 3px 8px;
681
+ background: rgba(168,85,247,0.15);
682
+ border: 1px solid var(--purple);
683
+ border-radius: 10px;
684
+ font-size: 10px;
685
+ color: var(--purple);
686
+ letter-spacing: 0.5px;
687
+ }
688
+
689
+ #gradio-iframe {
690
+ display: none;
691
+ position: absolute;
692
+ inset: 0;
693
+ width: 100%;
694
+ height: 100%;
695
+ min-height: 0;
696
+ border: none;
697
+ }
698
+
699
  /* ═══════════════════════════════════════════════════════
700
  DEPLOY TAB
701
  ═══════════════════════════════════════════════════════ */
 
909
  <!-- Header -->
910
  <header id="header">
911
  <div class="header-title">
912
+ <div class="header-ascii">&#9556;&#9552;&#9552;&#9552; FULLSTACK CODE BUILDER &#9552;&#9552;&#9552;&#9562;</div>
913
  <div class="header-subtitle">Local AI App Generator | MiniCPM5-1B</div>
914
  </div>
915
  <div class="header-actions">
 
922
  </header>
923
 
924
  <div id="playground-banner">
925
+ 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.
926
  </div>
927
 
928
  <!-- Main Layout -->
 
945
  <div id="input-row">
946
  <span class="input-prompt-symbol">&#10095;</span>
947
  <textarea id="chat-input" rows="1" placeholder="Describe the app you want to build..." spellcheck="false"></textarea>
948
+ <button id="btn-web-search" onclick="searchAndGenerate()" title="Search web + Generate">&#128269;</button>
949
  <button id="btn-send" onclick="handleSend()" title="Send message (Shift+Enter)">&#10148;</button>
950
  <button id="btn-stop" onclick="stopGeneration()" title="Stop generation">&#9632; STOP</button>
951
  </div>
 
959
  <button class="output-tab active" data-tab="preview" onclick="switchTab('preview')">Preview</button>
960
  <button class="output-tab" data-tab="console" onclick="switchTab('console')">Console</button>
961
  <button class="output-tab" data-tab="code" onclick="switchTab('code')">Code</button>
962
+ <button class="output-tab" data-tab="search" onclick="switchTab('search')">Search</button>
963
  <button class="output-tab" data-tab="deploy" onclick="switchTab('deploy')">Deploy</button>
964
  </div>
965
  <div id="output-content">
 
967
  <div class="tab-pane active" id="pane-preview">
968
  <div class="preview-placeholder" id="preview-placeholder">
969
  <div class="ascii-art">
970
+ &#9484;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9488;
971
+ &#9474; &#9585;&#9473;&#9473;&#9473;&#9586; &#9474;
972
+ &#9474; &#9474; &#9654; &#9474; OUTPUT &#9474;
973
+ &#9474; &#9589;&#9473;&#9473;&#9473;&#9588; &#9474;
974
+ &#9492;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9496;</div>
975
  <div class="placeholder-text">Generate code to see output here</div>
976
  </div>
977
  <img id="preview-image" alt="Generated output">
 
1005
  </div>
1006
  </div>
1007
 
1008
+ <!-- Search Pane -->
1009
+ <div class="tab-pane" id="pane-search">
1010
+ <div class="search-bar">
1011
+ <input type="text" id="search-input" placeholder="Search the web... (Google, no API needed)" spellcheck="false">
1012
+ <button id="btn-search-go" onclick="doWebSearch()">&#128269; Search</button>
1013
+ </div>
1014
+ <div id="search-results">
1015
+ <div class="search-results-empty">Search the web for documentation, examples, and references to use in your code.</div>
1016
+ </div>
1017
+ </div>
1018
+
1019
  <!-- Deploy Pane -->
1020
  <div class="tab-pane" id="pane-deploy">
1021
  <div class="deploy-section">
 
1092
  reasoningExpanded: false,
1093
  lastReasoningPressAt: 0,
1094
  modelReady: false,
1095
+ searchEnabled: false,
1096
+ lastSearchResults: [],
1097
  };
1098
 
1099
  // ═══════════════════════════════════════════════════════
 
1129
  }
1130
  });
1131
 
1132
+ // Search input Enter key
1133
+ document.getElementById('search-input')?.addEventListener('keydown', (e) => {
1134
+ if (e.key === 'Enter') { e.preventDefault(); doWebSearch(); }
1135
+ });
1136
+
1137
  document.addEventListener('pointerdown', handleReasoningPress, true);
1138
  document.addEventListener('mousedown', handleReasoningPress, true);
1139
  document.addEventListener('keydown', handleReasoningKeydown, true);
 
1168
  statusText.textContent = 'MODEL READY';
1169
  indicator.className = 'status-indicator status-success';
1170
  document.getElementById('btn-push-hf').disabled = false;
 
1171
  setTimeout(() => {
1172
  if (!state.isGenerating) {
1173
  indicator.className = 'status-indicator status-idle';
 
1185
  indicator.className = 'status-indicator status-error';
1186
  }
1187
 
 
1188
  setTimeout(pollModelStatus, 3000);
1189
  } catch (err) {
1190
  console.error('Model status poll error:', err);
 
1537
  fsBtn.style.display = 'none';
1538
  img.src = execution.image_url;
1539
  img.style.display = 'block';
1540
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
1541
  switchTab('preview');
1542
  }
1543
  } else if (execution.is_web && execution.code) {
 
1548
  state.pendingWebPreviewCode = execution.code;
1549
  state.loadedWebPreviewCode = '';
1550
  state.scheduledWebPreviewCode = '';
1551
+ if (state.activeTab !== 'console' && state.activeTab !== 'code' && state.activeTab !== 'search' && state.activeTab !== 'deploy') {
1552
  switchTab('preview', { forcePreviewReload: true });
1553
  } else {
1554
  iframe.srcdoc = '';
1555
  }
1556
+ } else if (execution.is_gradio && execution.gradio_url) {
1557
+ // Gradio app handling
1558
+ placeholder.style.display = 'none';
1559
+ img.style.display = 'none';
1560
+ iframe.style.display = 'block';
1561
+ fsBtn.style.display = 'block';
1562
+ // Show Gradio badge
1563
+ const badge = document.createElement('span');
1564
+ badge.className = 'gradio-badge';
1565
+ badge.innerHTML = '\u26a1 Gradio';
1566
+ const codeTabHeader = document.querySelector('.code-tab-header');
1567
+ if (codeTabHeader) codeTabHeader.prepend(badge);
1568
+ 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>`;
1569
+ state.loadedWebPreviewCode = '';
1570
+ state.scheduledWebPreviewCode = '';
1571
+ switchTab('preview', { forcePreviewReload: true });
1572
  } else {
1573
  if (execution.stdout || execution.stderr) {
1574
  const suggested = execution.suggested_tab || 'console';
 
1764
  method: 'POST',
1765
  headers: { 'Content-Type': 'application/json' },
1766
  body: JSON.stringify({
1767
+ data: [prompt, state.targetLanguage, framework, historyJSON, execContextJSON, state.searchEnabled ? 'true' : 'false']
1768
  })
1769
  });
1770
 
 
1908
  resetConversation(`Session reset. Welcome back to ${CONFIG.app_title || 'Fullstack Code Builder'}.`);
1909
  }
1910
 
1911
+ // ═══════════════════════════════════════════════════════
1912
+ // WEB SEARCH
1913
+ // ═══════════════════════════════════════════════════════
1914
+ async function doWebSearch() {
1915
+ const query = document.getElementById('search-input').value.trim();
1916
+ if (!query) return;
1917
+
1918
+ const resultsContainer = document.getElementById('search-results');
1919
+ resultsContainer.innerHTML = '<div class="search-results-empty" style="color:var(--amber);">Searching...</div>';
1920
+
1921
+ try {
1922
+ const resp = await fetch('/gradio_api/call/web_search', {
1923
+ method: 'POST',
1924
+ headers: { 'Content-Type': 'application/json' },
1925
+ body: JSON.stringify({ data: [query] })
1926
+ });
1927
+ const { event_id } = await resp.json();
1928
+ const eventSource = new EventSource(`/gradio_api/call/web_search/${event_id}`);
1929
+
1930
+ eventSource.addEventListener('complete', (e) => {
1931
+ const dataArray = JSON.parse(e.data);
1932
+ const result = JSON.parse(dataArray[0]);
1933
+ if (result.success) {
1934
+ state.lastSearchResults = result.results;
1935
+ renderSearchResults(result.results);
1936
+ } else {
1937
+ resultsContainer.innerHTML = `<div class="search-results-empty">${escapeHtml(result.message)}</div>`;
1938
+ }
1939
+ eventSource.close();
1940
+ });
1941
+ eventSource.addEventListener('error', (e) => {
1942
+ resultsContainer.innerHTML = '<div class="search-results-empty">Search failed</div>';
1943
+ eventSource.close();
1944
+ });
1945
+ } catch (err) {
1946
+ resultsContainer.innerHTML = `<div class="search-results-empty">Error: ${err.message}</div>`;
1947
+ }
1948
+ }
1949
+
1950
+ function searchAndGenerate() {
1951
+ const input = document.getElementById('chat-input');
1952
+ const prompt = input.value.trim();
1953
+ if (!prompt || state.isGenerating) return;
1954
+ input.value = '';
1955
+ autoResize();
1956
+ state.searchEnabled = true;
1957
+ sendMessage(prompt);
1958
+ // Reset after sending
1959
+ state.searchEnabled = false;
1960
+ }
1961
+
1962
+ function renderSearchResults(results) {
1963
+ const container = document.getElementById('search-results');
1964
+ if (!results || results.length === 0) {
1965
+ container.innerHTML = '<div class="search-results-empty">No results found.</div>';
1966
+ return;
1967
+ }
1968
+ let html = '';
1969
+ results.forEach((r) => {
1970
+ html += `<div class="search-result-item">
1971
+ <a class="search-result-title" href="${escapeHtml(r.url)}" target="_blank" rel="noopener">${escapeHtml(r.title)}</a>
1972
+ <span class="search-result-url">${escapeHtml(r.url)}</span>
1973
+ <div class="search-result-snippet">${escapeHtml(r.snippet)}</div>
1974
+ </div>`;
1975
+ });
1976
+ container.innerHTML = html;
1977
+ }
1978
+
1979
  // ═══════════════════════════════════════════════════════
1980
  // HUGGINGFACE PUSH
1981
  // ═══════════════════════════════════════════════════════
requirements.txt CHANGED
@@ -4,3 +4,6 @@ torch>=2.1.0
4
  accelerate>=0.25.0
5
  huggingface_hub>=0.20.0
6
  matplotlib>=3.8
 
 
 
 
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