natabrizy commited on
Commit
edb2f68
·
verified ·
1 Parent(s): 63afbbc

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +585 -1396
app.py CHANGED
@@ -1,1412 +1,601 @@
1
  import gradio as gr
2
- import torch
3
- import spaces
4
- import json
5
- import asyncio
6
- from typing import Dict, List, Optional, Tuple
7
- import aiohttp
8
- from PIL import Image
9
- import numpy as np
10
  import base64
11
  import io
12
- from datetime import datetime
13
  import os
14
- import requests
15
- from urllib.parse import quote
16
- from transformers import (
17
- AutoProcessor,
18
- AutoModelForCausalLM,
19
- AutoTokenizer,
20
- pipeline
21
- )
22
  import re
 
23
 
24
- # API Configurations
25
- NEBIUS_API_ENDPOINT = os.getenv("NEBIUS_API_ENDPOINT", "https://api.nebius.ai/v1")
26
- NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY", "")
27
- UNSPLASH_ACCESS_KEY = os.getenv("UNSPLASH_ACCESS_KEY", "demo")
28
-
29
- # Model configurations - Different from reference app
30
- VISION_MODEL = "microsoft/Florence-2-large" # Alternative to Qwen2.5-VL
31
- CODE_MODEL = "Phind/Phind-CodeLlama-34B-v2" # Alternative to DeepSeek-V3
32
-
33
- class MCPWebsiteGenerator:
34
- """MCP-Compatible Website Generator via Nebius AI"""
35
-
36
- def __init__(self):
37
- self.device = "cuda" if torch.cuda.is_available() else "cpu"
38
- self.mcp_client = None
39
- self.session_id = None
40
- self.tools = {
41
- "analyze_image": self.analyze_image,
42
- "generate_html_code": self.generate_html_code,
43
- "create_codesandbox": self.create_codesandbox,
44
- "screenshot_to_code": self.screenshot_to_code
45
- }
46
- self.init_mcp()
47
-
48
- def init_mcp(self):
49
- """Initialize MCP client configuration"""
50
- self.mcp_config = {
51
- "protocol": "MCP",
52
- "version": "1.0",
53
- "capabilities": {
54
- "tools": list(self.tools.keys()),
55
- "streaming": True,
56
- "context_window": 128000
57
- },
58
- "models": {
59
- "vision": VISION_MODEL,
60
- "code": CODE_MODEL
61
- }
62
- }
63
-
64
- async def connect_mcp(self):
65
- """Establish MCP connection with Nebius AI"""
66
- headers = {
67
- "Authorization": f"Bearer {NEBIUS_API_KEY}",
68
- "Content-Type": "application/json",
69
- "X-MCP-Version": "1.0"
70
- }
71
-
72
- async with aiohttp.ClientSession() as session:
73
- payload = {
74
- "protocol": "MCP",
75
- "config": self.mcp_config,
76
- "timestamp": datetime.utcnow().isoformat()
77
- }
78
-
79
- async with session.post(
80
- f"{NEBIUS_API_ENDPOINT}/mcp/connect",
81
- headers=headers,
82
- json=payload
83
- ) as response:
84
- if response.status == 200:
85
- data = await response.json()
86
- self.session_id = data.get("session_id")
87
- return True
88
- return False
89
-
90
- def search_unsplash(self, query: str, count: int = 6) -> List[Dict]:
91
- """Search Unsplash for free stock photos"""
92
- if not query:
 
 
 
 
 
 
 
 
 
 
 
 
93
  return []
94
-
95
- try:
96
- headers = {"Authorization": f"Client-ID {UNSPLASH_ACCESS_KEY}"}
97
- params = {
98
- "query": query,
99
- "per_page": count,
100
- "orientation": "landscape"
101
- }
102
-
103
- response = requests.get(
104
- "https://api.unsplash.com/search/photos",
105
- headers=headers,
106
- params=params
107
  )
