natabrizy commited on
Commit
7ae04c9
·
verified ·
1 Parent(s): 46433dd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +398 -89
app.py CHANGED
@@ -2,8 +2,9 @@ import base64
2
  import io
3
  import json
4
  import os
 
5
  import tempfile
6
- from typing import Tuple, Optional
7
 
8
  import gradio as gr
9
  import httpx
@@ -14,14 +15,28 @@ from lzstring import LZString
14
  # Configuration
15
  # =========================
16
  NEBIUS_BASE_URL = "https://api.studio.nebius.com/v1/"
 
 
17
  DEFAULT_VISION_MODEL = "Qwen/Qwen2.5-VL-72B-Instruct"
18
- DEFAULT_CODE_MODEL = "deepseek-ai/DeepSeek-V3-0324"
 
 
 
 
19
 
20
- # More forgiving timeouts and built-in retries to reduce "read operation timed out"
 
 
 
 
 
 
 
 
21
  HTTP_TIMEOUTS = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=60.0)
22
- HTTP_RETRIES = 2 # extra attempts on transient failures
23
 
24
- # Use the same default key you provided
25
  DEFAULT_NEBIUS_API_KEY = (
26
  "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwNTA1MTQzMDg2MDMwMzIxNDEwMiIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkwNjU5ODA0NCwidXVpZCI6ImNkOGFiMWZlLTIxN2QtNDJlMy04OWUwLWM1YTg4MjcwMGVhNyIsIm5hbWUiOiJodW5nZ2luZyIsImV4cGlyZXNfYXQiOiIyMDMwLTA2LTAyVDAyOjM0OjA0KzAwMDAifQ.MA52QuIiNruK7_lX688RXAEI2TkcCOjcf_02XrpnhI8"
27
  )
