admin08077 commited on
Commit
60a5248
·
verified ·
1 Parent(s): b80b672

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +635 -31
app.py CHANGED
@@ -1,57 +1,661 @@
 
1
  import gradio as gr
 
 
2
  from google import genai
3
  from google.genai import types
 
4
  from PIL import Image
5
  from io import BytesIO
6
- import os
 
 
 
7
 
8
- # Make sure your GEMINI_API_KEY is set in Hugging Face secrets/environment
 
 
9
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
10
  if not GEMINI_API_KEY:
11
- raise ValueError("GEMINI_API_KEY not found in environment variables.")
12
 
13
  # Initialize Gemini client
14
  client = genai.Client(api_key=GEMINI_API_KEY)
15
 
16
- # Gradio function
17
- def generate_image_and_caption(prompt):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try:
 
 
 
19
  response = client.models.generate_content(
20
  model="gemini-2.0-flash-preview-image-generation",
21
- contents=prompt,
22
- config=types.GenerateContentConfig(
23
- response_modalities=['TEXT', 'IMAGE']
24
- )
25
  )
 
 
26
 
27
- caption = "No text response."
28
- img = None
 
 
 
 
 
 
 
 
29
 
30
- for part in response.candidates[0].content.parts:
31
- if part.text:
32
- caption = part.text
33
- elif part.inline_data:
34
- img = Image.open(BytesIO(part.inline_data.data))
35
 
36
- return caption, img
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
38
- except Exception as e:
39
- return f"Error: {str(e)}", None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- # Gradio UI
42
- with gr.Blocks(title="Gemini 2.0 Flash Preview Image Generator") as demo:
43
- gr.Markdown("## 🧠✨ Gemini 2.0 Flash Preview Image Generator")
44
- gr.Markdown("Enter a prompt and get a 3D rendered image + text generated by Google Gemini.")
45
 
46
- with gr.Row():
47
- prompt = gr.Textbox(label="Your Prompt", placeholder="Describe the scene...")
48
- submit = gr.Button("Generate")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- output_text = gr.Textbox(label="Generated Description")
51
- output_image = gr.Image(label="Generated Image")
52
 
53
- submit.click(fn=generate_image_and_caption, inputs=prompt, outputs=[output_text, output_image])
54
 
55
- # Run the app
56
  if __name__ == "__main__":
57
- demo.launch()
 
 
 
1
+
2
  import gradio as gr
3
+ import os
4
+ # Updated import to use the new 'google.genai' SDK
5
  from google import genai
6
  from google.genai import types
7
+ import json
8
  from PIL import Image
9
  from io import BytesIO
10
+ import base64
11
+ import uuid
12
+ import concurrent.futures
13
+ import re
14
 
15
+ # --- Configuration and Initialization ---
16
+
17
+ # Get the API key from environment variables
18
  GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
19
  if not GEMINI_API_KEY:
20
+ raise ValueError("GEMINI_API_KEY not found in environment variables. Please set it in your Hugging Face Space secrets.")
21
 
22
  # Initialize Gemini client
23
  client = genai.Client(api_key=GEMINI_API_KEY)
24
 