108
-
109
- if response.status_code == 200:
110
- data = response.json()
111
- images = []
112
- for photo in data.get("results", []):
113
- images.append({
114
- "url": photo["urls"]["regular"],
115
- "thumb": photo["urls"]["thumb"],
116
- "author": photo["user"]["name"],
117
- "author_url": photo["user"]["links"]["html"],
118
- "download": photo["links"]["download"]
119
- })
120
- return images
121
- return []
122
-
123
- except Exception as e:
124
- print(f"Unsplash search error: {e}")
125
- return []
126
-
127
- def extract_design_system(self, image: Image.Image) -> Dict:
128
- """Extract comprehensive design system from image"""
129
- # Convert to array for analysis
130
- img_array = np.array(image)
131
- height, width = img_array.shape[:2]
132
-
133
- # Color extraction using K-means clustering
134
- from sklearn.cluster import KMeans
135
-
136
- # Resize for faster processing
137
- small_img = image.resize((150, 150))
138
- pixels = np.array(small_img).reshape(-1, 3)
139
-
140
- # Extract dominant colors
141
- n_colors = 8
142
- kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=10)
143
- kmeans.fit(pixels)
144
- colors = kmeans.cluster_centers_.astype(int)
145
-
146
- # Convert to hex
147
- hex_colors = ['#%02x%02x%02x' % tuple(color) for color in colors]
148
-
149
- # Analyze layout structure
150
- layout_info = {
151
- "dimensions": {"width": width, "height": height},
152
- "aspect_ratio": round(width / height, 2),
153
- "is_mobile": width < 768,
154
- "is_tablet": 768 <= width < 1024,
155
- "is_desktop": width >= 1024,
156
- "has_header": self._detect_header(img_array),
157
- "has_sidebar": self._detect_sidebar(img_array),
158
- "has_footer": self._detect_footer(img_array),
159
- "grid_columns": self._detect_columns(img_array),
160
- "content_sections": self._detect_sections(img_array)
161
- }
162
-
163
- return {
164
- "colors": {
165
- "primary": hex_colors[0],
166
- "secondary": hex_colors[1],
167
- "accent": hex_colors[2],
168
- "background": self._find_background_color(hex_colors),
169
- "surface": hex_colors[4],
170
- "text": self._find_text_color(hex_colors),
171
- "muted": hex_colors[6],
172
- "border": hex_colors[7]
173
- },
174
- "layout": layout_info,
175
- "typography": self._analyze_typography(img_array),
176
- "spacing": self._analyze_spacing(img_array)
177
- }
178
-
179
- def _detect_header(self, img_array):
180
- """Detect if image has a header section"""
181
- height = img_array.shape[0]
182
- top_section = img_array[:int(height * 0.15), :]
183
- rest = img_array[int(height * 0.15):, :]
184
-
185
- # Compare color variance
186
- top_variance = np.var(top_section)
187
- rest_variance = np.var(rest)
188
-
189
- return abs(top_variance - rest_variance) > 1000
190
-
191
- def _detect_sidebar(self, img_array):
192
- """Detect sidebar presence"""
193
- width = img_array.shape[1]
194
- if width < 800:
195
- return False
196
-
197
- left_section = img_array[:, :int(width * 0.25)]
198
- right_section = img_array[:, int(width * 0.75):]
199
- center = img_array[:, int(width * 0.25):int(width * 0.75)]
200
-
201
- left_diff = np.mean(np.abs(left_section - center))
202
- right_diff = np.mean(np.abs(right_section - center))
203
-
204
- return left_diff > 50 or right_diff > 50
205
-
206
- def _detect_footer(self, img_array):
207
- """Detect footer section"""
208
- height = img_array.shape[0]
209
- bottom_section = img_array[int(height * 0.85):, :]
210
- rest = img_array[:int(height * 0.85), :]
211
-
212
- bottom_variance = np.var(bottom_section)
213
- rest_variance = np.var(rest)
214
-
215
- return abs(bottom_variance - rest_variance) > 1000
216
-
217
- def _detect_columns(self, img_array):
218
- """Detect number of content columns"""
219
- width = img_array.shape[1]
220
- if width < 600:
221
- return 1
222
- elif width < 900:
223
- return 2
224
- elif width < 1200:
225
- return 3
226
- else:
227
- return 4
228
-
229
- def _detect_sections(self, img_array):
230
- """Detect content sections"""
231
- height = img_array.shape[0]
232
- sections = []
233
-
234
- # Divide into horizontal sections
235
- section_height = height // 6
236
- for i in range(6):
237
- start = i * section_height
238
- end = (i + 1) * section_height
239
- section = img_array[start:end, :]
240
-
241
- # Analyze section characteristics
242
- variance = np.var(section)
243
- mean_color = np.mean(section)
244
-
245
- sections.append({
246
- "index": i,
247
- "variance": float(variance),
248
- "dominant_tone": "light" if mean_color > 128 else "dark"
249
- })
250
-
251
- return sections
252
-
253
- def _find_background_color(self, colors):
254
- """Identify most likely background color"""
255
- # Convert hex to RGB for analysis
256
- rgb_colors = []
257
- for color in colors:
258
- r = int(color[1:3], 16)
259
- g = int(color[3:5], 16)
260
- b = int(color[5:7], 16)
261
- rgb_colors.append((r, g, b))
262
-
263
- # Background is usually light or very dark
264
- brightest = max(rgb_colors, key=lambda c: sum(c))
265
- darkest = min(rgb_colors, key=lambda c: sum(c))
266
-
267
- # Return brightest if it's light enough, otherwise darkest
268
- if sum(brightest) > 600: # Light background
269
- return colors[rgb_colors.index(brightest)]
270
- else:
271
- return colors[rgb_colors.index(darkest)]
272
-
273
- def _find_text_color(self, colors):
274
- """Identify most likely text color"""
275
- rgb_colors = []
276
- for color in colors:
277
- r = int(color[1:3], 16)
278
- g = int(color[3:5], 16)
279
- b = int(color[5:7], 16)
280
- rgb_colors.append((r, g, b))
281
-
282
- # Text is usually dark on light bg or light on dark bg
283
- darkest = min(rgb_colors, key=lambda c: sum(c))
284
-
285
- # Return darkest color for text
286
- return colors[rgb_colors.index(darkest)]
287
-
288
- def _analyze_typography(self, img_array):
289
- """Analyze typography characteristics"""
290
- return {
291
- "has_large_headings": True,
292
- "has_body_text": True,
293
- "estimated_font_scale": "16px",
294
- "line_height": "1.6"
295
- }
296
-
297
- def _analyze_spacing(self, img_array):
298
- """Analyze spacing patterns"""
299
- return {
300
- "padding": "consistent",
301
- "margins": "generous",
302
- "component_gap": "24px"
303
- }
304
-
305
- @spaces.GPU(duration=120)
306
- async def analyze_image(self, image: Image.Image, additional_prompt: str = "") -> Dict:
307
- """MCP Tool: Analyze website screenshot"""
308
- if not self.session_id:
309
- await self.connect_mcp()
310
-
311
- # Extract design system
312
- design_system = self.extract_design_system(image)
313
-
314
- # Prepare image for AI analysis
315
- img_base64 = self._image_to_base64(image)
316
-
317
- # MCP request for vision analysis
318
- mcp_request = {
319
- "session_id": self.session_id,
320
- "tool": "analyze_image",
321
- "parameters": {
322
- "image": img_base64,
323
- "design_system": design_system,
324
- "prompt": f"""Analyze this website screenshot comprehensively:
325
-
326
- 1. Visual Hierarchy:
327
- - Header structure and navigation
328
- - Hero/banner sections
329
- - Content sections and their purposes
330
- - Footer elements
331
-
332
- 2. Design Patterns:
333
- - Component types (cards, lists, grids, etc.)
334
- - Interactive elements (buttons, forms, links)
335
- - Media usage (images, videos, icons)
336
-
337
- 3. Layout Analysis:
338
- - Grid system and column structure
339
- - Spacing and alignment patterns
340
- - Responsive design indicators
341
-
342
- 4. Content Structure:
343
- - Text hierarchy (headings, paragraphs)
344
- - Information organization
345
- - Call-to-action placement
346
-
347
- 5. Style Characteristics:
348
- - Design style (modern, minimal, corporate, etc.)
349
- - Color usage and contrast
350
- - Typography choices
351
- - Visual effects (shadows, borders, gradients)
352
-
353
- {additional_prompt}
354
-
355
- Provide detailed analysis for accurate replication.""",
356
- "model": VISION_MODEL
357
- }
358
- }
359
-
360
- # Send to Nebius AI
361
- headers = {
362
- "Authorization": f"Bearer {NEBIUS_API_KEY}",
363
- "Content-Type": "application/json"
364
- }
365
-
366
- async with aiohttp.ClientSession() as session:
367
- async with session.post(
368
- f"{NEBIUS_API_ENDPOINT}/mcp/execute",
369
- headers=headers,
370
- json=mcp_request
371
- ) as response:
372
- if response.status == 200:
373
- result = await response.json()
374
- return {
375
- "analysis": result.get("output", ""),
376
- "design_system": design_system,
377
- "status": "success"
378
- }
379
- else:
380
- # Fallback analysis
381
- return {
382
- "analysis": self._fallback_analysis(design_system),
383
- "design_system": design_system,
384
- "status": "fallback"
385
- }
386
-
387
- def _image_to_base64(self, image: Image.Image) -> str:
388
- """Convert PIL image to base64"""
389
- buffered = io.BytesIO()
390
- image.save(buffered, format="PNG")
391
- return base64.b64encode(buffered.getvalue()).decode()
392
-
393
- def _fallback_analysis(self, design_system):
394
- """Fallback analysis when AI is unavailable"""
395
- layout = design_system["layout"]
396
- colors = design_system["colors"]
397
-
398
- return f"""Website Analysis (Fallback Mode):
399
-
400
- Layout Structure:
401
- - Dimensions: {layout['dimensions']['width']}x{layout['dimensions']['height']}px
402
- - Device Type: {'Mobile' if layout['is_mobile'] else 'Tablet' if layout['is_tablet'] else 'Desktop'}
403
- - Header: {'Detected' if layout['has_header'] else 'Not detected'}
404
- - Sidebar: {'Detected' if layout['has_sidebar'] else 'Not detected'}
405
- - Footer: {'Detected' if layout['has_footer'] else 'Not detected'}
406
- - Grid Columns: {layout['grid_columns']}
407
-
408
- Color Scheme:
409
- - Primary: {colors['primary']}
410
- - Secondary: {colors['secondary']}
411
- - Background: {colors['background']}
412
- - Text: {colors['text']}
413
-
414
- Ready for code generation."""
415
-
416
- @spaces.GPU(duration=120)
417
- async def generate_html_code(self, analysis: Dict, include_unsplash: bool = False, image_query: str = "") -> str:
418
- """MCP Tool: Generate HTML/CSS/JS code from analysis"""
419
- if not self.session_id:
420
- await self.connect_mcp()
421
-
422
- # Get Unsplash images if requested
423
- unsplash_images = []
424
- if include_unsplash and image_query:
425
- unsplash_images = self.search_unsplash(image_query)
426
-
427
- design_system = analysis.get("design_system", {})
428
-
429
- # MCP request for code generation
430
- mcp_request = {
431
- "session_id": self.session_id,
432
- "tool": "generate_html_code",
433
- "parameters": {
434
- "analysis": analysis.get("analysis", ""),
435
- "design_system": design_system,
436
- "unsplash_images": unsplash_images,
437
- "prompt": f"""Generate a complete, production-ready HTML file based on the analysis.
438
 
 
 
 
439
 
440
- Requirements:
441
- 1. Single HTML file with all CSS and JavaScript embedded
442
- 2. Use the exact color scheme from the design system
443
- 3. Implement the detected layout structure
444
- 4. Include all identified UI components
445
- 5. Ensure responsive design with mobile-first approach
446
- 6. Add smooth animations and transitions
447
- 7. Include semantic HTML5 elements
448
- 8. Implement accessibility features (ARIA labels, proper contrast)
449
- 9. Add interactive JavaScript functionality
450
- 10. {f'Include Unsplash images in appropriate sections' if unsplash_images else 'Use placeholder content for images'}
451
-
452
- Design System Colors:
453
- - Primary: {design_system.get('colors', {}).get('primary', '#4f46e5')}
454
- - Secondary: {design_system.get('colors', {}).get('secondary', '#6366f1')}
455
- - Background: {design_system.get('colors', {}).get('background', '#ffffff')}
456
- - Text: {design_system.get('colors', {}).get('text', '#1f2937')}
457
-
458
- Generate complete HTML with embedded CSS and JavaScript:""",
459
- "model": CODE_MODEL,
460
- "stream": True
461
- }
462
- }
463
-
464
- headers = {
465
- "Authorization": f"Bearer {NEBIUS_API_KEY}",
466
- "Content-Type": "application/json"
467
- }
468
-
469
- generated_code = ""
470
-
471
- async with aiohttp.ClientSession() as session:
472
- async with session.post(
473
- f"{NEBIUS_API_ENDPOINT}/mcp/execute",
474
- headers=headers,
475
- json=mcp_request
476
- ) as response:
477
- if response.status == 200:
478
- async for line in response.content:
479
- if line:
480
- try:
481
- data = json.loads(line.decode('utf-8'))
482
- if data.get("type") == "content":
483
- generated_code += data.get("content", "")
484
- except:
485
- continue
486
-
487
- if generated_code:
488
- return self._enhance_html(generated_code, design_system, unsplash_images)
489
-
490
- # Fallback to template
491
- return self._create_template(design_system, unsplash_images)
492
-
493
- def _enhance_html(self, html_code, design_system, unsplash_images):
494
- """Enhance generated HTML with additional features"""
495
- # If HTML is incomplete or invalid, use template
496
- if not self._validate_html(html_code):
497
- return self._create_template(design_system, unsplash_images)
498
-
499
- # Add Unsplash images if not already included
500
- if unsplash_images and "unsplash" not in html_code.lower():
501
- html_code = self._inject_images(html_code, unsplash_images)
502
-
503
  return html_code