@@ -40,6 +55,90 @@ def get_api_key(user_key: str = "") -> str:
40
  return (user_key or "").strip() or os.getenv("NEBIUS_API_KEY", "").strip() or DEFAULT_NEBIUS_API_KEY
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def call_chat_completions(
44
  model: str,
45
  messages: list,
@@ -48,37 +147,137 @@ def call_chat_completions(
48
  temperature: float = 0.7,
49
  ) -> str:
50
  """
51
- Calls the Nebius OpenAI-compatible chat completions endpoint via HTTP.
52
- Returns the assistant message content string.
53
- Includes retries and increased read timeout to mitigate timeouts.
54
  """
55
  if not api_key:
56
  raise ValueError("Nebius API key is required.")
57
 
58
- url = f"{NEBIUS_BASE_URL}chat/completions"
59
- payload = {
 
 
 
 
60
  "model": model,
61
  "messages": messages,
62
  "max_tokens": max_tokens,
63
  "temperature": temperature,
64
  }
65
- headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
66
 
67
- # HTTPX transport with retries
68
- transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
  with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
71
- resp = client.post(url, headers=headers, json=payload)
72
  resp.raise_for_status()
73
  data = resp.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
- choices = data.get("choices", [])
76
- if not choices:
77
- raise RuntimeError("No choices returned from the API.")
78
- content = choices[0].get("message", {}).get("content", "")
79
- if not content:
80
- raise RuntimeError("Empty content returned from the API.")
81
- return content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
 
84
  # =========================
@@ -134,18 +333,6 @@ def analyze_image(
134
  return f"Error analyzing image: {str(e)}"
135
 
136
 
137
- def _strip_fenced_code(text: str) -> str:
138
- """
139
- Removes ```html ... ``` fences from a content block if present.
140
- """
141
- s = text.strip()
142
- if s.startswith("```html"):
143
- s = s.split("```html", 1)[1].strip()
144
- if s.endswith("```"):
145
- s = s.rsplit("```", 1)[0].strip()
146
- return s
147
-
148
-
149
  def generate_html_code(
150
  description: str,
151
  nebius_api_key: str = "",
@@ -155,7 +342,6 @@ def generate_html_code(
155
  ) -> str:
156
  """
157
  Generate HTML/CSS/JavaScript code based on a website description.
158
- Adds timeout-aware retry with a reduced token budget on second attempt.
159
  """
160
  if not description or description.startswith("Error"):
161
  return "Error: Invalid or missing description."
@@ -217,47 +403,67 @@ Return only the complete HTML code starting with <!DOCTYPE html> and ending with
217
  return html_code
218
  except Exception as e2:
219
  return f"Error generating HTML code after retry: {str(e2)}. Tips: lower Max tokens, pick a smaller/faster model, or try again."
 
 
 
 
 
 
 
 
 
220
  except Exception as e:
221
  return f"Error generating HTML code: {str(e)}"
222
 
223
 
224
  def create_codesandbox(html_code: str) -> str:
225
  """
226
- Create a CodeSandbox project from HTML code.
227
- Returns the sandbox URL, or a prefilled URL if API POST fails.
228
  """
229
  if not html_code or html_code.startswith("Error"):
230
  return "Error: No valid HTML code provided."
231
 
232
  try:
 
 
233
  files = {
234
- "index.html": {"content": html_code, "isBinary": False},
235
- "package.json": {
236
- "content": json.dumps(
237
- {
238
- "name": "ai-generated-website",
239
- "version": "1.0.0",
240
- "description": "Website generated from image analysis",
241
- "main": "index.html",
242
- "scripts": {"start": "serve .", "build": "echo 'No build required'"},
243
- "devDependencies": {"serve": "^14.0.0"},
244
- },
245
- indent=2,
246
- ),
247
- "isBinary": False,
248
- },
 
 
 
 
 
 
249
  }
250
 
251
  parameters = {"files": files, "template": "static"}
252
 
253
- # Build fallback GET URL with compressed parameters
254
  json_str = json.dumps(parameters, separators=(",", ":"))
255
  lz = LZString()
256
  compressed = lz.compressToBase64(json_str)
257
  compressed = compressed.replace("+", "-").replace("/", "_").rstrip("=")
258
- prefill_url = f"https://codesandbox.io/api/v1/sandboxes/define?parameters={compressed}"
 
 
 
259
 
260
- # Try POST API to get a sandbox_id
261
  url = "https://codesandbox.io/api/v1/sandboxes/define"
262
  transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
263
  with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
@@ -266,10 +472,24 @@ def create_codesandbox(html_code: str) -> str:
266
  data = resp.json()
267
  sandbox_id = data.get("sandbox_id")
268
  if sandbox_id:
269
- return f"https://codesandbox.io/s/{sandbox_id}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
271
- # Fallback to prefill URL
272
- return prefill_url
273
  except Exception as e:
274
  return f"Error creating CodeSandbox: {str(e)}"
275
 
@@ -315,27 +535,115 @@ def export_html_to_file(html_code: str) -> Optional[str]:
315
 
316
 
317
  # =========================
318
- # Gradio UI (English-only, user-friendly)
319
  # =========================
 
 
 
 
 
 
 
 
 
320
  with gr.Blocks(
321
  theme=gr.themes.Soft(),
322
  title="AI Website Generator (Nebius)",
323
- css="""
324
- .section { border: 1px solid #e5e7eb; padding: 16px; border-radius: 10px; margin: 10px 0; }
325
- .muted { color: #6b7280; font-size: 0.9em; }
326
- .footer { text-align:center; color:#9ca3af; padding: 8px 0; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
327
  """,
328
  ) as app:
329
  gr.Markdown(
330
  """
331
- # 🚀 AI Website Generator (Nebius)
332
- Turn website screenshots into functional HTML using Nebius models.
333
-
334
- - 📸 Image analysis (default: Qwen2.5-VL-72B-Instruct)
335
- - 💻 Code generation (default: DeepSeek-V3-0324)
336
- - 🌐 One-click CodeSandbox deployment
337
- - 🔒 Your key is used at runtime only
338
- """
 
339
  )
340
 
341
  with gr.Accordion("API & Models", open=True):
@@ -349,15 +657,17 @@ Turn website screenshots into functional HTML using Nebius models.
349
  with gr.Row():
350
  vision_model_dd = gr.Dropdown(
351
  label="Vision Model",
352
- choices=[DEFAULT_VISION_MODEL],
353
  value=DEFAULT_VISION_MODEL,
354
  allow_custom_value=True,
 
355
  )
356
  code_model_dd = gr.Dropdown(
357
  label="Code Generation Model",
358
- choices=[DEFAULT_CODE_MODEL],
359
  value=DEFAULT_CODE_MODEL,
360
  allow_custom_value=True,
 
361
  )
362
  with gr.Row():
363
  code_max_tokens = gr.Slider(
@@ -377,7 +687,7 @@ Turn website screenshots into functional HTML using Nebius models.
377
  info="Higher is more creative; lower is more deterministic.",
378
  )
379
 
380
- with gr.Tab("🎯 Quick Generate"):
381
  with gr.Row():
382
  with gr.Column(scale=1):
383
  gr.Markdown("### Step 1: Upload Screenshot", elem_classes=["section"])
@@ -387,7 +697,7 @@ Turn website screenshots into functional HTML using Nebius models.
387
  sources=["upload", "clipboard"],
388
  height=280,
389
  )
390
- generate_btn = gr.Button("🎨 Generate Website", variant="primary")
391
 
392
  with gr.Column(scale=2):
393
  gr.Markdown("### Step 2: Review Results", elem_classes=["section"])
@@ -403,28 +713,29 @@ Turn website screenshots into functional HTML using Nebius models.
403
  )
404
 
405
  with gr.Row():
406
- codesandbox_btn = gr.Button("🚀 Deploy to CodeSandbox")
407
- download_btn = gr.Button("💾 Download index.html")
 
408
 
409
- codesandbox_link = gr.Markdown(value="")
410
  download_file = gr.File(
411
  label="Download (index.html)",
412
  interactive=False,
413
  visible=False,
414
  )
415
 
416
- with gr.Tab("🔧 Individual Tools"):
417
  with gr.Row():
418
  with gr.Column():
419
  gr.Markdown("### Image Analysis Tool", elem_classes=["section"])
420
  img_tool = gr.Image(type="pil", label="Image")
421
- analyze_btn = gr.Button("Analyze Image")
422
  analysis_result = gr.Textbox(label="Analysis Result", lines=6)
423
 
424
  with gr.Column():
425
  gr.Markdown("### Code Generation Tool", elem_classes=["section"])
426
  desc_input = gr.Textbox(label="Description", lines=4, placeholder="Describe the page you want...")
427
- code_btn = gr.Button("Generate Code")
428
  code_result = gr.Code(label="Generated Code", language="html")
429
 
430
  gr.Markdown("Made with Gradio • Nebius API compatible", elem_classes=["footer"])
@@ -437,15 +748,17 @@ Turn website screenshots into functional HTML using Nebius models.
437
  )