25
+
26
+ # --- Default Data Structures (Ported from TypeScript) ---
27
+
28
+ DEFAULT_WEBSITE_SETTINGS = {
29
+ "theme": {
30
+ "appBg": "#111827", "panelBg": "#1f2937", "headerBg": "#1f2937",
31
+ "footerBg": "#1f2937", "primaryTextColor": "#f9fafb", "secondaryTextColor": "#d1d5db",
32
+ "primaryAccent": "#4f46e5", "secondaryAccent": "#10b981", "fontFamily": "font-sans",
33
+ },
34
+ "header": { "title": "My Awesome Site", "logoUrl": "", "navLinks": [] },
35
+ "footer": {
36
+ "text": "© 2024 My Awesome Site. All rights reserved.",
37
+ "links": [
38
+ {"text": "Privacy Policy", "href": "#"},
39
+ {"text": "Terms of Service", "href": "#"},
40
+ ],
41
+ },
42
+ "pages": [{
43
+ "path": "index.html", "name": "Home",
44
+ "contentBlocks": [{
45
+ "id": str(uuid.uuid4()), "type": "hero", "headline": "Welcome to Your AI-Generated Website",
46
+ "subheadline": "Describe your vision and watch it come to life.",
47
+ "bgImage": "", "headlineColor": "#ffffff", "subheadlineColor": "#d1d5db",
48
+ }],
49
+ }],
50
+ }
51
+
52
+ # --- Core AI and Helper Functions ---
53
+
54
+ def create_prompt_from_answers(answers):
55
+ # This prompt is a direct port from the React app, instructing the AI on how to generate the website layout.
56
+ return f"""
57
+ You are a world-class creative director, UI/UX designer, and content strategist tasked with building a complete multi-page website.
58
+ The user has provided the following details through a questionnaire. Your task is to interpret their answers and generate a cohesive and complete website design.
59
+
60
+ **User's Website Requirements:**
61
+ - **Website Name:** {answers.get('name', 'AI Generated Website')}
62
+ - **Core Purpose:** {answers.get('purpose', 'Not specified')}
63
+ - **Target Audience:** {answers.get('audience', 'General audience')}
64
+ - **Desired Style/Vibe:** {answers.get('style', 'A modern, clean design')}
65
+ - **Color Palette:** {answers.get('colors', 'A balanced color scheme based on the style')}
66
+ - **Theme Preference:** {answers.get('theme', 'dark')}
67
+ - **Required Pages:** {answers.get('pages', 'Home, About, Contact')}
68
+ - **Home Page Headline/Message:** {answers.get('homeMessage', f"Welcome to {answers.get('name', 'Our Website')}")}
69
+ - **Key Features/Services:** {answers.get('features', 'Feature 1, Feature 2, Feature 3')}
70
+ - **About Us Content:** {answers.get('about', 'A brief description of the company.')}
71
+ - **Contact Info:** {answers.get('contact', 'A contact form.')}
72
+ - **Brand Tone of Voice:** {answers.get('tone', 'Professional and friendly')}
73
+ - **Tagline:** {answers.get('tagline', '')}
74
+ - **Additional Instructions:** {answers.get('extra', 'None')}
75
+
76
+ **Your Mission:**
77
+ Based *only* on the requirements above, generate a single, comprehensive JSON object for this website. Adhere strictly to the provided JSON schema.
78
+
79
+ **Mandatory Requirements:**
80
+ 1. **Page Creation:** Create pages based on the user's request ({answers.get('pages')}). If not specified, create a logical set of pages (e.g., Home, About, Services, Contact).
81
+ 2. **Cohesive Branding:** The theme (colors, fonts) and all generated content (text, image prompts) must be consistent with the user's described style ({answers.get('style')}, {answers.get('colors')}, {answers.get('theme')}).
82
+ 3. **Content Generation:** Write compelling copy for all text blocks, headlines, and features, adopting the user's desired tone ({answers.get('tone')}). The content should directly relate to the website's purpose ({answers.get('purpose')}) and features ({answers.get('features')}).
83
+ 4. **Creative Asset Prompts:** Generate 5 distinct, creative prompts for logos that match the brand identity. Also, create a unique, descriptive image prompt for every single image required in the content blocks.
84
+ 5. **Navigation:** Populate the 'navLinks' array in the header settings. The links must correspond to the pages you create.
85
+ 6. **Data Integrity:** All paths must be unique and end in '.html'. All block IDs must be unique. Use valid hex color codes with good accessibility contrast. For forms, include relevant fields.
86
+ """
87
+
88
+ def generate_image(prompt: str, aspect_ratio: str):
89
+ """Generates an image using Gemini and returns a base64 data URL."""
90
+ print(f"Generating image for prompt: {prompt}")
91
  try:
92
+ # Refactored to use the new client.models.generate_content method
93
+ full_prompt = f'Generate a single, high-quality image. Description: "{prompt}". The image should have a {aspect_ratio} aspect ratio.'
94
+
95
  response = client.models.generate_content(
96
  model="gemini-2.0-flash-preview-image-generation",
97
+ contents=full_prompt,
98
+ config=types.GenerateContentConfig(response_modalities=['TEXT', 'IMAGE'])
 
 
99
  )
100
+
101
+ image_part = next((part for part in response.candidates[0].content.parts if part.inline_data), None)
102
 
103
+ if image_part:
104
+ mime_type = image_part.inline_data.mime_type
105
+ data = base64.b64encode(image_part.inline_data.data).decode("utf-8")
106
+ return f"data:{mime_type};base64,{data}"
107
+ else:
108
+ print(f"Warning: Image generation failed for prompt: {prompt}. No image data in response.")
109
+ return ""
110
+ except Exception as e:
111
+ print(f"Error generating image for prompt '{prompt}': {e}")
112
+ return ""
113
 
114
+ def generate_page_html(page, settings):
115
+ """Generates the HTML for a single page based on the website settings."""
116
+ if not page or not settings:
117
+ return "<p>Error: Page or settings not found.</p>"
 
118
 