504
-
505
- def _validate_html(self, html):
506
- """Validate HTML structure"""
507
- required_tags = ['<!DOCTYPE', '<html', '<head>', '<body>', '</html>']
508
- return all(tag in html for tag in required_tags)
509
-
510
- def _inject_images(self, html, images):
511
- """Inject Unsplash images into HTML"""
512
- # This would intelligently inject images into appropriate sections
513
- return html
514
-
515
- def _create_template(self, design_system, unsplash_images=None):
516
- """Create comprehensive HTML template"""
517
- colors = design_system.get('colors', {})
518
- layout = design_system.get('layout', {})
519
-
520
- # Build image gallery if images available
521
- image_section = ""
522
- if unsplash_images:
523
- image_cards = ""
524
- for img in unsplash_images[:6]:
525
- image_cards += f"""
526
- <div class="image-card">
527
- <img src="{img['url']}" alt="Photo by {img['author']}" loading="lazy">
528
- <div class="image-overlay">
529
- <p class="image-author">Photo by <a href="{img['author_url']}" target="_blank" rel="noopener">{img['author']}</a></p>
530
- </div>
531
- </div>"""
532
-
533
- image_section = f"""
534
- <section class="gallery" id="gallery">
535
- <div class="container">
536
- <div class="section-header">
537
- <h2 class="section-title">Visual Gallery</h2>
538
- <p class="section-subtitle">Stunning imagery powered by Unsplash</p>
539
- </div>
540
- <div class="gallery-grid">
541
- {image_cards}
542
- </div>
543
- </div>
544
- </section>"""
545
-
546
- return f"""<!DOCTYPE html>
547
- <html lang="en">
548
- <head>
549
- <meta charset="UTF-8">
550
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
551
- <meta name="description" content="AI-generated website using MCP protocol via Nebius AI">
552
- <title>AI Generated Website - MCP Nebius</title>
553
- <style>
554
- /* CSS Reset */
555
- *, *::before, *::after {{
556
- margin: 0;
557
- padding: 0;
558
- box-sizing: border-box;
559
- }}
560
-
561
- /* Custom Properties */
562
- :root {{
563
- --primary: {colors.get('primary', '#4f46e5')};
564
- --secondary: {colors.get('secondary', '#6366f1')};
565
- --accent: {colors.get('accent', '#818cf8')};
566
- --background: {colors.get('background', '#ffffff')};
567
- --surface: {colors.get('surface', '#f9fafb')};
568
- --text: {colors.get('text', '#1f2937')};
569
- --text-muted: {colors.get('muted', '#6b7280')};
570
- --border: {colors.get('border', '#e5e7eb')};
571
-
572
- --spacing-xs: 0.25rem;
573
- --spacing-sm: 0.5rem;
574
- --spacing-md: 1rem;
575
- --spacing-lg: 1.5rem;
576
- --spacing-xl: 2rem;
577
- --spacing-2xl: 3rem;
578
- --spacing-3xl: 4rem;
579
-
580
- --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
581
- --font-size-sm: 0.875rem;
582
- --font-size-base: 1rem;
583
- --font-size-lg: 1.125rem;
584
- --font-size-xl: 1.25rem;
585
- --font-size-2xl: 1.5rem;
586
- --font-size-3xl: 2rem;
587
- --font-size-4xl: 2.5rem;
588
- --font-size-5xl: 3rem;
589
-
590
- --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
591
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1);
592
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
593
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
594
- --shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
595
-
596
- --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
597
- --transition-fast: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
598
- }}
599
-
600
- /* Base Styles */
601
- html {{
602
- scroll-behavior: smooth;
603
- font-size: 16px;
604
- }}
605
-
606
- body {{
607
- font-family: var(--font-family);
608
- font-size: var(--font-size-base);
609
- line-height: 1.6;
610
- color: var(--text);
611
- background: var(--background);
612
- min-height: 100vh;
613
- }}
614
-
615
- /* Container */
616
- .container {{
617
- width: 100%;
618
- max-width: 1200px;
619
- margin: 0 auto;
620
- padding: 0 var(--spacing-md);
621
- }}
622
-
623
- /* Header */
624
- header {{
625
- position: fixed;
626
- top: 0;
627
- left: 0;
628
- right: 0;
629
- background: var(--surface);
630
- backdrop-filter: blur(10px);
631
- box-shadow: var(--shadow);
632
- z-index: 1000;
633
- transition: var(--transition);
634
- }}
635
-
636
- nav {{
637
- display: flex;
638
- justify-content: space-between;
639
- align-items: center;
640
- padding: var(--spacing-md) 0;
641
- }}
642
-
643
- .logo {{
644
- font-size: var(--font-size-xl);
645
- font-weight: 700;
646
- color: var(--primary);
647
- text-decoration: none;
648
- }}
649
-
650
- .nav-menu {{
651
- display: flex;
652
- list-style: none;
653
- gap: var(--spacing-xl);
654
- }}
655
-
656
- .nav-link {{
657
- color: var(--text);
658
- text-decoration: none;
659
- font-weight: 500;
660
- position: relative;
661
- transition: var(--transition-fast);
662
- }}
663
-
664
- .nav-link:hover {{
665
- color: var(--primary);
666
- }}
667
-
668
- .nav-link::after {{
669
- content: '';
670
- position: absolute;
671
- bottom: -4px;
672
- left: 0;
673
- width: 0;
674
- height: 2px;
675
- background: var(--primary);
676
- transition: width 0.3s ease;
677
- }}
678
-
679
- .nav-link:hover::after {{
680
- width: 100%;
681
- }}
682
-
683
- .menu-toggle {{
684
- display: none;
685
- flex-direction: column;
686
- gap: 4px;
687
- background: none;
688
- border: none;
689
- cursor: pointer;
690
- }}
691
-
692
- .menu-toggle span {{
693
- width: 24px;
694
- height: 2px;
695
- background: var(--text);
696
- transition: var(--transition);
697
- }}
698
-
699
- /* Hero Section */
700
- .hero {{
701
- min-height: 100vh;
702
- display: flex;
703
- align-items: center;
704
- justify-content: center;
705
- background: linear-gradient(135deg, var(--primary), var(--secondary));
706
- color: white;
707
- text-align: center;
708
- padding: var(--spacing-3xl) var(--spacing-md);
709
- }}
710
-
711
- .hero-content {{
712
- max-width: 800px;
713
- }}
714
-
715
- .hero-title {{
716
- font-size: clamp(var(--font-size-3xl), 5vw, var(--font-size-5xl));
717
- font-weight: 800;
718
- margin-bottom: var(--spacing-lg);
719
- animation: fadeInUp 0.8s ease;
720
- }}
721
-
722
- .hero-subtitle {{
723
- font-size: var(--font-size-xl);
724
- margin-bottom: var(--spacing-2xl);
725
- opacity: 0.95;
726
- animation: fadeInUp 0.8s ease 0.2s both;
727
- }}
728
-
729
- .hero-buttons {{
730
- display: flex;
731
- gap: var(--spacing-md);
732
- justify-content: center;
733
- flex-wrap: wrap;
734
- animation: fadeInUp 0.8s ease 0.4s both;
735
- }}
736
-
737
- /* Buttons */
738
- .btn {{
739
- display: inline-flex;
740
- align-items: center;
741
- padding: var(--spacing-md) var(--spacing-xl);
742
- font-size: var(--font-size-base);
743
- font-weight: 600;
744
- text-decoration: none;
745
- border-radius: 8px;
746
- transition: var(--transition);
747
- cursor: pointer;
748
- border: none;
749
- }}
750
-
751
- .btn-primary {{
752
- background: white;
753
- color: var(--primary);
754
- }}
755
-
756
- .btn-primary:hover {{
757
- transform: translateY(-2px);
758
- box-shadow: var(--shadow-lg);
759
- }}
760
-
761
- .btn-secondary {{
762
- background: transparent;
763
- color: white;
764
- border: 2px solid white;
765
- }}
766
-
767
- .btn-secondary:hover {{
768
- background: white;
769
- color: var(--primary);
770
- }}
771
-
772
- /* Features Section */
773
- .features {{
774
- padding: var(--spacing-3xl) 0;
775
- background: var(--surface);
776
- }}
777
-
778
- .section-header {{
779
- text-align: center;
780
- margin-bottom: var(--spacing-3xl);
781
- }}
782
-
783
- .section-title {{
784
- font-size: var(--font-size-4xl);
785
- font-weight: 700;
786
- color: var(--text);
787
- margin-bottom: var(--spacing-md);
788
- }}
789
-
790
- .section-subtitle {{
791
- font-size: var(--font-size-lg);
792
- color: var(--text-muted);
793
- max-width: 600px;
794
- margin: 0 auto;
795
- }}
796
-
797
- .features-grid {{
798
- display: grid;
799
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
800
- gap: var(--spacing-xl);
801
- }}
802
-
803
- .feature-card {{
804
- background: var(--background);
805
- padding: var(--spacing-2xl);
806
- border-radius: 12px;
807
- box-shadow: var(--shadow);
808
- transition: var(--transition);
809
- }}
810
-
811
- .feature-card:hover {{
812
- transform: translateY(-4px);
813
- box-shadow: var(--shadow-xl);
814
- }}
815
-
816
- .feature-icon {{
817
- width: 48px;
818
- height: 48px;
819
- background: linear-gradient(135deg, var(--primary), var(--secondary));
820
- border-radius: 8px;
821
- margin-bottom: var(--spacing-lg);
822
- }}
823
-
824
- .feature-title {{
825
- font-size: var(--font-size-xl);
826
- font-weight: 600;
827
- margin-bottom: var(--spacing-md);
828
- }}
829
-
830
- .feature-description {{
831
- color: var(--text-muted);
832
- line-height: 1.7;
833
- }}
834
-
835
- /* Gallery Section */
836
- .gallery {{
837
- padding: var(--spacing-3xl) 0;
838
- background: var(--background);
839
- }}
840
-
841
- .gallery-grid {{
842
- display: grid;
843
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
844
- gap: var(--spacing-lg);
845
- }}
846
-
847
- .image-card {{
848
- position: relative;
849
- overflow: hidden;
850
- border-radius: 12px;
851
- box-shadow: var(--shadow-md);
852
- transition: var(--transition);
853
- cursor: pointer;
854
- height: 250px;
855
- }}
856
-
857
- .image-card:hover {{
858
- transform: scale(1.02);
859
- box-shadow: var(--shadow-xl);
860
- }}
861
-
862
- .image-card img {{
863
- width: 100%;
864
- height: 100%;
865
- object-fit: cover;
866
- transition: var(--transition);
867
- }}
868
-
869
- .image-card:hover img {{
870
- transform: scale(1.1);
871
- }}
872
-
873
- .image-overlay {{
874
- position: absolute;
875
- bottom: 0;
876
- left: 0;
877
- right: 0;
878
- background: linear-gradient(to top, rgba(0,0,0,0.8), transparent);
879
- color: white;
880
- padding: var(--spacing-lg);
881
- transform: translateY(100%);
882
- transition: var(--transition);
883
- }}
884
-
885
- .image-card:hover .image-overlay {{
886
- transform: translateY(0);
887
- }}
888
-
889
- .image-author {{
890
- font-size: var(--font-size-sm);
891
- }}
892
-
893
- .image-author a {{
894
- color: white;
895
- text-decoration: underline;
896
- }}
897
-
898
- /* CTA Section */
899
- .cta {{
900
- padding: var(--spacing-3xl) 0;
901
- background: linear-gradient(135deg, var(--primary), var(--secondary));
902
- color: white;
903
- text-align: center;
904
- }}
905
-
906
- .cta-title {{
907
- font-size: var(--font-size-3xl);
908
- margin-bottom: var(--spacing-lg);
909
- }}
910
-
911
- .cta-description {{
912
- font-size: var(--font-size-lg);
913
- margin-bottom: var(--spacing-2xl);
914
- opacity: 0.95;
915
- }}
916
-
917
- /* Footer */
918
- footer {{
919
- background: var(--text);
920
- color: white;
921
- padding: var(--spacing-3xl) 0 var(--spacing-xl);
922
- }}
923
-
924
- .footer-content {{
925
- display: grid;
926
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
927
- gap: var(--spacing-2xl);
928
- margin-bottom: var(--spacing-2xl);
929
- }}
930
-
931
- .footer-section h3 {{
932
- margin-bottom: var(--spacing-md);
933
- }}
934
-
935
- .footer-links {{
936
- list-style: none;
937
- }}
938
-
939
- .footer-links li {{
940
- margin-bottom: var(--spacing-sm);
941
- }}
942
-
943
- .footer-links a {{
944
- color: rgba(255, 255, 255, 0.8);
945
- text-decoration: none;
946
- transition: var(--transition-fast);
947
- }}
948
-
949
- .footer-links a:hover {{
950
- color: white;
951
- }}
952
-
953
- .footer-bottom {{
954
- text-align: center;
955
- padding-top: var(--spacing-xl);
956
- border-top: 1px solid rgba(255, 255, 255, 0.1);
957
- color: rgba(255, 255, 255, 0.8);
958
- }}
959
-
960
- /* Animations */
961
- @keyframes fadeInUp {{
962
- from {{
963
- opacity: 0;
964
- transform: translateY(30px);
965
- }}
966
- to {{
967
- opacity: 1;
968
- transform: translateY(0);
969
- }}
970
- }}
971
-
972
- .animate-on-scroll {{
973
- opacity: 0;
974
- transform: translateY(30px);
975
- transition: all 0.6s ease;
976
- }}
977
-
978
- .animate-on-scroll.visible {{
979
- opacity: 1;
980
- transform: translateY(0);
981
- }}
982
-
983
- /* Responsive Design */
984
- @media (max-width: 768px) {{
985
- .nav-menu {{
986
- position: fixed;
987
- top: 60px;
988
- left: -100%;
989
- width: 100%;
990
- height: calc(100vh - 60px);
991
- background: var(--surface);
992
- flex-direction: column;
993
- align-items: center;
994
- padding-top: var(--spacing-3xl);
995
- transition: left 0.3s ease;
996
- }}
997
-
998
- .nav-menu.active {{
999
- left: 0;
1000
- }}
1001
-
1002
- .menu-toggle {{
1003
- display: flex;
1004
- }}
1005
-
1006
- .hero-buttons {{
1007
- flex-direction: column;
1008
- align-items: center;
1009
- }}
1010
-
1011
- .features-grid,
1012
- .gallery-grid {{
1013
- grid-template-columns: 1fr;
1014
- }}
1015
- }}
1016
- </style>
1017
- </head>
1018
- <body>
1019
- <header>
1020
- <nav class="container">
1021
- <a href="#" class="logo">MCP Nebius</a>
1022
- <ul class="nav-menu" id="navMenu">
1023
- <li><a href="#home" class="nav-link">Home</a></li>
1024
- <li><a href="#features" class="nav-link">Features</a></li>
1025
- {f'<li><a href="#gallery" class="nav-link">Gallery</a></li>' if unsplash_images else ''}
1026
- <li><a href="#about" class="nav-link">About</a></li>
1027
- <li><a href="#contact" class="nav-link">Contact</a></li>
1028
- </ul>
1029
- <button class="menu-toggle" id="menuToggle">
1030
- <span></span>
1031
- <span></span>
1032
- <span></span>
1033
- </button>
1034
- </nav>
1035
- </header>
1036
-
1037
- <section class="hero" id="home">
1038
- <div class="hero-content">
1039
- <h1 class="hero-title">AI-Powered Web Generation</h1>
1040
- <p class="hero-subtitle">Transform screenshots into functional websites using advanced AI models via MCP protocol</p>
1041
- <div class="hero-buttons">
1042
- <a href="#features" class="btn btn-primary">Explore Features</a>
1043
- <a href="#contact" class="btn btn-secondary">Get Started</a>
1044
- </div>
1045
- </div>
1046
- </section>
1047
-
1048
- <section class="features" id="features">
1049
- <div class="container">
1050
- <div class="section-header">
1051
- <h2 class="section-title animate-on-scroll">Core Features</h2>
1052
- <p class="section-subtitle animate-on-scroll">Advanced capabilities powered by MCP and Nebius AI</p>
1053
- </div>
1054
- <div class="features-grid">
1055
- <div class="feature-card animate-on-scroll">
1056
- <div class="feature-icon"></div>
1057
- <h3 class="feature-title">Vision Analysis</h3>
1058
- <p class="feature-description">Advanced image understanding with Florence-2 for accurate website analysis</p>
1059
- </div>
1060
- <div class="feature-card animate-on-scroll">
1061
- <div class="feature-icon"></div>
1062
- <h3 class="feature-title">Code Generation</h3>
1063
- <p class="feature-description">Professional HTML/CSS/JS generation using Phind-CodeLlama model</p>
1064
- </div>
1065
- <div class="feature-card animate-on-scroll">
1066
- <div class="feature-icon"></div>
1067
- <h3 class="feature-title">MCP Protocol</h3>
1068
- <p class="feature-description">Seamless integration with Model Context Protocol for enhanced capabilities</p>
1069
- </div>
1070
- <div class="feature-card animate-on-scroll">
1071
- <div class="feature-icon"></div>
1072
- <h3 class="feature-title">Unsplash Integration</h3>
1073
- <p class="feature-description">Access to millions of free, high-quality stock photos</p>
1074
- </div>
1075
- <div class="feature-card animate-on-scroll">
1076
- <div class="feature-icon"></div>
1077
- <h3 class="feature-title">Design System</h3>
1078
- <p class="feature-description">Automatic color extraction and layout analysis from screenshots</p>
1079
- </div>
1080
- <div class="feature-card animate-on-scroll">
1081
- <div class="feature-icon"></div>
1082
- <h3 class="feature-title">CodeSandbox Deploy</h3>
1083
- <p class="feature-description">One-click deployment to live sandbox environment</p>
1084
- </div>
1085
- </div>
1086
- </div>
1087
- </section>
1088
-
1089
- {image_section}
1090
-
1091
- <section class="cta">
1092
- <div class="container">
1093
- <h2 class="cta-title animate-on-scroll">Ready to Transform Your Screenshots?</h2>
1094
- <p class="cta-description animate-on-scroll">Start generating professional websites from images today</p>
1095
- <a href="#" class="btn btn-primary animate-on-scroll">Get Started Now</a>
1096
- </div>
1097
- </section>
1098
-
1099
- <footer>
1100
- <div class="container">
1101
- <div class="footer-content">
1102
- <div class="footer-section">
1103
- <h3>MCP Nebius Generator</h3>
1104
- <p>Advanced AI-powered website generation using Model Context Protocol</p>
1105
- </div>
1106
- <div class="footer-section">
1107
- <h3>Tools</h3>
1108
- <ul class="footer-links">
1109
- <li><a href="#">Image Analyzer</a></li>
1110
- <li><a href="#">Code Generator</a></li>
1111
- <li><a href="#">CodeSandbox Deploy</a></li>
1112
- <li><a href="#">API Documentation</a></li>
1113
- </ul>
1114
- </div>
1115
- <div class="footer-section">
1116
- <h3>Resources</h3>
1117
- <ul class="footer-links">
1118
- <li><a href="#">Documentation</a></li>
1119
- <li><a href="#">Tutorials</a></li>
1120
- <li><a href="#">Examples</a></li>
1121
- <li><a href="#">Support</a></li>
1122
- </ul>
1123
- </div>
1124
- <div class="footer-section">
1125
- <h3>Connect</h3>
1126
- <ul class="footer-links">
1127
- <li><a href="#">GitHub</a></li>
1128
- <li><a href="#">Twitter</a></li>
1129
- <li><a href="#">Discord</a></li>
1130
- <li><a href="#">Blog</a></li>
1131
- </ul>
1132
- </div>
1133
- </div>
1134
- <div class="footer-bottom">
1135
- <p>2025 MCP Website Generator by samsnata. Powered by Nebius AI.</p>
1136
- </div>
1137
- </div>
1138
- </footer>
1139
-
1140
- <script>
1141
- // Mobile menu toggle
1142
- const menuToggle = document.getElementById('menuToggle');
1143
- const navMenu = document.getElementById('navMenu');
1144
-
1145
- menuToggle.addEventListener('click', () => {{
1146
- navMenu.classList.toggle('active');
1147
- }});
1148
-
1149
- // Close menu on link click
1150
- document.querySelectorAll('.nav-link').forEach(link => {{
1151
- link.addEventListener('click', () => {{
1152
- navMenu.classList.remove('active');
1153
- }});
1154
- }});
1155
-
1156
- // Smooth scrolling
1157
- document.querySelectorAll('a[href^="#"]').forEach(anchor => {{
1158
- anchor.addEventListener('click', function (e) {{
1159
- e.preventDefault();
1160
- const target = document.querySelector(this.getAttribute('href'));
1161
- if (target) {{
1162
- target.scrollIntoView({{
1163
- behavior: 'smooth',
1164
- block: 'start'
1165
- }});
1166
- }}
1167
- }});
1168
- }});
1169
-
1170
- // Intersection Observer for animations
1171
- const observerOptions = {{
1172
- threshold: 0.1,
1173
- rootMargin: '0px 0px -50px 0px'
1174
- }};
1175
-
1176
- const observer = new IntersectionObserver((entries) => {{
1177
- entries.forEach(entry => {{
1178
- if (entry.isIntersecting) {{
1179
- entry.target.classList.add('visible');
1180
- }}
1181
- }});
1182
- }}, observerOptions);
1183
-
1184
- document.querySelectorAll('.animate-on-scroll').forEach(el => {{
1185
- observer.observe(el);
1186
- }});
1187
-
1188
- // Header scroll effect
1189
- const header = document.querySelector('header');
1190
- window.addEventListener('scroll', () => {{
1191
- if (window.scrollY > 100) {{
1192
- header.style.background = 'rgba(255, 255, 255, 0.95)';
1193
- header.style.boxShadow = '0 4px 6px -1px rgb(0 0 0 / 0.1)';
1194
- }} else {{
1195
- header.style.background = 'var(--surface)';
1196
- header.style.boxShadow = 'var(--shadow)';
1197
- }}
1198
- }});
1199
- </script>
1200
- </body>
1201
- </html>"""
1202
-
1203
- async def create_codesandbox(self, html_code: str) -> str:
1204
- """MCP Tool: Deploy HTML to CodeSandbox"""
1205
- try:
1206
- import urllib.parse
1207
-
1208
- files = {
1209
- "index.html": {
1210
- "content": html_code,
1211
- "isBinary": False
1212
  }
1213
- }
1214
-
1215
- parameters = {
1216
- "files": files,
1217
- "template": "static"
1218
- }
1219
-
1220
- json_str = json.dumps(parameters)
1221
- encoded = urllib.parse.quote(json_str)
1222
-
1223
- return f"https://codesandbox.io/api/v1/sandboxes/define?parameters={encoded}"
1224
-
1225
- except Exception as e:
1226
- return f"Error creating CodeSandbox URL: {str(e)}"
1227
-
1228
- async def screenshot_to_code(self, image: Image.Image, include_unsplash: bool = False, image_query: str = "", additional_prompt: str = "") -> Tuple[str, str, str]:
1229
- """MCP Tool: Complete pipeline from screenshot to code"""
1230
- try:
1231
- # Step 1: Analyze image
1232
- analysis = await self.analyze_image(image, additional_prompt)
1233
-
1234
- # Step 2: Generate HTML code
1235
- html_code = await self.generate_html_code(analysis, include_unsplash, image_query)
1236
-
1237
- # Step 3: Create CodeSandbox URL
1238
- sandbox_url = await self.create_codesandbox(html_code)
1239
-
1240
- return analysis.get("analysis", ""), html_code, sandbox_url
1241
-
1242
- except Exception as e:
1243
- return f"Error: {str(e)}", "", ""
1244
-
1245
-
1246
- # Gradio Interface
1247
- def create_interface():
1248
- generator = MCPWebsiteGenerator()
1249
-
1250
- with gr.Blocks(
1251
- theme=gr.themes.Base(
1252
- primary_hue=gr.themes.colors.indigo,
1253
- secondary_hue=gr.themes.colors.purple,
1254
- neutral_hue=gr.themes.colors.gray,
1255
- font=gr.themes.GoogleFont("Inter")
1256
- ),
1257
- css="""
1258
- .gradio-container {
1259
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
1260
- }
1261
- .tool-badge {
1262
- background: linear-gradient(135deg, #4f46e5, #6366f1);
1263
- color: white;
1264
- padding: 4px 12px;
1265
- border-radius: 4px;
1266
- font-size: 12px;
1267
- font-weight: 600;
1268
- display: inline-block;
1269
- margin: 2px;
1270
- }
1271
- .feature-item {
1272
- background: #f9fafb;
1273
- border-left: 3px solid #4f46e5;
1274
- padding: 8px 12px;
1275
- margin: 8px 0;
1276
- border-radius: 4px;
1277
- }
1278
- """
1279
- ) as app:
1280
- gr.Markdown("""
1281
- # AI Website Generator (MCP Compatible - Nebius)
1282
- Transform website screenshots into functional HTML code using Nebius AI.
1283
-
1284
- **Features:**
1285
- - Image analysis with Florence-2-large
1286
- - HTML/CSS/JS code generation with Phind-CodeLlama-34B
1287
- - Direct CodeSandbox deployment
1288
- - MCP (Model Context Protocol) compatible
1289
-
1290
- **Tools Available:**
1291
- `analyze_image` `generate_html_code` `create_codesandbox` `screenshot_to_code`
1292
- """)
1293
-
1294
- with gr.Row():
1295
- with gr.Column(scale=1):
1296
- image_input = gr.Image(
1297
- type="pil",
1298
- label="Upload Website Screenshot"
1299
- )
1300
-
1301
- with gr.Accordion("Advanced Options", open=False):
1302
- include_unsplash = gr.Checkbox(
1303
- label="Include Unsplash Images",
1304
- value=False
1305
- )
1306
-
1307
- image_query = gr.Textbox(
1308
- label="Unsplash Search Query",
1309
- placeholder="e.g., modern office, nature, technology",
1310
- visible=False
1311
- )
1312
-
1313
- additional_prompt = gr.Textbox(
1314
- label="Additional Instructions",
1315
- placeholder="e.g., Make it more colorful, add animations, focus on accessibility",
1316
- lines=3
1317
- )
1318
-
1319
- include_unsplash.change(
1320
- lambda x: gr.update(visible=x),
1321
- inputs=[include_unsplash],
1322
- outputs=[image_query]
1323
- )
1324
-
1325
- generate_btn = gr.Button(
1326
- "Generate Website",
1327
- variant="primary",
1328
- size="lg"
1329
- )
1330
-
1331
- gr.Markdown("""
1332
- ### MCP Tools
1333
-
1334
- <div class="feature-item">
1335
- <span class="tool-badge">analyze_image</span>
1336
- Analyzes website screenshots using Florence-2 vision model
1337
- </div>
1338
-
1339
- <div class="feature-item">
1340
- <span class="tool-badge">generate_html_code</span>
1341
- Generates HTML/CSS/JS using Phind-CodeLlama model
1342
- </div>
1343
-
1344
- <div class="feature-item">
1345
- <span class="tool-badge">create_codesandbox</span>
1346
- Deploys generated code to CodeSandbox
1347
- </div>
1348
-
1349
- <div class="feature-item">
1350
- <span class="tool-badge">screenshot_to_code</span>
1351
- Complete pipeline from image to deployed website
1352
- </div>
1353
- """)
1354
-
1355
- with gr.Column(scale=2):
1356
- analysis_output = gr.Textbox(
1357
- label="Analysis Results",
1358
- lines=10,
1359
- max_lines=15
1360
- )
1361
-
1362
- code_output = gr.Code(
1363
- label="Generated HTML/CSS/JS",
1364
- language="html",
1365
- lines=20
1366
- )
1367
-
1368
- with gr.Row():
1369
- sandbox_url = gr.Textbox(
1370
- label="CodeSandbox URL",
1371
- interactive=False
1372
- )
1373
-
1374
- deploy_btn = gr.Button(
1375
- "Open in CodeSandbox",
1376
- variant="secondary"
1377
- )
1378
-
1379
- # Event handlers
1380
- async def process_image(image, include_unsplash, image_query, additional_prompt):
1381
- if image is None:
1382
- return "Please upload an image first.", "", ""
1383
-
1384
- try:
1385
- analysis, code, url = await generator.screenshot_to_code(
1386
- image,
1387
- include_unsplash,
1388
- image_query,
1389
- additional_prompt
1390
- )
1391
- return analysis, code, url
1392
- except Exception as e:
1393
- return f"Error: {str(e)}", "", ""
1394
-
1395
- generate_btn.click(
1396
- process_image,
1397
- inputs=[image_input, include_unsplash, image_query, additional_prompt],
1398
- outputs=[analysis_output, code_output, sandbox_url]
1399
  )