438
 
439
  def _deploy_to_codesandbox(html_code: str) -> str:
440
- url = create_codesandbox(html_code)
441
- if url.startswith("Error"):
442
- return f"**Error:** {url}"
443
- return f"✅ Deployed. Open your sandbox: [{url}]({url})"
 
 
444
 
445
  codesandbox_btn.click(
446
  fn=_deploy_to_codesandbox,
447
  inputs=[html_output],
448
- outputs=[codesandbox_link],
449
  )
450
 
451
  def _download_html(html_code: str):
@@ -472,9 +785,5 @@ Turn website screenshots into functional HTML using Nebius models.
472
  outputs=[code_result],
473
  )
474
 
475
- # Optional examples (uncomment and provide your own image file)
476
- # gr.Examples(examples=[["1.jpg"]], inputs=[image_input], label="Example Screenshots")
477
-
478
  if __name__ == "__main__":
479
- # Set share=True if you want a public Gradio URL
480
  app.launch(share=False)
 
2
  import io
3
  import json
4
  import os
5
+ import re
6
  import tempfile
7
+ from typing import Tuple, Optional, List, Dict, Any
8
 
9
  import gradio as gr
10
  import httpx
 
15
  # Configuration
16
  # =========================
17
  NEBIUS_BASE_URL = "https://api.studio.nebius.com/v1/"
18
+
19
+ # Add more selectable models (you can also type your own in the dropdowns)
20
  DEFAULT_VISION_MODEL = "Qwen/Qwen2.5-VL-72B-Instruct"
21
+ VISION_MODELS = [
22
+ DEFAULT_VISION_MODEL,
23
+ "Qwen/Qwen2.5-VL-7B-Instruct",
24
+ "Qwen/Qwen2-VL-72B-Instruct",
25
+ ]
26
 
27
+ DEFAULT_CODE_MODEL = "deepseek-ai/DeepSeek-V3-0324"
28
+ CODE_MODELS = [
29
+ DEFAULT_CODE_MODEL,
30
+ "Qwen/Qwen2.5-Coder-32B-Instruct",
31
+ "Meta-Llama-3.1-70B-Instruct",
32
+ "Mistral-7B-Instruct",
33
+ ]
34
+
35
+ # Timeouts and simple retries for stability
36
  HTTP_TIMEOUTS = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=60.0)
37
+ HTTP_RETRIES = 2
38
 
