natabrizy commited on
Commit
66fd1f4
Β·
verified Β·
1 Parent(s): fee64e7

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +570 -0
app.py ADDED
@@ -0,0 +1,570 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import io
3
+ import json
4
+ import os
5
+ import re
6
+ import tempfile
7
+ from typing import Tuple, Optional
8
+
9
+ import gradio as gr
10
+ import httpx
11
+ from PIL import Image
12
+ from lzstring import LZString
13
+
14
+ # =========================
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
+ # More forgiving timeouts and built-in retries
36
+ HTTP_TIMEOUTS = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=60.0)
37
+ HTTP_RETRIES = 2 # extra attempts on transient failures
38
+
39
+ # Use the same default key you provided
40
+ DEFAULT_NEBIUS_API_KEY = (
41
+ "eyJhbGciOiJIUzI1NiIsImtpZCI6IlV6SXJWd1h0dnprLVRvdzlLZWstc0M1akptWXBvX1VaVkxUZlpnMDRlOFUiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiJnb29nbGUtb2F1dGgyfDEwNTA1MTQzMDg2MDMwMzIxNDEwMiIsInNjb3BlIjoib3BlbmlkIG9mZmxpbmVfYWNjZXNzIiwiaXNzIjoiYXBpX2tleV9pc3N1ZXIiLCJhdWQiOlsiaHR0cHM6Ly9uZWJpdXMtaW5mZXJlbmNlLmV1LmF1dGgwLmNvbS9hcGkvdjIvIl0sImV4cCI6MTkwNjU5ODA0NCwidXVpZCI6ImNkOGFiMWZlLTIxN2QtNDJlMy04OWUwLWM1YTg4MjcwMGVhNyIsIm5hbWUiOiJodW5nZ2luZyIsImV4cGlyZXNfYXQiOiIyMDMwLTA2LTAyVDAyOjM0OjA0KzAwMDAifQ.MA52QuIiNruK7_lX688RXAEI2TkcCOjcf_02XrpnhI8"
42
+ )
43
+
44
+
45
+ # =========================
46
+ # Helpers
47
+ # =========================
48
+ def get_api_key(user_key: str = "") -> str:
49
+ """
50
+ Resolve the Nebius API key from:
51
+ 1) The provided user_key field
52
+ 2) The NEBIUS_API_KEY environment variable
53
+ 3) The built-in DEFAULT_NEBIUS_API_KEY
54
+ """
55
+ return (user_key or "").strip() or os.getenv("NEBIUS_API_KEY", "").strip() or DEFAULT_NEBIUS_API_KEY
56
+
57
+
58
+ def call_chat_completions(
59
+ model: str,
60
+ messages: list,
61
+ api_key: str,
62
+ max_tokens: int = 2000,
63
+ temperature: float = 0.7,
64
+ ) -> str:
65
+ """
66
+ Calls the Nebius OpenAI-compatible chat completions endpoint via HTTP.
67
+ Returns the assistant message content string.
68
+ Includes retries and increased read timeout to mitigate timeouts.
69
+ """
70
+ if not api_key:
71
+ raise ValueError("Nebius API key is required.")
72
+
73
+ url = f"{NEBIUS_BASE_URL}chat/completions"
74
+ payload = {
75
+ "model": model,
76
+ "messages": messages,
77
+ "max_tokens": max_tokens,
78
+ "temperature": temperature,
79
+ }
80
+ headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
81
+
82
+ transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
83
+ with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
84
+ resp = client.post(url, headers=headers, json=payload)
85
+ resp.raise_for_status()
86
+ data = resp.json()
87
+
88
+ choices = data.get("choices", [])
89
+ if not choices:
90
+ raise RuntimeError("No choices returned from the API.")
91
+ content = choices[0].get("message", {}).get("content", "")
92
+ if not content:
93
+ raise RuntimeError("Empty content returned from the API.")
94
+ return content
95
+
96
+
97
+ def _strip_fenced_code(text: str) -> str:
98
+ """
99
+ Removes ```html ... ``` fences from a content block if present.
100
+ """
101
+ s = text.strip()
102
+ if s.startswith("```html"):
103
+ s = s.split("```html", 1)[1].strip()
104
+ if s.endswith("```"):
105
+ s = s.rsplit("```", 1)[0].strip()
106
+ return s
107
+
108
+
109
+ def _split_assets(html_code: str) -> Tuple[str, str, str]:
110
+ """
111
+ Split inline <style> and <script> (without src) from the HTML into separate CSS and JS strings.
112
+ Return tuple: (updated_html, css_text, js_text)
113
+ """
114
+ if not html_code:
115
+ return html_code, "", ""
116
+
117
+ html = html_code
118
+
119
+ # Collect and remove style blocks
120
+ css_blocks = re.findall(r"<style[^>]*>(.*?)</style>", html, flags=re.IGNORECASE | re.DOTALL)
121
+ css_text = "\n\n".join(block.strip() for block in css_blocks if block.strip())
122
+ html = re.sub(r"<style[^>]*>.*?</style>", "", html, flags=re.IGNORECASE | re.DOTALL)
123
+
124
+ # Collect and remove inline scripts (no src)
125
+ js_blocks = []
126
+ def _script_repl(m):
127
+ attrs = m.group("attrs") or ""
128
+ code = m.group("code") or ""
129
+ if "src=" in attrs.lower():
130
+ return m.group(0) # keep external scripts
131
+ if code.strip():
132
+ js_blocks.append(code.strip())
133
+ return "" # remove inline script
134
+ html = re.sub(r"<script(?P<attrs>[^>]*)>(?P<code>.*?)</script>", _script_repl, html, flags=re.IGNORECASE | re.DOTALL)
135
+ js_text = "\n\n".join(js_blocks)
136
+
137
+ # If CSS collected, ensure link tag is added
138
+ if css_text:
139
+ if re.search(r"</head>", html, flags=re.IGNORECASE):
140
+ html = re.sub(r"</head>", ' <link rel="stylesheet" href="style.css">\n</head>', html, flags=re.IGNORECASE)
141
+ else:
142
+ # add a minimal head if missing
143
+ html = f"<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\"/>\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n{html}"
144
+
145
+ # If JS collected, ensure script tag before </body> or at end
146
+ if js_text:
147
+ if re.search(r"</body>", html, flags=re.IGNORECASE):
148
+ html = re.sub(r"</body>", ' <script src="script.js"></script>\n</body>', html, flags=re.IGNORECASE)
149
+ else:
150
+ html = html.rstrip() + '\n <script src="script.js"></script>\n'
151
+
152
+ return html, css_text, js_text
153
+
154
+
155
+ # =========================
156
+ # Core functions
157
+ # =========================
158
+ def analyze_image(
159
+ image: Optional[Image.Image],
160
+ nebius_api_key: str = "",
161
+ vision_model: str = DEFAULT_VISION_MODEL,
162
+ ) -> str:
163
+ """
164
+ Analyze an uploaded image and provide a concise description of its content and layout.
165
+ """
166
+ if image is None:
167
+ return "Error: No image provided."
168
+
169
+ api_key = get_api_key(nebius_api_key)
170
+ if not api_key:
171
+ return "Error: Nebius API key not provided."
172
+
173
+ try:
174
+ # Encode image to base64
175
+ buffered = io.BytesIO()
176
+ image.save(buffered, format="PNG")
177
+ img_b64 = base64.b64encode(buffered.getvalue()).decode("utf-8")
178
+
179
+ prompt = (
180
+ "Analyze this image and provide a concise description. "
181
+ "Describe the main elements, colors, layout, and UI components. "
182
+ "Identify what type of website or application this resembles. "
183
+ "Focus on structural and visual elements that would be important for recreating the design."
184
+ )
185
+
186
+ messages = [
187
+ {
188
+ "role": "user",
189
+ "content": [
190
+ {"type": "text", "text": prompt},
191
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}"}},
192
+ ],
193
+ }
194
+ ]
195
+
196
+ content = call_chat_completions(
197
+ model=vision_model,
198
+ messages=messages,
199
+ api_key=api_key,
200
+ max_tokens=1000,
201
+ temperature=0.7,
202
+ )
203
+ return content
204
+ except Exception as e:
205
+ return f"Error analyzing image: {str(e)}"
206
+
207
+
208
+ def generate_html_code(
209
+ description: str,
210
+ nebius_api_key: str = "",
211
+ code_model: str = DEFAULT_CODE_MODEL,
212
+ code_max_tokens: int = 4000,
213
+ code_temperature: float = 0.7,
214
+ ) -> str:
215
+ """
216
+ Generate HTML/CSS/JavaScript code based on a website description.
217
+ """
218
+ if not description or description.startswith("Error"):
219
+ return "Error: Invalid or missing description."
220
+
221
+ api_key = get_api_key(nebius_api_key)
222
+ if not api_key:
223
+ return "Error: Nebius API key not provided."
224
+
225
+ prompt = f"""
226
+ Generate a complete, responsive webpage based on this description:
227
+
228
+ {description}
229
+
230
+ Requirements:
231
+ - Use modern HTML5, CSS3, and vanilla JavaScript only
232
+ - Include TailwindCSS via CDN for styling
233
+ - Make it responsive and visually appealing
234
+ - Use placeholder images from https://unsplash.com/ if needed
235
+ - Include proper semantic HTML structure
236
+ - Add interactive elements where appropriate
237
+ - Ensure the design matches the described layout and style
238
+
239
+ Return only the complete HTML code starting with <!DOCTYPE html> and ending with </html>.
240
+ """.strip()
241
+
242
+ try:
243
+ messages = [{"role": "user", "content": prompt}]
244
+ content = call_chat_completions(
245
+ model=code_model,
246
+ messages=messages,
247
+ api_key=api_key,
248
+ max_tokens=code_max_tokens,
249
+ temperature=code_temperature,
250
+ )
251
+ html_code = _strip_fenced_code(content)
252
+
253
+ if "<!DOCTYPE html>" in html_code and "</html>" in html_code:
254
+ start = html_code.find("<!DOCTYPE html>")
255
+ end = html_code.rfind("</html>") + len("</html>")
256
+ return html_code[start:end]
257
+ return html_code
258
+ except (httpx.ReadTimeout, httpx.TimeoutException):
259
+ # Retry once with a reduced token budget to improve latency
260
+ try:
261
+ reduced_tokens = max(1000, int(code_max_tokens * 0.6))
262
+ messages = [{"role": "user", "content": prompt}]
263
+ content = call_chat_completions(
264
+ model=code_model,
265
+ messages=messages,
266
+ api_key=api_key,
267
+ max_tokens=reduced_tokens,
268
+ temperature=code_temperature,
269
+ )
270
+ html_code = _strip_fenced_code(content)
271
+ if "<!DOCTYPE html>" in html_code and "</html>" in html_code:
272
+ start = html_code.find("<!DOCTYPE html>")
273
+ end = html_code.rfind("</html>") + len("</html>")
274
+ return html_code[start:end]
275
+ return html_code
276
+ except Exception as e2:
277
+ return f"Error generating HTML code after retry: {str(e2)}. Tips: lower Max tokens, pick a smaller/faster model, or try again."
278
+ except Exception as e:
279
+ return f"Error generating HTML code: {str(e)}"
280
+
281
+
282
+ def create_codesandbox(html_code: str) -> str:
283
+ """
284
+ Create a CodeSandbox project from HTML code.
285
+ Splits inline CSS/JS into separate files so you can open index.html and style.css directly.
286
+ Returns Markdown with direct links to open index.html (and style.css/script.js if present) in the editor.
287
+ """
288
+ if not html_code or html_code.startswith("Error"):
289
+ return "Error: No valid HTML code provided."
290
+
291
+ try:
292
+ updated_html, css_text, js_text = _split_assets(html_code)
293
+
294
+ files = {
295
+ "index.html": {"content": updated_html, "isBinary": False},
296
+ }
297
+ if css_text:
298
+ files["style.css"] = {"content": css_text, "isBinary": False}
299
+ if js_text:
300
+ files["script.js"] = {"content": js_text, "isBinary": False}
301
+
302
+ # package.json is optional for static template
303
+ files["package.json"] = {
304
+ "content": json.dumps(
305
+ {
306
+ "name": "ai-generated-website",
307
+ "version": "1.0.0",
308
+ "description": "Website generated from image analysis",
309
+ "main": "index.html",
310
+ "scripts": {"start": "serve .", "build": "echo 'No build required'"},
311
+ "devDependencies": {"serve": "^14.0.0"},
312
+ },
313
+ indent=2,
314
+ ),
315
+ "isBinary": False,
316
+ }
317
+
318
+ parameters = {"files": files, "template": "static"}
319
+
320
+ # Fallback GET URL with compressed parameters (also add file query to open index.html)
321
+ json_str = json.dumps(parameters, separators=(",", ":"))
322
+ lz = LZString()
323
+ compressed = lz.compressToBase64(json_str)
324
+ compressed = compressed.replace("+", "-").replace("/", "_").rstrip("=")
325
+ prefill_base = "https://codesandbox.io/api/v1/sandboxes/define"
326
+ prefill_index = f"{prefill_base}?parameters={compressed}&file=/index.html"
327
+ prefill_css = f"{prefill_base}?parameters={compressed}&file=/style.css" if "style.css" in files else ""
328
+ prefill_js = f"{prefill_base}?parameters={compressed}&file=/script.js" if "script.js" in files else ""
329
+
330
+ # Try POST API to get a sandbox_id so we can link directly to the editor
331
+ url = "https://codesandbox.io/api/v1/sandboxes/define"
332
+ transport = httpx.HTTPTransport(retries=HTTP_RETRIES)
333
+ with httpx.Client(timeout=HTTP_TIMEOUTS, transport=transport) as client:
334
+ resp = client.post(url, json=parameters)
335
+ if resp.status_code == 200:
336
+ data = resp.json()
337
+ sandbox_id = data.get("sandbox_id")
338
+ if sandbox_id:
339
+ editor_base = f"https://codesandbox.io/p/sandbox/{sandbox_id}"
340
+ preview_base = f"https://codesandbox.io/s/{sandbox_id}"
341
+ lines = [
342
+ f"- Open index.html in editor: {editor_base}?file=/index.html",
343
+ ]
344
+ if "style.css" in files:
345
+ lines.append(f"- Open style.css in editor: {editor_base}?file=/style.css")
346
+ if "script.js" in files:
347
+ lines.append(f"- Open script.js in editor: {editor_base}?file=/script.js")
348
+ lines.append(f"- Live preview: {preview_base}")
349
+ return "\n".join(lines)
350
+
351
+ # Fallback to prefill URLs if POST fails
352
+ lines = [f"- Open index.html in editor: {prefill_index}"]
353
+ if prefill_css:
354
+ lines.append(f"- Open style.css in editor: {prefill_css}")
355
+ if prefill_js:
356
+ lines.append(f"- Open script.js in editor: {prefill_js}")
357
+ return "\n".join(lines)
358
+
359
+ except Exception as e:
360
+ return f"Error creating CodeSandbox: {str(e)}"
361
+
362
+
363
+ def screenshot_to_code(
364
+ image: Optional[Image.Image],
365
+ nebius_api_key: str = "",
366
+ vision_model: str = DEFAULT_VISION_MODEL,
367
+ code_model: str = DEFAULT_CODE_MODEL,
368
+ code_max_tokens: int = 4000,
369
+ code_temperature: float = 0.7,
370
+ ) -> Tuple[str, str]:
371
+ """
372
+ Complete pipeline: analyze image and generate corresponding HTML code.
373
+ Returns (description, html_code).
374
+ """
375
+ description = analyze_image(image, nebius_api_key, vision_model)
376
+ if description.startswith("Error"):
377
+ return description, "Error: Cannot generate code due to image analysis failure."
378
+ html_code = generate_html_code(
379
+ description,
380
+ nebius_api_key,
381
+ code_model=code_model,
382
+ code_max_tokens=code_max_tokens,
383
+ code_temperature=code_temperature,
384
+ )
385
+ return description, html_code
386
+
387
+
388
+ def export_html_to_file(html_code: str) -> Optional[str]:
389
+ """
390
+ Writes the HTML code to a temporary .html file and returns its path for download.
391
+ """
392
+ if not html_code or html_code.startswith("Error"):
393
+ return None
394
+ try:
395
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".html")
396
+ with open(tmp.name, "w", encoding="utf-8") as f:
397
+ f.write(html_code)
398
+ return tmp.name
399
+ except Exception:
400
+ return None
401
+
402
+
403
+ # =========================
404
+ # Gradio UI (English-only, user-friendly)
405
+ # =========================
406
+ with gr.Blocks(
407
+ theme=gr.themes.Soft(),
408
+ title="AI Website Generator (Nebius)",
409
+ css="""
410
+ .section { border: 1px solid #e5e7eb; padding: 16px; border-radius: 10px; margin: 10px 0; }
411
+ .muted { color: #6b7280; font-size: 0.9em; }
412
+ .footer { text-align:center; color:#9ca3af; padding: 8px 0; }
413
+ """,
414
+ ) as app:
415
+ gr.Markdown(
416
+ """
417
+ # πŸš€ AI Website Generator (Nebius)
418
+ Turn website screenshots into functional HTML using Nebius-compatible models.
419
+
420
+ - πŸ“Έ Image analysis (choose a vision model)
421
+ - πŸ’» Code generation (choose a code-capable model)
422
+ - 🌐 One-click CodeSandbox deployment
423
+ - 🧩 Editor links open index.html and style.css directly
424
+ - πŸ”’ Your key is used at runtime only
425
+ """
426
+ )
427
+
428
+ with gr.Accordion("API & Models", open=True):
429
+ gr.Markdown("Provide your Nebius API key or use the default configured in this app.", elem_classes=["muted"])
430
+ nebius_key = gr.Textbox(
431
+ label="Nebius API Key",
432
+ type="password",
433
+ placeholder="Paste your Nebius API key, or leave as-is to use the default key.",
434
+ value=DEFAULT_NEBIUS_API_KEY,
435
+ )
436
+ with gr.Row():
437
+ vision_model_dd = gr.Dropdown(
438
+ label="Vision Model",
439
+ choices=VISION_MODELS,
440
+ value=DEFAULT_VISION_MODEL,
441
+ allow_custom_value=True,
442
+ info="You can also type a custom model name supported by your Nebius endpoint.",
443
+ )
444
+ code_model_dd = gr.Dropdown(
445
+ label="Code Generation Model",
446
+ choices=CODE_MODELS,
447
+ value=DEFAULT_CODE_MODEL,
448
+ allow_custom_value=True,
449
+ info="You can also type a custom model name supported by your Nebius endpoint.",
450
+ )
451
+ with gr.Row():
452
+ code_max_tokens = gr.Slider(
453
+ label="Max tokens (code generation)",
454
+ minimum=500,
455
+ maximum=8000,
456
+ step=100,
457
+ value=4000,
458
+ info="Lower this if you see timeouts; higher values may take longer.",
459
+ )
460
+ code_temperature = gr.Slider(
461
+ label="Temperature",
462
+ minimum=0.0,
463
+ maximum=1.5,
464
+ step=0.1,
465
+ value=0.7,
466
+ info="Higher is more creative; lower is more deterministic.",
467
+ )
468
+
469
+ with gr.Tab("🎯 Quick Generate"):
470
+ with gr.Row():
471
+ with gr.Column(scale=1):
472
+ gr.Markdown("### Step 1: Upload Screenshot", elem_classes=["section"])
473
+ image_input = gr.Image(
474
+ type="pil",
475
+ label="Website Screenshot",
476
+ sources=["upload", "clipboard"],
477
+ height=280,
478
+ )
479
+ generate_btn = gr.Button("🎨 Generate Website", variant="primary")
480
+
481
+ with gr.Column(scale=2):
482
+ gr.Markdown("### Step 2: Review Results", elem_classes=["section"])
483
+ description_output = gr.Textbox(
484
+ label="Image Analysis",
485
+ lines=6,
486
+ interactive=False,
487
+ )
488
+ html_output = gr.Code(
489
+ label="Generated HTML (copy or download)",
490
+ language="html",
491
+ lines=18,
492
+ )
493
+
494
+ with gr.Row():
495
+ codesandbox_btn = gr.Button("πŸš€ Deploy to CodeSandbox")
496
+ download_btn = gr.Button("πŸ’Ύ Download index.html")
497
+
498
+ codesandbox_links = gr.Markdown(value="")
499
+ download_file = gr.File(
500
+ label="Download (index.html)",
501
+ interactive=False,
502
+ visible=False,
503
+ )
504
+
505
+ with gr.Tab("πŸ”§ Individual Tools"):
506
+ with gr.Row():
507
+ with gr.Column():
508
+ gr.Markdown("### Image Analysis Tool", elem_classes=["section"])
509
+ img_tool = gr.Image(type="pil", label="Image")
510
+ analyze_btn = gr.Button("Analyze Image")
511
+ analysis_result = gr.Textbox(label="Analysis Result", lines=6)
512
+
513
+ with gr.Column():
514
+ gr.Markdown("### Code Generation Tool", elem_classes=["section"])
515
+ desc_input = gr.Textbox(label="Description", lines=4, placeholder="Describe the page you want...")
516
+ code_btn = gr.Button("Generate Code")
517
+ code_result = gr.Code(label="Generated Code", language="html")
518
+
519
+ gr.Markdown("Made with Gradio β€’ Nebius API compatible", elem_classes=["footer"])
520
+
521
+ # Event bindings
522
+ generate_btn.click(
523
+ fn=screenshot_to_code,
524
+ inputs=[image_input, nebius_key, vision_model_dd, code_model_dd, code_max_tokens, code_temperature],
525
+ outputs=[description_output, html_output],
526
+ )
527
+
528
+ def _deploy_to_codesandbox(html_code: str) -> str:
529
+ url_block = create_codesandbox(html_code)
530
+ if url_block.startswith("Error"):
531
+ return f"**{url_block}**"
532
+ # Render as Markdown list
533
+ lines = ["### CodeSandbox Links", "", url_block]
534
+ return "\n".join(lines)
535
+
536
+ codesandbox_btn.click(
537
+ fn=_deploy_to_codesandbox,
538
+ inputs=[html_output],
539
+ outputs=[codesandbox_links],
540
+ )
541
+
542
+ def _download_html(html_code: str):
543
+ path = export_html_to_file(html_code)
544
+ return gr.update(value=path, visible=bool(path))
545
+
546
+ download_btn.click(
547
+ fn=_download_html,
548
+ inputs=[html_output],
549
+ outputs=[download_file],
550
+ )
551
+
552
+ analyze_btn.click(
553
+ fn=lambda img, key, vmod: analyze_image(img, key, vmod),
554
+ inputs=[img_tool, nebius_key, vision_model_dd],
555
+ outputs=[analysis_result],
556
+ )
557
+
558
+ code_btn.click(
559
+ fn=lambda desc, key, cmod, mtoks, temp: generate_html_code(
560
+ desc, key, code_model=cmod, code_max_tokens=mtoks, code_temperature=temp
561
+ ),
562
+ inputs=[desc_input, nebius_key, code_model_dd, code_max_tokens, code_temperature],
563
+ outputs=[code_result],
564
+ )
565
+
566
+ # Optional examples (uncomment and provide your own image file)
567
+ # gr.Examples(examples=[["1.jpg"]], inputs=[image_input], label="Example Screenshots")
568
+
569
+ if __name__ == "__main__":
570
+ app.launch(share=False)