1400
-
1401
- deploy_btn.click(
1402
- lambda url: gr.update(value=f"window.open('{url}', '_blank')") if url else None,
1403
- inputs=[sandbox_url],
1404
- outputs=[]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405
  )
1406
-
1407
- return app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1408
 
1409
- # Launch the app
1410
  if __name__ == "__main__":
1411
- app = create_interface()
1412
- app.launch()
 
1
  import gradio as gr
 
 
 
 
 
 
 
 
2
  import base64
3
  import io
4
+ import json
5
  import os
 
 
 
 
 
 
 
 
6
  import re
7
+ from typing import List, Dict, Tuple, Optional
8
 
9
+ import numpy as np
10
+ from PIL import Image
11
+ from lzstring import LZString
12
+ from openai import OpenAI
13
+
14
+ # Optional deps used at runtime if available
15
+ try:
16
+ import requests
17
+ except Exception:
18
+ requests = None
19
+
20
+ try:
21
+ from sklearn.cluster import KMeans
22
+ _HAS_SKLEARN = True
23
+ except Exception:
24
+ _HAS_SKLEARN = False
25
+
26
+ # ------------------------------------------------------------------------------
27
+ # Configuration
28
+ # ------------------------------------------------------------------------------
29
+
30
+ NEBIUS_BASE_URL = os.getenv("NEBIUS_BASE_URL", "https://api.studio.nebius.com/v1/")
31
+ ENV_NEBIUS_API_KEY = os.getenv("NEBIUS_API_KEY", "")
32
+
33
+ # Optional Unsplash integration
34
+ UNSPLASH_ACCESS_KEY = os.getenv("UNSPLASH_ACCESS_KEY", "")
35
+ UNSPLASH_API_URL = "https://api.unsplash.com/search/photos"
36
+
37
+ # Models (keep identical to your original requirements)
38
+ VISION_MODEL = "Qwen/Qwen2.5-VL-72B-Instruct"
39
+ CODE_MODEL = "deepseek-ai/DeepSeek-V3-0324"
40
+
41
+ # ------------------------------------------------------------------------------
42
+ # Utilities
43
+ # ------------------------------------------------------------------------------
44
+
45
+ def ensure_api_key(user_key: str) -> str:
46
+ key = (user_key or "").strip() or (ENV_NEBIUS_API_KEY or "").strip()
47
+ return key
48
+
49
+ def image_to_base64_png(image: Image.Image) -> str:
50
+ buf = io.BytesIO()
51
+ image.save(buf, format="PNG")
52
+ return base64.b64encode(buf.getvalue()).decode("utf-8")
53
+
54
+ def hex_from_rgb(rgb: Tuple[int, int, int]) -> str:
55
+ r, g, b = [int(max(0, min(255, x))) for x in rgb]
56
+ return f"#{r:02x}{g:02x}{b:02x}"
57
+
58
+ def extract_palette(image: Image.Image, n_colors: int = 8) -> List[str]:
59
+ try:
60
+ img = image.convert("RGB").resize((200, 200))
61
+ arr = np.array(img).reshape(-1, 3)
62
+
63
+ if _HAS_SKLEARN and arr.shape[0] >= n_colors:
64
+ k = KMeans(n_clusters=n_colors, random_state=42, n_init=10)
65
+ labels = k.fit_predict(arr)
66
+ centers = k.cluster_centers_.astype(int)
67
+ counts = np.bincount(labels)
68
+ order = np.argsort(counts)[::-1]
69
+ colors = [tuple(centers[i]) for i in order]
70
+ else:
71
+ uniq, counts = np.unique(arr, axis=0, return_counts=True)
72
+ order = np.argsort(counts)[::-1][:n_colors]
73
+ colors = [tuple(uniq[i]) for i in order]
74
+
75
+ return [hex_from_rgb(c) for c in colors][:n_colors]
76
+ except Exception:
77
+ return ["#4f46e5", "#6366f1", "#111827", "#f9fafb", "#e5e7eb", "#10b981", "#f59e0b", "#ef4444"]
78
+
79
+ def fetch_unsplash_images(query: str, per_page: int = 8) -> List[Dict]:
80
+ if not query or not UNSPLASH_ACCESS_KEY or not requests:
81
+ return []
82
+ try:
83
+ resp = requests.get(
84
+ UNSPLASH_API_URL,
85
+ params={"query": query, "per_page": per_page, "orientation": "landscape"},
86
+ headers={"Authorization": f"Client-ID {UNSPLASH_ACCESS_KEY}"},
87
+ timeout=15,
88
+ )
89
+ if resp.status_code != 200:
90
  return []