39
+ # Keep the same default key you provided
40
  DEFAULT_NEBIUS_API_KEY = (
41
  "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwNTA1MTQzMDg2MDMwMzIxNDEwMiIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkwNjU5ODA0NCwidXVpZCI6ImNkOGFiMWZlLTIxN2QtNDJlMy04OWUwLWM1YTg4MjcwMGVhNyIsIm5hbWUiOiJodW5nZ2luZyIsImV4cGlyZXNfYXQiOiIyMDMwLTA2LTAyVDAyOjM0OjA0KzAwMDAifQ.MA52QuIiNruK7_lX688RXAEI2TkcCOjcf_02XrpnhI8"
42
  )
 
55
  return (user_key or "").strip() or os.getenv("NEBIUS_API_KEY", "").strip() or DEFAULT_NEBIUS_API_KEY
56
 
57
 
58
+ def _convert_messages_to_responses_input(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
59
+ """
60
+ Convert OpenAI Chat-style messages to the Responses API input format.
61
+ Supports multimodal parts: text and image_url (data URL or remote).
62
+ """
63
+ converted = []
64
+ for m in messages:
65
+ role = m.get("role", "user")
66
+ content = m.get("content", "")
67
+
68
+ parts: List[Dict[str, Any]] = []
69
+ if isinstance(content, list):
70
+ for part in content:
71
+ ptype = part.get("type")
72
+ if ptype == "text":
73
+ parts.append({"type": "input_text", "text": part.get("text", "")})
74
+ elif ptype == "image_url":
75
+ image_obj = part.get("image_url", {})
76
+ url = image_obj if isinstance(image_obj, str) else image_obj.get("url", "")
77
+ parts.append({"type": "input_image", "image_url": url})
78
+ elif isinstance(content, str):
79
+ parts.append({"type": "input_text", "text": content})
80
+ else:
81
+ # Fallback: stringify unknown structures
82
+ parts.append({"type": "input_text", "text": json.dumps(content)})
83
+
84
+ converted.append({"role": role, "content": parts})
85
+ return converted
86
+
87
+
88
+ def _parse_possible_response_payload(data: Dict[str, Any]) -> str:
89
+ """
90
+ Try to extract text from either chat/completions or responses-style payloads.
91
+ """
92
+ # chat/completions format
93
+ choices = data.get("choices")
94
+ if isinstance(choices, list) and choices:
95
+ msg = choices[0].get("message", {})
96
+ if isinstance(msg, dict):
97
+ content = msg.get("content")
98
+ if isinstance(content, str) and content.strip():
99
+ return content
100
+
101
+ # responses format - common variants
102
+ # 1) top-level output_text
103
+ if "output_text" in data and isinstance(data["output_text"], str) and data["output_text"].strip():
104
+ return data["output_text"]
105
+
106
+ # 2) nested under response
107
+ resp = data.get("response", {})
108
+ if isinstance(resp, dict):
109
+ if "output_text" in resp and isinstance(resp["output_text"], str) and resp["output_text"].strip():
110
+ return resp["output_text"]
111
+ # walk response.output[0].content[0].text
112
+ out = resp.get("output")
113
+ if isinstance(out, list) and out:
114
+ cont = out[0].get("content")
115
+ if isinstance(cont, list) and cont:
116
+ if isinstance(cont[0], dict):
117
+ text = cont[0].get("text")
118
+ if isinstance(text, str) and text.strip():
119
+ return text
120
+
121
+ # 3) sometimes top-level output array
122
+ out_top = data.get("output")
123
+ if isinstance(out_top, list) and out_top:
124
+ cont = out_top[0].get("content")
125
+ if isinstance(cont, list) and cont:
126
+ text = cont[0].get("text")
127
+ if isinstance(text, str) and text.strip():
128
+ return text
129
+
130
+ # 4) fallback: first string found in 'content'
131
+ content = data.get("content")
132
+ if isinstance(content, list) and content:
133
+ maybe = content[0]
134
+ if isinstance(maybe, dict):
135
+ text = maybe.get("text")
136
+ if isinstance(text, str) and text.strip():
137
+ return text
138
+
139
+ raise RuntimeError("Unable to parse text from model response.")
140
+
141
+
142
  def call_chat_completions(
143
  model: str,
144
  messages: list,
 
147
  temperature: float = 0.7,
148
  ) -> str:
149
  """
150
+ Tries OpenAI-compatible chat/completions first.
151
+ If it receives 404/405 (route not found or method mismatch), it falls back to the Responses API.
152
+ Returns the assistant text content.
153
  """
154
  if not api_key:
155
  raise ValueError("Nebius API key is required.")
156
 
157
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
158
+ transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
159
+
160
+ # Attempt 1: chat/completions
161
+ chat_url = f"{NEBIUS_BASE_URL}chat/completions"
162
+ chat_payload = {
163
  "model": model,
164
  "messages": messages,
165
  "max_tokens": max_tokens,
166
  "temperature": temperature,
167
  }
 
168
 
169
+ try:
170
+ with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
171
+ resp = client.post(chat_url, headers=headers, json=chat_payload)
172
+ resp.raise_for_status()
173
+ data = resp.json()
174
+ return _parse_possible_response_payload(data)
175
+ except httpx.HTTPStatusError as e:
176
+ status = e.response.status_code
177
+ # Only fall back to Responses when the route is not found or method not allowed
178
+ if status not in (404, 405):
179
+ # Provide clearer error for common 404 model issues too
180
+ if status == 404:
181
+ detail = e.response.text
182
+ raise RuntimeError(
183
+ f"404 Not Found from chat/completions. The endpoint or model may be unavailable. Details: {detail}"
184
+ ) from e
185
+ raise
186
+ except Exception:
187
+ # Fall through to responses attempt
188
+ pass
189
+
190
+ # Attempt 2: responses API
191
+ responses_url = f"{NEBIUS_BASE_URL}responses"
192
+ responses_input = _convert_messages_to_responses_input(messages)
193
+ responses_payload = {
194
+ "model": model,
195
+ "input": responses_input,
196
+ # Some providers expect max_output_tokens; include both for compatibility
197
+ "max_tokens": max_tokens,
198
+ "max_output_tokens": max_tokens,
199
+ "temperature": temperature,
200
+ }
201
 
202
  with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
203
+ resp = client.post(responses_url, headers=headers, json=responses_payload)
204
  resp.raise_for_status()
205
  data = resp.json()
206
+ return _parse_possible_response_payload(data)
207
+
208
+
209
+ def _strip_fenced_code(text: str) -> str:
210
+ """
211
+ Removes ```html ... ``` fences from a content block if present.
212
+ """
213
+ s = text.strip()
214
+ if s.startswith("```html"):
215
+ s = s.split("```html", 1)[1].strip()
216
+ if s.endswith("```"):
217
+ s = s.rsplit("```", 1)[0].strip()
218
+ return s
219
 
220
+
221
+ def _split_assets(html_code: str) -> Tuple[str, str, str]:
222
+ """
223
+ Split inline <style> and <script> (without src) from the HTML into separate CSS and JS strings.
224
+ Return tuple: (updated_html, css_text, js_text)
225
+ """
226
+ if not html_code:
227
+ return html_code, "", ""
228
+
229
+ html = html_code
230
+
231
+ # Collect and remove style blocks
232
+ css_blocks = re.findall(r"<style[^>]*>(.*?)</style>", html, flags=re.IGNORECASE | re.DOTALL)
233
+ css_text = "\n\n".join(block.strip() for block in css_blocks if block.strip())
234
+ html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.IGNORECASE | re.DOTALL)
235
+
236
+ # Collect and remove inline scripts (no src)
237
+ js_blocks = []
238
+
239
+ def _script_repl(m):
240
+ attrs = m.group("attrs") or ""
241
+ code = m.group("code") or ""
242
+ if "src=" in attrs.lower():
243
+ return m.group(0) # keep external scripts
244
+ if code.strip():
245
+ js_blocks.append(code.strip())
246
+ return "" # remove inline script
247
+
248
+ html = re.sub(
249
+ r"<script(?P<attrs>[^>]*)>(?P<code>.*?)</script>",
250
+ _script_repl,
251
+ html,
252
+ flags=re.IGNORECASE | re.DOTALL,
253
+ )
254
+ js_text = "\n\n".join(js_blocks)
255
+
256
+ # If CSS collected, ensure link tag is added
257
+ if css_text:
258
+ if re.search(r"</head>", html, flags=re.IGNORECASE):
259
+ html = re.sub(
260
+ r"</head>",
261
+ ' <link rel="stylesheet" href="style.css">\n</head>',
262
+ html,
263
+ flags=re.IGNORECASE,
264
+ )
265
+ else:
266
+ html = f"<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\"/>\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n{html}"
267
+
268
+ # If JS collected, ensure script tag before </body> or at end
269
+ if js_text:
270
+ if re.search(r"</body>", html, flags=re.IGNORECASE):
271
+ html = re.sub(
272
+ r"</body>",
273
+ ' <script src="script.js"></script>\n</body>',
274
+ html,
275
+ flags=re.IGNORECASE,
276
+ )
277
+ else:
278
+ html = html.rstrip() + '\n <script src="script.js"></script>\n'
279
+
280
+ return html, css_text, js_text
281
 