119
+ theme = settings.get("theme", {})
120
+ header = settings.get("header", {})
121
+ footer = settings.get("footer", {})
122
+ nav_links = header.get("navLinks", [])
123
+
124
+ # Header HTML
125
+ header_html = f"""
126
+ <header style="background-color: {theme.get('headerBg')}; color: {theme.get('primaryTextColor')}; padding: 1rem 2rem;">
127
+ <div class="container mx-auto flex justify-between items-center">
128
+ <div class="flex items-center space-x-4">
129
+ {'<img src="' + header.get('logoUrl', '') + '" alt="Logo" class="h-10 w-10 object-contain bg-white rounded-full p-1">' if header.get('logoUrl') else ''}
130
+ <a href="index.html" class="text-2xl font-bold">{header.get('title', '')}</a>
131
+ </div>
132
+ <div class="hidden md:flex items-center space-x-6">
133
+ {''.join([f'<a href="{link.get("href")}" style="color: {theme.get("secondaryTextColor")};" class="hover:text-white transition-colors {"font-bold !text-white" if page.get("path") == link.get("href") else ""}">{link.get("text")}</a>' for link in nav_links])}
134
+ </div>
135
+ </div>
136
+ </header>
137
+ """
138
 
139
+ # Content Blocks HTML
140
+ main_content_html = ""
141
+ for block in page.get("contentBlocks", []):
142
+ block_type = block.get("type")
143
+ if block_type == 'hero':
144
+ main_content_html += f"""
145
+ <section class="text-center flex flex-col items-center justify-center min-h-[50vh] p-8" style="background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)), url({block.get('bgImage', '')}); background-size: cover; background-position: center;">
146
+ <h2 class="text-4xl md:text-6xl font-bold" style="color: {block.get('headlineColor', '#ffffff')};">{block.get('headline', '')}</h2>
147
+ <p class="mt-4 text-lg md:text-xl max-w-2xl" style="color: {block.get('subheadlineColor', '#d1d5db')};">{block.get('subheadline', '')}</p>
148
+ </section>"""
149
+ elif block_type == 'textWithImage':
150
+ text_html = (block.get('text', '') or '').replace('\n', '<br/>')
151
+ main_content_html += f"""
152
+ <section class="container mx-auto py-12 md:py-20 px-6">
153
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
154
+ <div class="{'md:order-1' if block.get('imagePosition') == 'right' else 'md:order-2'}">
155
+ <h3 class="text-3xl font-bold mb-4" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h3>
156
+ <p style="color: {theme.get('secondaryTextColor')};">{text_html}</p>
157
+ </div>
158
+ <div class="{'md:order-2' if block.get('imagePosition') == 'right' else 'md:order-1'}">
159
+ <img src="{block.get('image', '')}" alt="{block.get('headline', 'content image')}" class="rounded-lg shadow-xl w-full h-auto object-cover aspect-square">
160
+ </div>
161
+ </div>
162
+ </section>"""
163
+ elif block_type == 'featureList':
164
+ features_html = ''.join([f"""
165
+ <div class="p-6 rounded-lg" style="background-color: {theme.get('panelBg')};">
166
+ <h4 class="text-xl font-bold mb-2" style="color: {theme.get('primaryAccent')};">{f.get('title', '')}</h4>
167
+ <p style="color: {theme.get('secondaryTextColor')};">{f.get('description', '')}</p>
168
+ </div>""" for f in block.get('features', [])])
169
+ main_content_html += f"""
170
+ <section class="container mx-auto py-12 md:py-20 px-6">
171
+ <h2 class="text-4xl font-bold text-center mb-12" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
172
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">{features_html}</div>
173
+ </section>"""
174
+ elif block_type == 'text':
175
+ paragraphs_html = ''.join([f'<p>{p}</p>' for p in block.get('paragraphs', [])])
176
+ main_content_html += f"""
177
+ <section class="container mx-auto py-12 md:py-20 px-6 max-w-4xl">
178
+ <h2 class="text-4xl font-bold text-center mb-12" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
179
+ <div class="prose prose-invert lg:prose-xl mx-auto" style="color: {theme.get('secondaryTextColor')};">{paragraphs_html}</div>
180
+ </section>
181
+ """
182
+ elif block_type == 'form':
183
+ fields_html = ''.join([f"""
184
+ <div>
185
+ <label for="{f.get('name')}" class="block text-sm font-medium mb-1" style="color: {theme.get('secondaryTextColor')};">{f.get('label', '')}</label>
186
+ <input type="{f.get('type')}" name="{f.get('name')}" id="{f.get('name')}" class="w-full rounded-md p-2 text-sm" style="background-color: {theme.get('appBg')}; color: {theme.get('primaryTextColor')}; border: 1px solid {theme.get('panelBg')};" />
187
+ </div>""" for f in block.get('fields', [])])
188
+ main_content_html += f"""
189
+ <section class="container mx-auto py-12 md:py-20 px-6 flex justify-center">
190
+ <div class="w-full max-w-md p-8 rounded-lg" style="background-color: {theme.get('panelBg')};">
191
+ <h2 class="text-3xl font-bold text-center mb-8" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
192
+ <form class="space-y-6" onsubmit="event.preventDefault(); alert('Form submitted! (This is a preview)');">
193
+ {fields_html}
194
+ <div><button type="submit" class="w-full flex items-center justify-center p-2 text-white font-semibold rounded-md transition-colors" style="background-color: {theme.get('secondaryAccent')};">{block.get('submitButtonText', 'Submit')}</button></div>
195
+ </form>
196
+ </div>
197
+ </section>"""
198
+
199
+ # Footer HTML
200
+ footer_html = f"""
201
+ <footer style="background-color: {theme.get('footerBg')}; color: {theme.get('secondaryTextColor')};" class="py-6 px-4 mt-auto">
202
+ <div class="container mx-auto text-center">
203
+ <p>{footer.get('text', '')}</p>
204
+ <div class="mt-2 flex justify-center space-x-4">
205
+ {''.join([f'<a href="{link.get("href")}" class="hover:text-white transition-colors">{link.get("text")}</a>' for link in footer.get("links", [])])}
206
+ </div>
207
+ </div>
208
+ </footer>
209
+ """
210
+
211
+ # Final Page Assembly
212
+ return f"""
213
+ <!DOCTYPE html>
214
+ <html lang="en" class="{theme.get('fontFamily', 'font-sans')}">
215
+ <head>
216
+ <meta charset="UTF-8">
217
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
218
+ <title>{header.get('title', '')} - {page.get('name', '')}</title>
219
+ <script src="https://cdn.tailwindcss.com"></script>
220
+ <script>
221
+ tailwind.config = {{
222
+ theme: {{
223
+ extend: {{
224
+ typography: ({{ theme }}) => ({{
225
+ invert: {{
226
+ css: {{
227
+ '--tw-prose-body': theme('colors.gray[300]'),
228
+ '--tw-prose-headings': theme('colors.white'),
229
+ '--tw-prose-links': theme('colors.white'),
230
+ '--tw-prose-bold': theme('colors.white'),
231
+ }},
232
+ }},
233
+ }}),
234
+ }},
235
+ }}
236
+ }}
237
+ </script>
238
+ </head>
239
+ <body style="background-color: {theme.get('appBg')};" class="flex flex-col min-h-screen">
240
+ {header_html}
241
+ <main>{main_content_html}</main>
242
+ {footer_html}
243
+ <script>
244
+ // Prevent iframe navigation from redirecting the whole page
245
+ document.addEventListener('click', function (e) {{
246
+ let target = e.target;
247
+ while (target && target.tagName !== 'A') {{
248
+ target = target.parentElement;
249
+ }}
250
+ if (target && target.tagName === 'A') {{
251
+ const href = target.getAttribute('href');
252
+ if (href && !href.startsWith('#') && !href.startsWith('http')) {{
253
+ e.preventDefault();
254
+ // In a real scenario, you'd postMessage to the parent,
255
+ // but Gradio's buttons will handle navigation instead.
256
+ alert(`Preview navigation to: ${href}`);
257
+ }}
258
+ }}
259
+ }});
260
+ </script>
261
+ </body>
262
+ </html>
263
+ """.strip()
264
+
265
+
266
+ # --- Gradio UI Definition ---
267
+
268
+ def create_gradio_app():
269
+
270
+ # --- UI Helper Functions ---
271
+ def get_page_by_path(path, settings):
272
+ return next((p for p in settings.get("pages", []) if p.get("path") == path), None)
273
+
274
+ def get_block_by_id(page, block_id):
275
+ if not page: return None
276
+ return next((b for b in page.get("contentBlocks", []) if b.get("id") == block_id), None)
277
+
278
+ # --- Event Handlers ---
279
+
280
+ def handle_start():
281
+ return {
282
+ welcome_view: gr.update(visible=False),
283
+ questionnaire_view: gr.update(visible=True),
284
+ }
285
+
286
+ def handle_generate_layout(*questionnaire_answers):
287
+ yield {
288
+ questionnaire_view: gr.update(visible=False),
289
+ generating_view: gr.update(visible=True),
290
+ status_text: "Generating website structure..."
291
+ }
292
+
293
+ # Map flat answer list to dictionary
294
+ question_ids = [
295
+ 'name', 'purpose', 'audience', 'style', 'colors', 'theme', 'pages',
296
+ 'homeMessage', 'features', 'about', 'tone', 'tagline', 'extra'
297
+ ]
298
+ answers = dict(zip(question_ids, questionnaire_answers))
299
+
300
+ # --- Step 1: Generate Layout ---
301
+ try:
302
+ # Refactored to use the new client.models.generate_content method
303
+ prompt = create_prompt_from_answers(answers)
304
+ schema = {
305
+ "type": "object",
306
+ "properties": {
307
+ "websiteSettings": {
308
+ "type": "object",
309
+ "properties": {
310
+ "theme": {"type": "object", "properties": {"appBg": {"type": "string"},"panelBg": {"type": "string"},"headerBg": {"type": "string"},"footerBg": {"type": "string"},"primaryTextColor": {"type": "string"},"secondaryTextColor": {"type": "string"},"primaryAccent": {"type": "string"},"secondaryAccent": {"type": "string"},"fontFamily": {"type": "string"}}},
311
+ "header": {"type": "object", "properties": {"title": {"type": "string"},"navLinks": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
312
+ "footer": {"type": "object", "properties": {"text": {"type": "string"},"links": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
313
+ "pages": {"type": "array","items": {"type": "object","properties": {"path": {"type": "string"},"name": {"type": "string"},"contentBlocks": {"type": "array","items": {"type": "object", "properties": { "id": {"type": "string"}, "type": {"type": "string"}, "headline": {"type": "string"}, "subheadline": {"type": "string"}, "headlineColor": {"type": "string"}, "subheadlineColor": {"type": "string"}, "text": {"type": "string"}, "imagePosition": {"type": "string"}, "features": {"type": "array", "items": {"type": "object", "properties": {"title": {"type": "string"}, "description": {"type": "string"}}}},"fields": {"type": "array","items": {"type": "object","properties": {"label": {"type": "string"},"type": {"type": "string"},"name": {"type": "string"}}}},"submitButtonText": {"type": "string"}, "paragraphs": {"type": "array", "items": {"type": "string"}}}}}}}},
314
+ },
315
+ },
316
+ "imagePrompts": {
317
+ "type": "object",
318
+ "properties": {
319
+ "logoPrompts": {"type": "array", "items": {"type": "string"}},
320
+ "blockImagePrompts": {"type": "array", "items": {"type": "object", "properties": {"pagePath": {"type": "string"},"blockId": {"type": "string"},"prompt": {"type": "string"}}}}
321
+ }
322
+ }
323
+ }
324
+ }
325
+
326
+ response = client.models.generate_content(
327
+ model='gemini-2.5-flash',
328
+ contents=prompt,
329
+ config=types.GenerateContentConfig(
330
+ response_mime_type="application/json",
331
+ response_schema=schema,
332
+ safety_settings={
333
+ types.HarmCategory.HARM_CATEGORY_HARASSMENT: types.HarmBlockThreshold.BLOCK_NONE,
334
+ types.HarmCategory.HARM_CATEGORY_HATE_SPEECH: types.HarmBlockThreshold.BLOCK_NONE,
335
+ types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: types.HarmBlockThreshold.BLOCK_NONE,
336
+ types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: types.HarmBlockThreshold.BLOCK_NONE,
337
+ }
338
+ )
339
+ )
340
+ design_plan = json.loads(response.text)
341
+ except Exception as e:
342
+ print(f"Layout generation failed: {e}")
343
+ yield {
344
+ generating_view: gr.update(visible=False),
345
+ questionnaire_view: gr.update(visible=True),
346
+ error_box: gr.update(value=f"Error generating layout: {e}", visible=True)
347
+ }
348
+ return
349
+
350
+ new_settings = design_plan['websiteSettings']
351
+ image_prompts = design_plan['imagePrompts']
352
+
353
+ # Add unique IDs and empty image fields to the settings
354
+ original_id_map = {}
355
+ for page in new_settings.get('pages', []):
356
+ new_blocks = []
357
+ for block in page.get('contentBlocks', []):
358
+ new_id = str(uuid.uuid4())
359
+ original_id = block.get('id')
360
+ if original_id:
361
+ original_id_map[original_id] = new_id
362
+
363
+ block['id'] = new_id
364
+ if block.get('type') == 'hero':
365
+ block['bgImage'] = ''
366
+ if block.get('type') == 'textWithImage':
367
+ block['image'] = ''
368
+ new_blocks.append(block)
369
+ page['contentBlocks'] = new_blocks
370
+ new_settings['header']['logoUrl'] = ''
371
+
372
+ active_page_path = new_settings['pages'][0]['path'] if new_settings.get('pages') else ''
373
+
374
+ # --- Step 2: Generate Images in Parallel ---
375
+ tasks = []
376
+ logo_prompts = image_prompts.get('logoPrompts', [])
377
+ block_prompts = image_prompts.get('blockImagePrompts', [])
378
+
379
+ if logo_prompts:
380
+ yield {status_text: f"Generating {len(logo_prompts)} logos..."}
381
+ for prompt in logo_prompts:
382
+ tasks.append(("logo", prompt))
383
+
384
+ if block_prompts:
385
+ yield {status_text: f"Generating {len(block_prompts)} content images..."}
386
+ for p_info in block_prompts:
387
+ tasks.append(("block", p_info))
388
+
389
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
390
+ future_to_task = {executor.submit(generate_image, task[1]['prompt'] if task[0]=='block' else task[1], '16:9' if task[0]=='block' and task[1].get('type')=='hero' else '1:1'): task for task in tasks}
391
+
392
+ logo_results = []
393
+ for i, future in enumerate(concurrent.futures.as_completed(future_to_task)):
394
+ task_type, task_data = future_to_task[future]
395
+ try:
396
+ image_url = future.result()
397
+ if not image_url: continue
398
+
399
+ if task_type == "logo":
400
+ logo_results.append(image_url)
401
+
402
+ elif task_type == "block":
403
+ original_block_id = task_data['blockId']
404
+ new_block_id = original_id_map.get(original_block_id)
405
+ if not new_block_id: continue
406
+
407
+ page_path = task_data['pagePath']
408
+ page_to_update = get_page_by_path(page_path, new_settings)
409
+ if not page_to_update: continue
410
+
411
+ block_to_update = get_block_by_id(page_to_update, new_block_id)
412
+ if not block_to_update: continue
413
+
414
+ key_to_update = 'bgImage' if block_to_update.get('type') == 'hero' else 'image'
415
+ block_to_update[key_to_update] = image_url
416
+
417
+ yield {status_text: f"Processing images... ({i+1}/{len(tasks)})"}
418
+
419
+ except Exception as exc:
420
+ print(f'{task_data} generated an exception: {exc}')
421
+
422
+ yield {
423
+ generating_view: gr.update(visible=False),
424
+ logo_picker_view: gr.update(visible=True, value=logo_results if logo_results else None),
425
+ website_settings_state: new_settings,
426
+ active_page_path_state: active_page_path
427
+ }
428
+
429
+ def handle_logo_select(evt: gr.SelectData, settings, active_path):
430
+ logo_url = evt.value
431
+ settings['header']['logoUrl'] = logo_url
432
+
433
+ page = get_page_by_path(active_path, settings)
434
+ html = generate_page_html(page, settings)
435
+
436
+ # Update controls with the new settings
437
+ page_names = [p['name'] for p in settings['pages']]
438
+ page_paths = [p['path'] for p in settings['pages']]
439
+ active_page_name = get_page_by_path(active_path, settings)['name']
440
+
441
+ return {
442
+ logo_picker_view: gr.update(visible=False),
443
+ preview_view: gr.update(visible=True),
444
+ website_settings_state: settings,
445
+ html_preview: html,
446
+ code_preview: html,
447
+ # Update controls
448
+ page_selector: gr.update(choices=page_names, value=active_page_name),
449
+ header_title_input: settings['header']['title'],
450
+ footer_text_input: settings['footer']['text'],
451
+ theme_app_bg_input: settings['theme']['appBg'],
452
+ theme_panel_bg_input: settings['theme']['panelBg'],
453
+ theme_header_bg_input: settings['theme']['headerBg'],
454
+ theme_footer_bg_input: settings['theme']['footerBg'],
455
+ theme_primary_text_input: settings['theme']['primaryTextColor'],
456
+ theme_secondary_text_input: settings['theme']['secondaryTextColor'],
457
+ theme_primary_accent_input: settings['theme']['primaryAccent'],
458
+ theme_secondary_accent_input: settings['theme']['secondaryAccent'],
459
+ theme_font_family_input: settings['theme']['fontFamily'],
460
+ }
461
+
462
+ def handle_page_change(page_name, settings):
463
+ path = next((p['path'] for p in settings['pages'] if p['name'] == page_name), None)
464
+ if not path: return {}
465
+
466
+ page = get_page_by_path(path, settings)
467
+ html = generate_page_html(page, settings)
468
+
469
+ # Create dynamic controls for the selected page's content blocks
470
+ content_controls = []
471
+ for i, block in enumerate(page.get('contentBlocks', [])):
472
+ with gr.Accordion(f"{block.get('type', 'Block').capitalize()} Block #{i+1}", open=True):
473
+ if 'headline' in block:
474
+ gr.Textbox(value=block.get('headline'), label="Headline", elem_id=f"{path}_{block['id']}_headline")
475
+ if 'subheadline' in block:
476
+ gr.Textbox(value=block.get('subheadline'), label="Subheadline", elem_id=f"{path}_{block['id']}_subheadline")
477
+ if 'text' in block:
478
+ gr.Textbox(value=block.get('text'), label="Text", lines=4, elem_id=f"{path}_{block['id']}_text")
479
+ # Add more controls for other block types (features, paragraphs, etc.) if needed
480
+
481
+ return {
482
+ active_page_path_state: path,
483
+ html_preview: html,
484
+ code_preview: html,
485
+ page_content_controls: gr.update(value=content_controls)
486
+ }
487
+
488
+ def handle_setting_change(settings, path, *values):
489
+ # This is a simplified handler. It assumes the order of values matches the controls.
490
+ # A more robust solution would use elem_id to map values to settings.
491
+ settings['header']['title'] = values[0]
492
+ settings['footer']['text'] = values[1]
493
+ theme_keys = list(settings['theme'].keys())
494
+ for i, key in enumerate(theme_keys):
495
+ settings['theme'][key] = values[2 + i]
496
+
497
+ page = get_page_by_path(path, settings)
498
+ html = generate_page_html(page, settings)
499
+ return settings, html, html
500
+
501
+ def handle_content_change(request: gr.Request, settings, active_path, *content_values):
502
+ # A very basic content handler based on elem_id.
503
+ # Gradio's current API makes granular updates complex.
504
+ # This is a placeholder for a more robust implementation.
505
+
506
+ # Example of how one would update:
507
+ # changed_elem_id = request.elem_id
508
+ # new_value = content_values[??]
509
+ # path, block_id, field = changed_elem_id.split('_')
510
+ # page = get_page_by_path(path, settings)
511
+ # block = get_block_by_id(page, block_id)
512
+ # block[field] = new_value
513
 
514
+ page = get_page_by_path(active_path, settings)
515
+ html = generate_page_html(page, settings)
516
+ return settings, html, html
 
517
 
518
+ # --- Gradio Blocks ---
519
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="indigo"), css="""
520
+ .gradio-container { background-color: #0d1117; }
521
+ #logo-picker .thumbnail-item { height: 120px !important; width: 120px !important; }
522
+ """) as demo:
523
+ # --- State Management ---
524
+ website_settings_state = gr.State(DEFAULT_WEBSITE_SETTINGS)
525
+ active_page_path_state = gr.State("index.html")
526
+
527
+ # --- View: Welcome ---
528
+ with gr.Column(visible=True) as welcome_view:
529
+ gr.Markdown("# 🚀 AI Website Architect\nWelcome! Instead of code, you'll answer a series of questions about your vision. Our AI will then act as your creative director, designer, and content strategist to generate a complete, multi-page website tailored to your needs—from brand identity and color schemes to page layouts and content.",)
530
+ start_button = gr.Button("Start Building Your Website", variant="primary")
531
+
532
+ # --- View: Questionnaire ---
533
+ with gr.Column(visible=False) as questionnaire_view:
534
+ gr.Markdown("# 📝 Build Your Website\nAnswer these questions to give the AI a creative brief for your project.")
535
+ error_box = gr.Textbox(label="Error", visible=False, interactive=False)
536
+
537
+ # Pre-filled answers for quick testing
538
+ default_answers = {
539
+ 'name': 'QuantumBank', 'purpose': 'A high-tech financial company exploring the intersection of quantum computing and finance.',
540
+ 'audience': 'Investors, researchers, and tech enthusiasts interested in fintech.', 'style': 'Dark, futuristic, and professional with a high-tech feel.',
541
+ 'colors': 'Deep blues, purples, with bright cyan accents.', 'theme': 'dark', 'pages': 'Home, About Us, Research, Philosophy, Contact',
542
+ 'homeMessage': 'QuantumBank: The Future of Finance is Here.', 'features': '- Quantum-secured transactions\n- AI-powered investment analysis\n- Decentralized financial instruments',
543
+ 'about': 'Founded by leading quantum physicists and financial experts, QuantumBank is pioneering the next generation of financial technology.',
544
+ 'tone': 'Authoritative, innovative, and forward-thinking.', 'tagline': 'Banking at the speed of light.', 'extra': 'The design should feel sleek and sophisticated, almost like science fiction made real.'
545
+ }
546
+
547
+ with gr.Accordion("Core Identity", open=True):
548
+ q_name = gr.Textbox(label="Website/Company Name", value=default_answers['name'])
549
+ q_purpose = gr.Textbox(label="Primary Purpose", lines=3, value=default_answers['purpose'])
550
+ q_audience = gr.Textbox(label="Target Audience", lines=3, value=default_answers['audience'])
551
+ with gr.Accordion("Aesthetics", open=True):
552
+ q_style = gr.Textbox(label="Overall Style/Vibe", value=default_answers['style'])
553
+ q_colors = gr.Textbox(label="Preferred Colors", value=default_answers['colors'])
554
+ q_theme = gr.Radio(label="Theme", choices=["dark", "light"], value=default_answers['theme'])
555
+ with gr.Accordion("Content & Structure", open=True):
556
+ q_pages = gr.Textbox(label="Required Pages", value=default_answers['pages'])
557
+ q_homeMessage = gr.Textbox(label="Home Page Message/Headline", value=default_answers['homeMessage'])
558
+ q_features = gr.Textbox(label="Key Features/Services", lines=3, value=default_answers['features'])
559
+ q_about = gr.Textbox(label="About Us Content", lines=4, value=default_answers['about'])
560
+ with gr.Accordion("Brand Voice & Final Touches", open=True):
561
+ q_tone = gr.Textbox(label="Tone of Voice", value=default_answers['tone'])
562
+ q_tagline = gr.Textbox(label="Tagline/Slogan", value=default_answers['tagline'])
563
+ q_extra = gr.Textbox(label="Additional Instructions", lines=3, value=default_answers['extra'])
564
+
565
+ questionnaire_inputs = [q_name, q_purpose, q_audience, q_style, q_colors, q_theme, q_pages, q_homeMessage, q_features, q_about, q_tone, q_tagline, q_extra]
566
+ generate_button = gr.Button("Generate My Website", variant="primary")
567
+
568
+ # --- View: Generating ---
569
+ with gr.Column(visible=False, elem_id="generating-view") as generating_view:
570
+ gr.Markdown("## ⏳ Generating Your Website...")
571
+ status_text = gr.Textbox("Initializing...", label="Status", interactive=False)
572
+
573
+ # --- View: Logo Picker ---
574
+ with gr.Column(visible=False) as logo_picker_view:
575
+ gr.Markdown("## ✨ Choose Your Logo\nThe AI has generated these logo options. Pick your favorite to continue.")
576
+ logo_gallery = gr.Gallery(label="Logo Options", columns=5, object_fit="contain", elem_id="logo-picker")
577
+
578
+ # --- View: Preview & Editor ---
579
+ with gr.Row(visible=False) as preview_view:
580
+ with gr.Column(scale=1):
581
+ gr.Markdown("## 🛠️ Website Editor")
582
+ with gr.Tabs():
583
+ with gr.TabItem("Pages & Content"):
584
+ page_selector = gr.Radio(label="Select Page to Edit", choices=["Home"], value="Home")
585
+ with gr.Column() as page_content_controls:
586
+ gr.Markdown("Page content controls will appear here.")
587
+ with gr.TabItem("Global Settings"):
588
+ with gr.Accordion("Header & Footer", open=True):
589
+ header_title_input = gr.Textbox(label="Header: Site Title")
590
+ footer_text_input = gr.Textbox(label="Footer: Copyright Text")
591
+ with gr.TabItem("Theme"):
592
+ with gr.Accordion("Colors", open=True):
593
+ theme_app_bg_input = gr.ColorPicker(label="App Background")
594
+ theme_panel_bg_input = gr.ColorPicker(label="Panel Background")
595
+ theme_header_bg_input = gr.ColorPicker(label="Header Background")
596
+ theme_footer_bg_input = gr.ColorPicker(label="Footer Background")
597
+ theme_primary_text_input = gr.ColorPicker(label="Primary Text")
598
+ theme_secondary_text_input = gr.ColorPicker(label="Secondary Text")
599
+ theme_primary_accent_input = gr.ColorPicker(label="Primary Accent")
600
+ theme_secondary_accent_input = gr.ColorPicker(label="Secondary Accent")
601
+ with gr.Accordion("Font", open=True):
602
+ theme_font_family_input = gr.Dropdown(label="Font Family", choices=['font-sans', 'font-serif', 'font-mono'])
603
+ with gr.Column(scale=3):
604
+ with gr.Tabs():
605
+ with gr.TabItem("Live Preview"):
606
+ html_preview = gr.HTML(value="<p>Your website preview will appear here.</p>",)
607
+ with gr.TabItem("Embed Code"):
608
+ code_preview = gr.Code(language="html", label="HTML Code")
609
+
610
+ # --- Event Wiring ---
611
+ start_button.click(handle_start, outputs=[welcome_view, questionnaire_view])
612
+
613
+ generate_button.click(
614
+ handle_generate_layout,
615
+ inputs=questionnaire_inputs,
616
+ outputs=[
617
+ questionnaire_view, generating_view, status_text, error_box,
618
+ logo_picker_view, website_settings_state, active_page_path_state
619
+ ]
620
+ )
621
+
622
+ logo_gallery.select(
623
+ handle_logo_select,
624
+ inputs=[website_settings_state, active_page_path_state],
625
+ outputs=[
626
+ logo_picker_view, preview_view, website_settings_state, html_preview, code_preview,
627
+ page_selector, header_title_input, footer_text_input,
628
+ theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
629
+ theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
630
+ theme_secondary_accent_input, theme_font_family_input
631
+ ]
632
+ )
633
+
634
+ page_selector.change(
635
+ handle_page_change,
636
+ inputs=[page_selector, website_settings_state],
637
+ outputs=[active_page_path_state, html_preview, code_preview, page_content_controls]
638
+ )
639
+
640
+ # Consolidate setting controls for easier handling
641
+ setting_controls = [
642
+ header_title_input, footer_text_input,
643
+ theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
644
+ theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
645
+ theme_secondary_accent_input, theme_font_family_input
646
+ ]
647
+
648
+ for control in setting_controls:
649
+ control.change(
650
+ handle_setting_change,
651
+ inputs=[website_settings_state, active_page_path_state] + setting_controls,
652
+ outputs=[website_settings_state, html_preview, code_preview]
653
+ )
654
 
655
+ return demo
 
656
 
 
657
 
 
658
  if __name__ == "__main__":
659
+ app = create_gradio_app()
660
+ # To run in a Hugging Face Space, the port must be 7860
661
+ app.launch(server_name="0.0.0.0", server_port=7860)