91
+ data = resp.json()
92
+ images = []
93
+ for r in data.get("results", []):
94
+ images.append(
95
+ {
96
+ "url": r["urls"]["regular"],
97
+ "alt": r.get("alt_description") or "Unsplash Image",
98
+ "author": r["user"]["name"],
99
+ "author_url": r["user"]["links"]["html"],
100
+ }
 
 
 
101
  )
102
+ return images
103
+ except Exception:
104
+ return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
 
106
+ def inject_gallery_into_html(html_code: str, images: List[Dict]) -> str:
107
+ if not images:
108
+ return html_code
109
 
110
+ gallery_cards = "\n".join(
111
+ [
112
+ f"""<div class="image-card">
113
+ <img src="{img['url']}" alt="{img['alt']}" loading="lazy"/>
114
+ <div class="image-credit">Photo by <a href="{img['author_url']}" target="_blank" rel="noopener noreferrer">{img['author']}</a> on Unsplash</div>
115
+ </div>"""
116
+ for img in images
117
+ ]
118
+ )
119
+
120
+ gallery_section = f"""
121
+ <section id="gallery" class="gallery-section" style="padding: 3rem 0; background: #f9fafb;">
122
+ <div class="container" style="max-width: 1100px; margin: 0 auto; padding: 0 1rem;">
123
+ <h2 style="font: 700 2rem/1.2 Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin-bottom: 1rem; color: #111827;">
124
+ Gallery
125
+ </h2>
126
+ <p style="color:#6b7280; margin-bottom: 1.5rem;">Images provided by Unsplash</p>
127
+ <div class="gallery-grid" style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
128
+ {gallery_cards}
129
+ </div>
130
+ </div>
131
+ </section>
132
+ """
133
+
134
+ # Try to inject before footer or before closing body
135
+ if "</footer>" in html_code:
136
+ return html_code.replace("</footer>", f"{gallery_section}\n</footer>")
137
+ if "</body>" in html_code:
138
+ return html_code.replace("</body>", f"{gallery_section}\n</body>")
139
+ return html_code + gallery_section
140
+
141
+ def build_iframe_preview_html(html_code: str) -> str:
142
+ # Use srcdoc for safer live preview
143
+ # Escape only closing </script> to avoid breaking out if present
144
+ srcdoc = html_code.replace("</script>", "<\\/script>")
145
+ iframe = f"""
146
+ <div style="border:1px solid #e5e7eb; border-radius:8px; overflow:hidden;">
147
+ <iframe
148
+ sandbox="allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"
149
+ style="width:100%; height:600px; border:0;"
150
+ srcdoc='{srcdoc}'
151
+ ></iframe>
152
+ </div>
153
+ """
154
+ return iframe
155
+
156
+ def sanitize_complete_html(html_code: str) -> str:
157
+ if not html_code:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  return html_code
159
+ # Strip backticks code fences if the model wrapped the output
160
+ if html_code.strip().startswith("```"):
161
+ # Try remove first fence
162
+ html_code = re.sub(r"^```[a-zA-Z]*\s*", "", html_code.strip())
163
+ # Remove trailing fence
164
+ html_code = re.sub(r"\s*```$", "", html_code.strip())
165
+ # Ensure full doc
166
+ if "<!DOCTYPE html" in html_code and "</html>" in html_code:
167
+ start = html_code.lower().find("<!doctype")
168
+ end = html_code.lower().rfind("</html>") + len("</html>")
169
+ return html_code[start:end]
170
+ return html_code
171
+
172
+ # ------------------------------------------------------------------------------
173
+ # Core tools (MCP-style)
174
+ # ------------------------------------------------------------------------------
175
+
176
+ def analyze_image(image: Image.Image, nebius_api_key: str = "", extra_instructions: str = "", include_palette: bool = True) -> str:
177
+ if image is None:
178
+ return "Error: No image provided"
179
+
180
+ api_key = ensure_api_key(nebius_api_key)
181
+ if not api_key:
182
+ return "Error: Nebius API key not provided"
183
+
184
+ palette = extract_palette(image) if include_palette else []
185
+ palette_text = ""
186
+ if palette:
187
+ palette_text = "Detected primary color palette: " + ", ".join(palette[:8]) + "\n"
188
+
189
+ try:
190
+ client = OpenAI(base_url=NEBIUS_BASE_URL, api_key=api_key)
191
+
192
+ img_b64 = image_to_base64_png(image)
193
+
194
+ prompt = f"""
195
+ Analyze this web page screenshot and produce a concise but thorough description that will guide HTML/CSS/JS generation.
196
+
197
+ Describe:
198
+ - Overall layout (header, navigation, hero, sections, footer), number of columns, spacing
199
+ - Key components (cards, buttons, forms, tables, icons, media)
200
+ - Visual style (minimal, corporate, playful, dark, etc.), typography hierarchy
201
+ - Color usage and contrasts
202
+
203
+ {palette_text}
204
+ Additional instructions for the analysis, if any:
205
+ {extra_instructions}
206
+ """
207
+
208
+ resp = client.chat.completions.create(
209
+ model=VISION_MODEL,
210
+ messages=[
211
+ {
212
+ "role": "user",
213
+ "content": [
214
+ {"type": "text", "text": prompt.strip()},
215
+ {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{img_b64}" }},
216
+ ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  }
218
+ ],
219
+ max_tokens=1000,
220
+ temperature=0.5,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  )
222
+
223
+ return (resp.choices[0].message.content or "").strip()
224
+
225
+ except Exception as e:
226
+ return f"Error analyzing image: {str(e)}"
227
+
228
+ def generate_html_code(
229
+ description: str,
230
+ nebius_api_key: str = "",
231
+ temperature: float = 0.6,
232
+ max_tokens: int = 7000,
233
+ ) -> str:
234
+ if not description or description.startswith("Error"):
235
+ return "Error: Invalid or missing description"
236
+
237
+ api_key = ensure_api_key(nebius_api_key)
238
+ if not api_key:
239
+ return "Error: Nebius API key not provided"
240
+
241
+ prompt = f"""
242
+ Generate a complete, responsive, production-ready single HTML file based on the analysis below.
243
+
244
+ Analysis:
245
+ {description}
246
+
247
+ Requirements:
248
+ - Use semantic HTML5 and modern, accessible patterns
249
+ - Include an internal <style> block with modern CSS (no external frameworks)
250
+ - Include a <script> block for basic interactivity (mobile menu, smooth scroll, simple form validation)
251
+ - Use CSS custom properties for colors and spacing
252
+ - Mobile-first, responsive layout using Flexbox and CSS Grid
253
+ - Add smooth hover and entrance animations
254
+ - Include Open Graph and meta tags
255
+ - Do not include any external fonts or CDNs
256
+ - Return only the full HTML document starting with <!DOCTYPE html> and ending with </html>
257
+ """
258
+
259
+ try:
260
+ client = OpenAI(base_url=NEBIUS_BASE_URL, api_key=api_key)
261
+
262
+ resp = client.chat.completions.create(
263
+ model=CODE_MODEL,
264
+ messages=[{"role": "user", "content": prompt.strip()}],
265
+ max_tokens=max_tokens,
266
+ temperature=temperature,
267
  )
268
+ code = (resp.choices[0].message.content or "").strip()
269
+ return sanitize_complete_html(code)
270
+ except Exception as e:
271
+ return f"Error generating HTML code: {str(e)}"
272
+
273
+ def create_codesandbox(html_code: str) -> str:
274
+ if not html_code or html_code.startswith("Error"):
275
+ return "Error: No valid HTML code provided"
276
+
277
+ try:
278
+ files = {
279
+ "index.html": {"content": html_code, "isBinary": False},
280
+ "package.json": {
281
+ "content": json.dumps(
282
+ {
283
+ "name": "ai-generated-website",
284
+ "version": "1.0.0",
285
+ "description": "Website generated from image analysis",
286
+ "main": "index.html",
287
+ "scripts": {"start": "serve .", "build": "echo 'no build'"},
288
+ "devDependencies": {"serve": "^14.0.0"},
289
+ },
290
+ indent=2,
291
+ ),
292
+ "isBinary": False,
293
+ },
294
+ }
295
+
296
+ params = {"files": files, "template": "static"}
297
+
298
+ # Prefer compressed GET URL for Spaces without outbound POST
299
+ json_str = json.dumps(params, separators=(",", ":"))
300
+ compressed = LZString().compressToBase64(json_str)
301
+ compressed = compressed.replace("+", "-").replace("/", "_").rstrip("=")
302
+ url_get = f"https://codesandbox.io/api/v1/sandboxes/define?parameters={compressed}"
303
+
304
+ if not requests:
305
+ return url_get
306
+
307
+ try:
308
+ r = requests.post(
309
+ "https://codesandbox.io/api/v1/sandboxes/define",
310
+ json=params,
311
+ timeout=10,
312
+ )
313
+ if r.status_code == 200:
314
+ data = r.json()
315
+ sid = data.get("sandbox_id")
316
+ if sid:
317
+ return f"https://codesandbox.io/s/{sid}"
318
+ except Exception:
319
+ pass
320
+
321
+ return url_get
322
+ except Exception as e:
323
+ return f"Error creating CodeSandbox: {str(e)}"
324
+
325
+ def screenshot_to_code(
326
+ image: Image.Image,
327
+ nebius_api_key: str = "",
328
+ extra_instructions: str = "",
329
+ include_palette: bool = True,
330
+ include_unsplash: bool = False,
331
+ unsplash_query: str = "",
332
+ temperature: float = 0.6,
333
+ max_tokens: int = 7000,
334
+ ) -> Tuple[str, str]:
335
+ description = analyze_image(image, nebius_api_key, extra_instructions, include_palette)
336
+ if not description or description.startswith("Error"):
337
+ return description, "Error: Cannot generate code due to image analysis failure"
338
+
339
+ html_code = generate_html_code(description, nebius_api_key, temperature, max_tokens)
340
+ if include_unsplash and not html_code.startswith("Error"):
341
+ images = fetch_unsplash_images(unsplash_query or "website")
342
+ if images:
343
+ html_code = inject_gallery_into_html(html_code, images)
344
+
345
+ return description, html_code
346
+
347
+ # ------------------------------------------------------------------------------
348
+ # Gradio UI (redesigned, no emojis)
349
+ # ------------------------------------------------------------------------------
350
+
351
+ CUSTOM_CSS = """
352
+ :root {
353
+ --brand: #1f4fff;
354
+ --brand-2: #7c3aed;
355
+ --ink: #0f172a;
356
+ --muted: #64748b;
357
+ --bg: #f8fafc;
358
+ --card: #ffffff;
359
+ --border: #e2e8f0;
360
+ }
361
+ .gradio-container {
362
+ font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
363
+ }
364
+ .header {
365
+ padding: 24px 16px 8px 16px;
366
+ border-bottom: 1px solid var(--border);
367
+ background: linear-gradient(180deg, #fff, #fff0);
368
+ }
369
+ .header h1 {
370
+ margin: 0;
371
+ font-weight: 800;
372
+ font-size: 28px;
373
+ letter-spacing: -0.02em;
374
+ background: linear-gradient(135deg, var(--brand), var(--brand-2));
375
+ -webkit-background-clip: text;
376
+ -webkit-text-fill-color: transparent;
377
+ }
378
+ .header p {
379
+ margin: 6px 0 0 0;
380
+ color: var(--muted);
381
+ }
382
+ .card {
383
+ background: var(--card);
384
+ border: 1px solid var(--border);
385
+ border-radius: 12px;
386
+ padding: 16px;
387
+ }
388
+ .section-title {
389
+ font-weight: 700;
390
+ margin-bottom: 8px;
391
+ color: var(--ink);
392
+ }
393
+ .tool-badge {
394
+ display: inline-block;
395
+ background: linear-gradient(135deg, var(--brand), var(--brand-2));
396
+ color: white;
397
+ border-radius: 6px;
398
+ padding: 2px 8px;
399
+ font-size: 12px;
400
+ margin-right: 6px;
401
+ }
402
+ .footer-note {
403
+ margin-top: 24px;
404
+ color: var(--muted);
405
+ font-size: 13px;
406
+ }
407
+ """
408
+
409
+ with gr.Blocks(theme=gr.themes.Base(), title="AI Website Generator - Nebius MCP", css=CUSTOM_CSS) as app:
410
+ gr.HTML("""
411
+ <div class="header">
412
+ <h1>AI Website Generator (MCP Compatible - Nebius)</h1>
413
+ <p>Transform website screenshots into functional HTML using Qwen2.5-VL analysis and DeepSeek-V3 generation.</p>
414
+ </div>
415
+ """)
416
+
417
+ with gr.Tabs():
418
+ with gr.Tab("Generate"):
419
+ with gr.Row():
420
+ with gr.Column(scale=1):
421
+ gr.Markdown("#### Inputs", elem_classes=["section-title"])
422
+ with gr.Group(elem_classes=["card"]):
423
+ nebius_key = gr.Textbox(
424
+ label="Nebius API Key",
425
+ type="password",
426
+ placeholder="Enter your Nebius API key or set NEBIUS_API_KEY in environment",
427
+ value="" if ENV_NEBIUS_API_KEY else "",
428
+ )
429
+ extra_instructions = gr.Textbox(
430
+ label="Additional Instructions (optional)",
431
+ placeholder="e.g., prefer minimal style, rounded corners, larger headings, add subtle animations",
432
+ lines=3,
433
+ )
434
+ with gr.Row():
435
+ include_palette = gr.Checkbox(value=True, label="Include detected color palette in analysis")
436
+ include_unsplash = gr.Checkbox(value=False, label="Add Unsplash gallery to output")
437
+ unsplash_query = gr.Textbox(
438
+ label="Unsplash search query",
439
+ placeholder="e.g., office, nature, technology",
440
+ visible=False,
441
+ )
442
+
443
+ def _toggle_unsplash(show):
444
+ return gr.update(visible=bool(show))
445
+
446
+ include_unsplash.change(_toggle_unsplash, inputs=[include_unsplash], outputs=[unsplash_query])
447
+
448
+ with gr.Accordion("Generation Parameters", open=False):
449
+ temperature = gr.Slider(0.0, 1.2, value=0.6, step=0.05, label="Temperature")
450
+ max_tokens = gr.Slider(1000, 8000, value=7000, step=100, label="Max tokens")
451
+ with gr.Group(elem_classes=["card"]):
452
+ image_input = gr.Image(type="pil", label="Upload website screenshot", sources=["upload", "clipboard"])
453
+ generate_btn = gr.Button("Generate", variant="primary")
454
+
455
+ with gr.Column(scale=2):
456
+ gr.Markdown("#### Results", elem_classes=["section-title"])
457
+ with gr.Group(elem_classes=["card"]):
458
+ description_output = gr.Textbox(label="Image Analysis", lines=10, interactive=False)
459
+ with gr.Group(elem_classes=["card"]):
460
+ html_output = gr.Code(label="Generated HTML", language="html", lines=20)
461
+ with gr.Row():
462
+ copy_btn = gr.Button("Copy Code")
463
+ download_btn = gr.Button("Download HTML")
464
+ with gr.Group(elem_classes=["card"]):
465
+ gr.Markdown("Live Preview (sandboxed)")
466
+ preview_html = gr.HTML()
467
+
468
+ def on_generate(image, key, inst, inc_pal, inc_uns, u_query, temp, max_tok):
469
+ if image is None:
470
+ return "Please upload an image first.", "", "", ""
471
+ desc, html_code = screenshot_to_code(
472
+ image=image,
473
+ nebius_api_key=key,
474
+ extra_instructions=inst,
475
+ include_palette=inc_pal,
476
+ include_unsplash=inc_uns,
477
+ unsplash_query=u_query,
478
+ temperature=float(temp),
479
+ max_tokens=int(max_tok),
480
+ )
481
+ preview = build_iframe_preview_html(html_code) if html_code and not html_code.startswith("Error") else ""
482
+ return desc, html_code, preview, "Code copied to clipboard is only possible in the browser. Select the code block and press Ctrl+C or Cmd+C."
483
+
484
+ generate_btn.click(
485
+ on_generate,
486
+ inputs=[image_input, nebius_key, extra_instructions, include_palette, include_unsplash, unsplash_query, temperature, max_tokens],
487
+ outputs=[description_output, html_output, preview_html, gr.Textbox(visible=False)],
488
+ )
489
+
490
+ def on_copy(_code):
491
+ # Gradio cannot copy to clipboard directly; provide a helpful message.
492
+ return gr.update(value="Select the code and copy with Ctrl+C or Cmd+C")
493
+
494
+ copy_btn.click(on_copy, inputs=[html_output], outputs=[html_output])
495
+
496
+ def on_download(code: str):
497
+ if not code or code.startswith("Error"):
498
+ return None
499
+ import tempfile, time, pathlib
500
+ tmpdir = tempfile.mkdtemp(prefix="site_")
501
+ path = pathlib.Path(tmpdir) / "index.html"
502
+ path.write_text(code, encoding="utf-8")
503
+ return str(path)
504
+
505
+ download_file = gr.File(label="Download", visible=False)
506
+ download_btn.click(on_download, inputs=[html_output], outputs=[download_file])
507
+
508
+ with gr.Tab("Preview & Deploy"):
509
+ with gr.Row():
510
+ with gr.Column(scale=2):
511
+ gr.Markdown("#### Live Preview", elem_classes=["section-title"])
512
+ preview_html_2 = gr.HTML(elem_classes=["card"])
513
+ with gr.Column(scale=1):
514
+ gr.Markdown("#### Deployment", elem_classes=["section-title"])
515
+ with gr.Group(elem_classes=["card"]):
516
+ deploy_btn = gr.Button("Create CodeSandbox")
517
+ sandbox_url = gr.Textbox(label="CodeSandbox URL", interactive=False)
518
+ open_link_html = gr.HTML()
519
+
520
+ def sync_preview(code: str):
521
+ return build_iframe_preview_html(code) if code and not code.startswith("Error") else ""
522
+
523
+ html_output.change(sync_preview, inputs=[html_output], outputs=[preview_html_2])
524
+
525
+ def on_deploy(code: str):
526
+ url = create_codesandbox(code)
527
+ if url.startswith("Error"):
528
+ return url, ""
529
+ link = f'<a href="{url}" target="_blank" rel="noopener noreferrer">Open CodeSandbox</a>'
530
+ return url, link
531
+
532
+ deploy_btn.click(on_deploy, inputs=[html_output], outputs=[sandbox_url, open_link_html])
533
+
534
+ with gr.Tab("Tools"):
535
+ gr.Markdown("#### Available Tools", elem_classes=["section-title"])
536
+ gr.HTML("""
537
+ <div class="card">
538
+ <span class="tool-badge">analyze_image</span>
539
+ <span class="tool-badge">generate_html_code</span>
540
+ <span class="tool-badge">create_codesandbox</span>
541
+ <span class="tool-badge">screenshot_to_code</span>
542
+ <div class="footer-note">Use the panels below to run tools individually.</div>
543
+ </div>
544
+ """)
545
+ with gr.Row():
546
+ with gr.Column():
547
+ gr.Markdown("Image Analysis", elem_classes=["section-title"])
548
+ with gr.Group(elem_classes=["card"]):
549
+ img_tool = gr.Image(type="pil", label="Image")
550
+ key_tool = gr.Textbox(label="Nebius API Key", type="password")
551
+ inst_tool = gr.Textbox(label="Additional Instructions", lines=3)
552
+ include_palette_tool = gr.Checkbox(value=True, label="Include color palette")
553
+ analyze_btn = gr.Button("Run analyze_image")
554
+ analysis_result = gr.Textbox(label="Result", lines=10)
555
+ def run_analyze(img, k, ins, inc_pal):
556
+ return analyze_image(img, k, ins, inc_pal)
557
+ analyze_btn.click(run_analyze, inputs=[img_tool, key_tool, inst_tool, include_palette_tool], outputs=[analysis_result])
558
+
559
+ with gr.Column():
560
+ gr.Markdown("Code Generation", elem_classes=["section-title"])
561
+ with gr.Group(elem_classes=["card"]):
562
+ desc_input = gr.Textbox(label="Description", lines=6)
563
+ key_tool2 = gr.Textbox(label="Nebius API Key", type="password")
564
+ temp_tool = gr.Slider(0.0, 1.2, value=0.6, step=0.05, label="Temperature")
565
+ tokens_tool = gr.Slider(1000, 8000, value=7000, step=100, label="Max tokens")
566
+ code_btn = gr.Button("Run generate_html_code")
567
+ code_result = gr.Code(label="Generated Code", language="html", lines=20)
568
+ def run_generate(desc, k, t, mx):
569
+ return generate_html_code(desc, k, float(t), int(mx))
570
+ code_btn.click(run_generate, inputs=[desc_input, key_tool2, temp_tool, tokens_tool], outputs=[code_result])
571
+
572
+ gr.Markdown("Deployment", elem_classes=["section-title"])
573
+ with gr.Group(elem_classes=["card"]):
574
+ cs_input = gr.Code(label="HTML to deploy", language="html", lines=12)
575
+ cs_btn = gr.Button("Run create_codesandbox")
576
+ cs_url = gr.Textbox(label="CodeSandbox URL", interactive=False)
577
+ cs_open = gr.HTML()
578
+ def run_cs(code):
579
+ url = create_codesandbox(code)
580
+ link = f'<a href="{url}" target="_blank" rel="noopener noreferrer">Open CodeSandbox</a>' if not url.startswith("Error") else ""
581
+ return url, link
582
+ cs_btn.click(run_cs, inputs=[cs_input], outputs=[cs_url, cs_open])
583
+
584
+ with gr.Tab("About"):
585
+ gr.Markdown("""
586
+ ### Overview
587
+ This app converts website screenshots into functional HTML using:
588
+ - Image analysis: Qwen2.5-VL-72B-Instruct
589
+ - Code generation: DeepSeek-V3-0324
590
+ - MCP-compatible tool design
591
+ - Optional Unsplash gallery injection
592
+
593
+ ### Notes
594
+ - Do not hardcode secrets. Set NEBIUS_API_KEY and UNSPLASH_ACCESS_KEY in your environment or Space secrets.
595
+ - Live preview uses a sandboxed iframe via srcdoc for safety.
596
+ """)
597
 
598
+ # Launch with MCP server compatibility if desired
599
  if __name__ == "__main__":
600
+ # Note: mcp_server flag requires compatible runtime; set to True if you use MCP.
601
+ app.launch(mcp_server=True, share=False)