282
 
283
  # =========================
 
333
  return f"Error analyzing image: {str(e)}"
334
 
335
 
 
 
 
 
 
 
 
 
 
 
 
 
336
  def generate_html_code(
337
  description: str,
338
  nebius_api_key: str = "",
 
342
  ) -> str:
343
  """
344
  Generate HTML/CSS/JavaScript code based on a website description.
 
345
  """
346
  if not description or description.startswith("Error"):
347
  return "Error: Invalid or missing description."
 
403
  return html_code
404
  except Exception as e2:
405
  return f"Error generating HTML code after retry: {str(e2)}. Tips: lower Max tokens, pick a smaller/faster model, or try again."
406
+ except httpx.HTTPStatusError as e:
407
+ status = e.response.status_code
408
+ detail = e.response.text
409
+ if status == 404:
410
+ return (
411
+ f"Error generating HTML code: 404 Not Found. This may indicate the endpoint or selected model is not available. "
412
+ f"Try a different model from the dropdown or ensure your Nebius endpoint supports chat/completions or responses. Details: {detail}"
413
+ )
414
+ return f"Error generating HTML code: HTTP {status}. Details: {detail}"
415
  except Exception as e:
416
  return f"Error generating HTML code: {str(e)}"
417
 
418
 
419
  def create_codesandbox(html_code: str) -> str:
420
  """
421
+ Create a CodeSandbox project from HTML code, returning only editor links
422
+ for index.html (and style.css/script.js if present). Does not provide any live preview link.
423
  """
424
  if not html_code or html_code.startswith("Error"):
425
  return "Error: No valid HTML code provided."
426
 
427
  try:
428
+ updated_html, css_text, js_text = _split_assets(html_code)
429
+
430
  files = {
431
+ "index.html": {"content": updated_html, "isBinary": False},
432
+ }
433
+ if css_text:
434
+ files["style.css"] = {"content": css_text, "isBinary": False}
435
+ if js_text:
436
+ files["script.js"] = {"content": js_text, "isBinary": False}
437
+
438
+ # package.json is optional for static template
439
+ files["package.json"] = {
440
+ "content": json.dumps(
441
+ {
442
+ "name": "ai-generated-website",
443
+ "version": "1.0.0",
444
+ "description": "Website generated from image analysis",
445
+ "main": "index.html",
446
+ "scripts": {"start": "serve .", "build": "echo 'No build required'"},
447
+ "devDependencies": {"serve": "^14.0.0"},
448
+ },
449
+ indent=2,
450
+ ),
451
+ "isBinary": False,
452
  }
453
 
454
  parameters = {"files": files, "template": "static"}
455
 
456
+ # Prefill GET URL with compressed parameters for editor file view
457
  json_str = json.dumps(parameters, separators=(",", ":"))
458
  lz = LZString()
459
  compressed = lz.compressToBase64(json_str)
460
  compressed = compressed.replace("+", "-").replace("/", "_").rstrip("=")
461
+ prefill_base = "https://codesandbox.io/api/v1/sandboxes/define"
462
+ prefill_index = f"{prefill_base}?parameters={compressed}&file=/index.html"
463
+ prefill_css = f"{prefill_base}?parameters={compressed}&file=/style.css" if "style.css" in files else ""
464
+ prefill_js = f"{prefill_base}?parameters={compressed}&file=/script.js" if "script.js" in files else ""
465
 
466
+ # Try POST API to get a sandbox_id and return only editor links (no preview)
467
  url = "https://codesandbox.io/api/v1/sandboxes/define"
468
  transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
469
  with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
 
472
  data = resp.json()
473
  sandbox_id = data.get("sandbox_id")
474
  if sandbox_id:
475
+ editor_base = f"https://codesandbox.io/p/sandbox/{sandbox_id}"
476
+ lines = [
477
+ f"- Open index.html in editor: {editor_base}?file=/index.html",
478
+ ]
479
+ if "style.css" in files:
480
+ lines.append(f"- Open style.css in editor: {editor_base}?file=/style.css")
481
+ if "script.js" in files:
482
+ lines.append(f"- Open script.js in editor: {editor_base}?file=/script.js")
483
+ return "\n".join(lines)
484
+
485
+ # Fallback to prefill URLs if POST fails
486
+ lines = [f"- Open index.html in editor: {prefill_index}"]
487
+ if prefill_css:
488
+ lines.append(f"- Open style.css in editor: {prefill_css}")
489
+ if prefill_js:
490
+ lines.append(f"- Open script.js in editor: {prefill_js}")
491
+ return "\n".join(lines)
492
 
 
 
493
  except Exception as e:
494
  return f"Error creating CodeSandbox: {str(e)}"
495
 
 
535
 
536
 
537
  # =========================
538
+ # Gradio UI (Brizy-inspired palette, no emojis)
539
  # =========================
540
+ BRIZY_PRIMARY = "#6C5CE7" # Indigo-like
541
+ BRIZY_SECONDARY = "#00C2FF" # Cyan accent
542
+ BRIZY_BG = "#F7F9FC" # Light background
543
+ BRIZY_SURFACE = "#FFFFFF" # Surface
544
+ BRIZY_TEXT = "#1F2937" # Dark text
545
+ BRIZY_MUTED = "#6B7280" # Muted text
546
+ BRIZY_BORDER = "#E5E7EB" # Soft border
547
+ BRIZY_GRADIENT = f"linear-gradient(135deg, {BRIZY_PRIMARY} 0%, {BRIZY_SECONDARY} 100%)"
548
+
549
  with gr.Blocks(
550
  theme=gr.themes.Soft(),
551
  title="AI Website Generator (Nebius)",
552
+ css=f"""
553
+ :root {{
554
+ --app-primary: {BRIZY_PRIMARY};
555
+ --app-secondary: {BRIZY_SECONDARY};
556
+ --app-bg: {BRIZY_BG};
557
+ --app-surface: {BRIZY_SURFACE};
558
+ --app-text: {BRIZY_TEXT};
559
+ --app-muted: {BRIZY_MUTED};
560
+ --app-border: {BRIZY_BORDER};
561
+ }}
562
+
563
+ body {{
564
+ background: var(--app-bg);
565
+ color: var(--app-text);
566
+ }}
567
+
568
+ .section {{
569
+ border: 1px solid var(--app-border);
570
+ padding: 16px;
571
+ border-radius: 12px;
572
+ background: var(--app-surface);
573
+ box-shadow: 0 1px 2px rgba(0,0,0,0.03);
574
+ margin: 10px 0;
575
+ }}
576
+
577
+ .muted {{
578
+ color: var(--app-muted);
579
+ font-size: 0.92em;
580
+ }}
581
+
582
+ .footer {{
583
+ text-align: center;
584
+ color: var(--app-muted);
585
+ padding: 8px 0;
586
+ }}
587
+
588
+ .title h1 {{
589
+ background: {BRIZY_GRADIENT};
590
+ -webkit-background-clip: text;
591
+ background-clip: text;
592
+ color: transparent;
593
+ font-weight: 800;
594
+ letter-spacing: -0.02em;
595
+ }}
596
+
597
+ .primary-btn button {{
598
+ background: {BRIZY_GRADIENT} !important;
599
+ color: #fff !important;
600
+ border: none !important;
601
+ }}
602
+ .primary-btn button:hover {{
603
+ filter: brightness(0.98);
604
+ }}
605
+
606
+ .secondary-btn button {{
607
+ background: var(--app-surface) !important;
608
+ color: var(--app-text) !important;
609
+ border: 1px solid var(--app-border) !important;
610
+ }}
611
+ .secondary-btn button:hover {{
612
+ border-color: {BRIZY_PRIMARY} !important;
613
+ color: {BRIZY_PRIMARY} !important;
614
+ }}
615
+
616
+ /* Inputs focus */
617
+ input:focus, textarea:focus, select:focus {{
618
+ outline-color: {BRIZY_PRIMARY} !important;
619
+ border-color: {BRIZY_PRIMARY} !important;
620
+ box-shadow: 0 0 0 3px rgba(108,92,231,0.15) !important;
621
+ }}
622
+
623
+ /* Code block accents */
624
+ .gr-code .cm-editor, .gr-code textarea {{
625
+ border-radius: 10px !important;
626
+ border: 1px solid var(--app-border) !important;
627
+ }}
628
+
629
+ /* Tabs accent */
630
+ .gradio-container .tabs .tab-nav button[aria-selected="true"] {{
631
+ color: {BRIZY_PRIMARY} !important;
632
+ border-bottom: 2px solid {BRIZY_PRIMARY} !important;
633
+ }}
634
  """,
635
  ) as app:
636
  gr.Markdown(
637
  """
638
+ # AI Website Generator (Nebius)
639
+ Turn website screenshots into functional HTML using Nebius-compatible models.
640
+
641
+ - Image analysis (choose a vision model)
642
+ - Code generation (choose a code-capable model)
643
+ - Open in CodeSandbox editor to inspect code only
644
+ - Your key is used at runtime only
645
+ """,
646
+ elem_classes=["title"],
647
  )
648
 
649
  with gr.Accordion("API & Models", open=True):
 
657
  with gr.Row():
658
  vision_model_dd = gr.Dropdown(
659
  label="Vision Model",
660
+ choices=VISION_MODELS,
661
  value=DEFAULT_VISION_MODEL,
662
  allow_custom_value=True,
663
+ info="You can also type a custom model name supported by your Nebius endpoint.",
664
  )
665
  code_model_dd = gr.Dropdown(
666
  label="Code Generation Model",
667
+ choices=CODE_MODELS,
668
  value=DEFAULT_CODE_MODEL,
669
  allow_custom_value=True,
670
+ info="You can also type a custom model name supported by your Nebius endpoint.",
671
  )
672
  with gr.Row():
673
  code_max_tokens = gr.Slider(
 
687
  info="Higher is more creative; lower is more deterministic.",
688
  )
689
 
690
+ with gr.Tab("Quick Generate"):
691
  with gr.Row():
692
  with gr.Column(scale=1):
693
  gr.Markdown("### Step 1: Upload Screenshot", elem_classes=["section"])
 
697
  sources=["upload", "clipboard"],
698
  height=280,
699
  )
700
+ generate_btn = gr.Button("Generate Website", elem_classes=["primary-btn"])
701
 
702
  with gr.Column(scale=2):
703
  gr.Markdown("### Step 2: Review Results", elem_classes=["section"])
 
713
  )
714
 
715
  with gr.Row():
716
+ # Editor-only links; no live preview link provided
717
+ codesandbox_btn = gr.Button("Open in CodeSandbox Editor", elem_classes=["secondary-btn"])
718
+ download_btn = gr.Button("Download index.html", elem_classes=["secondary-btn"])
719
 
720
+ codesandbox_links = gr.Markdown(value="")
721
  download_file = gr.File(
722
  label="Download (index.html)",
723
  interactive=False,
724
  visible=False,
725
  )
726
 
727
+ with gr.Tab("Individual Tools"):
728
  with gr.Row():
729
  with gr.Column():
730
  gr.Markdown("### Image Analysis Tool", elem_classes=["section"])
731
  img_tool = gr.Image(type="pil", label="Image")
732
+ analyze_btn = gr.Button("Analyze Image", elem_classes=["secondary-btn"])
733
  analysis_result = gr.Textbox(label="Analysis Result", lines=6)
734
 
735
  with gr.Column():
736
  gr.Markdown("### Code Generation Tool", elem_classes=["section"])
737
  desc_input = gr.Textbox(label="Description", lines=4, placeholder="Describe the page you want...")
738
+ code_btn = gr.Button("Generate Code", elem_classes=["secondary-btn"])
739
  code_result = gr.Code(label="Generated Code", language="html")
740
 
741
  gr.Markdown("Made with Gradio • Nebius API compatible", elem_classes=["footer"])
 
748
  )
749
 
750
  def _deploy_to_codesandbox(html_code: str) -> str:
751
+ # Only return editor links; no live preview URLs
752
+ url_block = create_codesandbox(html_code)
753
+ if url_block.startswith("Error"):
754
+ return f"**{url_block}**"
755
+ lines = ["### CodeSandbox Editor Links", "", url_block]
756
+ return "\n".join(lines)
757
 
758
  codesandbox_btn.click(
759
  fn=_deploy_to_codesandbox,
760
  inputs=[html_output],
761
+ outputs=[codesandbox_links],
762
  )
763
 
764
  def _download_html(html_code: str):
 
785
  outputs=[code_result],
786
  )
787
 
 
 
 
788
  if __name__ == "__main__":
 
789
  app.launch(share=False)