admin08077 commited on
Commit
bb8789f
·
verified ·
1 Parent(s): 361d146

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +628 -308
app.py CHANGED
@@ -1,336 +1,656 @@
1
 
2
  import gradio as gr
3
- import google as genai
4
  import os
5
- import random
6
- import time
7
- from textwrap import dedent
8
-
9
- # --- Load Gemini API key securely ---
10
- GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
11
- if not GEMINI_API_KEY:
12
- raise ValueError("❌ GEMINI_API_KEY not found. Please set it in your Hugging Face Space Secrets.")
13
-
14
- client = genai.Client()
15
-
16
-
17
- # --- 1. CONSTANTS ---
18
- # Replicates constants from the original React app
19
- GEMINI_TEXT_MODEL = 'gemini-2.5-flash'
20
- # Note: imagen-3.0-generate-002 is not available in the Python SDK yet.
21
- # We will handle this limitation gracefully in the app logic.
22
- GEMINI_IMAGE_MODEL = 'imagen-3.0-generate-002'
23
-
24
- class Agent:
25
- SYSTEM = 'SYSTEM'
26
- OPTIMIST = 'OPTIMIST'
27
- SKEPTIC = 'SKEPTIC'
28
-
29
- SYSTEM_AVATAR = 'https://i.imgur.com/z3uxLzT.png'
30
- AGENT_AVATARS = {
31
- Agent.OPTIMIST: 'https://i.imgur.com/2GMV9y7.png',
32
- Agent.SKEPTIC: 'https://i.imgur.com/KzX3T21.png',
33
- Agent.SYSTEM: SYSTEM_AVATAR,
34
- }
35
-
36
- AGENT_PROMPTS = {
37
- Agent.OPTIMIST: {
38
- "personality": "You are Agent A, a tech optimist and futurist. You see the incredible potential and benefits in every new technology. Your arguments are passionate, forward-thinking, and inspiring. Respond in 1-2 concise paragraphs.",
39
- "prompt": lambda topic, last_response: f"Debate Topic: '{topic}'. The last statement was: '{last_response}'. Provide your powerful, optimistic argument.",
40
  },
41
- Agent.SKEPTIC: {
42
- "personality": "You are Agent B, a cautious skeptic and ethicist. You ground the conversation by highlighting risks, societal impact, and unforeseen consequences. Your arguments are critical, grounded, and raise important questions. Respond in 1-2 concise paragraphs.",
43
- "prompt": lambda topic, last_response: f"Debate Topic: '{topic}'. Agent A said: '{last_response}'. Provide your insightful, skeptical counter-argument.",
 
 
 
 
44
  },
 
 
 
 
 
 
 
 
45
  }
46
 
47
- GENERATION_PROMPTS = {
48
- "topic": "Generate one specific, debatable topic about the future of AI. The topic should be a short phrase. Output only the topic itself.",
49
- "question": lambda topic: f"For the debate topic '{topic}', create one provocative question to start the debate. Output only the question itself.",
50
- }
51
-
52
- INITIAL_MESSAGE = [(None, "Awaiting signal to begin debate protocol...")]
53
-
54
- # --- 2. JAVASCRIPT FOR TEXT-TO-SPEECH ---
55
- # This JS code runs in the user's browser to enable speech.
56
- tts_js = dedent("""
57
- function() {
58
- let optimistVoice = null;
59
- let skepticVoice = null;
60
- let voices = [];
61
-
62
- const OPTIMIST_VOICE_CANDIDATES = ['Google UK English Female', 'Samantha', 'Microsoft Zira - English (United States)', 'Karen', 'en-GB-female'];
63
- const SKEPTIC_VOICE_CANDIDATES = ['Google UK English Male', 'Daniel', 'Microsoft David - English (United States)', 'Rishi', 'en-GB-male'];
64
-
65
- const getVoices = () => {
66
- return new Promise((resolve) => {
67
- const voiceList = window.speechSynthesis.getVoices();
68
- if (voiceList.length) {
69
- resolve(voiceList);
70
- return;
71
- }
72
- window.speechSynthesis.onvoiceschanged = () => {
73
- resolve(window.speechSynthesis.getVoices());
74
- };
75
- });
76
- };
77
-
78
- const selectVoices = async () => {
79
- voices = await getVoices();
80
- if (!voices.length) return;
81
-
82
- for (const name of OPTIMIST_VOICE_CANDIDATES) {
83
- const found = voices.find(v => v.name === name);
84
- if (found) { optimistVoice = found; break; }
85
- }
86
- if (!optimistVoice) {
87
- optimistVoice = voices.find(v => v.lang.startsWith('en') && /female/i.test(v.name)) || voices.find(v => v.lang.startsWith('en')) || null;
88
- }
89
-
90
- for (const name of SKEPTIC_VOICE_CANDIDATES) {
91
- const found = voices.find(v => v.name === name && v !== optimistVoice);
92
- if (found) { skepticVoice = found; break; }
93
- }
94
- if (!skepticVoice) {
95
- skepticVoice = voices.find(v => v.lang.startsWith('en') && /male/i.test(v.name) && v !== optimistVoice) || voices.find(v => v.lang.startsWith('en') && v !== optimistVoice) || null;
96
- }
97
- };
98
-
99
- window.initSpeech = () => {
100
- if (typeof window === 'undefined' || !('speechSynthesis' in window)) {
101
- console.warn("Speech Synthesis not supported in this browser.");
102
- return;
103
- }
104
- selectVoices();
105
- window.speechSynthesis.onvoiceschanged = selectVoices;
106
- };
107
-
108
- window.speakText = (text, agent) => {
109
- if (!window.speechSynthesis || !text) return;
110
- window.speechSynthesis.cancel();
111
- const utterance = new SpeechSynthesisUtterance(text);
112
- if (agent === 'OPTIMIST' && optimistVoice) {
113
- utterance.voice = optimistVoice;
114
- } else if (agent === 'SKEPTIC' && skepticVoice) {
115
- utterance.voice = skepticVoice;
116
- }
117
- utterance.pitch = 1;
118
- utterance.rate = 1;
119
- utterance.volume = 0.9;
120
- window.speechSynthesis.speak(utterance);
121
- };
122
-
123
- window.cancelSpeech = () => {
124
- if (window.speechSynthesis) {
125
- window.speechSynthesis.cancel();
126
- }
127
- };
128
-
129
- return (history, text, agent) => {
130
- window.speakText(text, agent);
131
- return history;
132
- }
133
- }
134
- """)
135
-
136
- # --- 3. GEMINI SERVICE ---
137
- class GeminiService:
138
- """A service class to encapsulate all interactions with the Google Gemini API."""
139
- def __init__(self):
140
- try:
141
- api_key = os.environ["API_KEY"]
142
- genai.configure(api_key=api_key)
143
- except KeyError:
144
- raise RuntimeError("FATAL: API_KEY environment variable not set. Please add it to your environment.")
145
- self.text_model = genai.GenerativeModel(GEMINI_TEXT_MODEL)
146
-
147
- def generate_topic(self):
148
- """Generates a debate topic."""
149
- response = self.text_model.generate_content(
150
- GENERATION_PROMPTS["topic"],
151
- generation_config={"temperature": 1.0}
152
- )
153
- return response.text.strip()
154
-
155
- def generate_question(self, topic):
156
- """Generates a starting question for a given topic."""
157
- response = self.text_model.generate_content(
158
- GENERATION_PROMPTS["question"](topic),
159
- generation_config={"temperature": 1.0}
160
- )
161
- return response.text.strip()
162
-
163
- def create_agent_session(self, agent_type):
164
- """Creates a stateful chat session for an agent with a specific personality."""
165
- personality = AGENT_PROMPTS[agent_type]["personality"]
166
- return self.text_model.start_chat(
167
- history=[
168
- {'role': 'user', 'parts': [personality]},
169
- {'role': 'model', 'parts': ["Understood. I am ready."]}
170
- ]
171
  )
172
-
173
- # Instantiate the service. This will raise an error on startup if the API key is missing.
174
- gemini_service = GeminiService()
175
-
176
- # --- 4. GRADIO APP ---
177
- def create_interface():
178
- # Gradio CSS for styling
179
- css = """
180
- @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=Roboto:wght@400&display=swap');
181
- body { font-family: 'Roboto', sans-serif; }
182
- .font-orbitron { font-family: 'Orbitron', sans-serif; }
183
- .gradio-container { background: linear-gradient(45deg, #0d001a, #1a001a); }
184
- .chatbot { background-color: rgba(0,0,0,0.2) !important; border: none !important; }
185
- .chatbot .user-message, .chatbot .bot-message { max-width: 100% !important; }
186
- .system-message-container { display: flex; align-items: center; justify-content: center; gap: 1rem; margin: 1rem 0; }
187
- .system-message-line { flex-grow: 1; border-top: 1px dashed rgba(52, 211, 153, 0.3); }
188
- .system-message-text { color: #34d399; font-family: monospace; font-size: 0.8rem; white-space: pre-wrap; text-align: center; }
189
- .image-message-container { padding: 0.5rem; border: 1px solid rgba(168, 85, 247, 0.5); background: rgba(168, 85, 247, 0.1); border-radius: 0.5rem; margin: 1.5rem auto; max-width: 80%; }
190
- .image-message-container img { border-radius: 0.25rem; }
191
- .image-message-caption { color: #c084fc; font-size: 0.75rem; text-align: center; font-family: monospace; margin-top: 0.5rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  """
193
 
194
- with gr.Blocks(theme=gr.themes.Base(), css=css) as demo:
195
- # App State
196
- topic_state = gr.State("")
197
- question_state = gr.State("")
198
-
199
- # Header
200
- with gr.Row():
201
- gr.HTML("""
202
- <div style="text-align: center; padding: 1rem;">
203
- <h1 class="font-orbitron" style="font-size: 2rem; font-weight: bold; color: #67e8f9; text-shadow: 0 0 5px #00ffff;">AI DEBATE SIMULATOR</h1>
204
- <h2 class="font-orbitron" style="font-size: 1rem; color: #f472b6; text-shadow: 0 0 5px #ff00ff;">Hardened Edition</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  </div>
206
- """)
207
-
208
- # Chat Window
209
- chatbot = gr.Chatbot(
210
- label="Debate Arena",
211
- value=INITIAL_MESSAGE,
212
- elem_classes=["chatbot"],
213
- height=600,
214
- avatar_images=(AGENT_AVATARS[Agent.SKEPTIC], AGENT_AVATARS[Agent.OPTIMIST]),
215
- show_copy_button=False,
216
- bubble_full_width=False,
217
- )
218
 
219
- # Controls
220
- start_btn = gr.Button("▷ START NEW DEBATE", variant="primary", elem_classes="font-orbitron")
221
-
222
- # Hidden components for JS interop
223
- text_to_speak = gr.Textbox(visible=False)
224
- agent_to_speak = gr.Textbox(visible=False)
225
-
226
- def add_system_message(history, text):
227
- container_html = f"""
228
- <div class="system-message-container">
229
- <div class="system-message-line"></div>
230
- <p class="system-message-text">{text}</p>
231
- <div class="system-message-line"></div>
232
- </div>
233
- """
234
- history.append((None, container_html))
235
- time.sleep(0.5)
236
- return history
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
 
238
- def run_full_debate_flow():
239
- yield {start_btn: gr.update(value="🔄 Initializing...", interactive=False), chatbot: []}
 
240
 
241
- history = []
242
- yield { chatbot: history, text_to_speak: "", agent_to_speak: "" }
243
 
244
- try:
245
- history = add_system_message(history, "SYSTEM: Receiving transmission... Booting up debate protocol.")
246
- yield {chatbot: history}
 
 
247
 
248
- history = add_system_message(history, "SYSTEM: Contacting orbital AI to generate a fresh topic...")
249
- yield {chatbot: history}
250
- topic = gemini_service.generate_topic()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
- history = add_system_message(history, "SYSTEM: Formulating a provocative starting question...")
253
- yield {chatbot: history}
254
- question = gemini_service.generate_question(topic)
 
 
 
 
 
 
 
 
 
255
 
256
- topic_and_question_text = f"TOPIC: {topic}\\n\\nQUESTION: {question}"
257
- history = add_system_message(history, topic_and_question_text)
258
- yield {chatbot: history, topic_state: topic, question_state: question}
259
-
260
- history = add_system_message(history, "SYSTEM: Initializing agents... Let the debate begin!")
261
- yield {chatbot: history, start_btn: gr.update(value="⚔️ DEBATE IN PROGRESS...")}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
 
263
- optimist_session = gemini_service.create_agent_session(Agent.OPTIMIST)
264
- skeptic_session = gemini_service.create_agent_session(Agent.SKEPTIC)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
- last_response = question
267
- debate_turns = 3
268
- image_turn = random.randint(2, debate_turns * 2 - 1)
269
-
270
- turn_counter = 0
271
- for _ in range(debate_turns):
272
- turn_counter += 1
273
- history.append(("", ""))
274
- optimist_prompt = AGENT_PROMPTS[Agent.OPTIMIST]['prompt'](topic, last_response)
275
- stream = optimist_session.send_message(optimist_prompt, stream=True)
276
- full_response = ""
277
- for chunk in stream:
278
- full_response += chunk.text
279
- history[-1] = (full_response + "▌", None)
280
- yield {chatbot: history}
281
- history[-1] = (full_response, None)
282
- last_response = full_response
283
- yield {chatbot: history, text_to_speak: full_response, agent_to_speak: Agent.OPTIMIST}
284
-
285
- if turn_counter == image_turn:
286
- history = add_system_message(history, "SYSTEM: Visual data generation is not supported in this Python version.\\n(Imagen 3 is not yet available in the Python SDK).")
287
- yield {chatbot: history}
288
-
289
- turn_counter += 1
290
- history.append(("", ""))
291
- skeptic_prompt = AGENT_PROMPTS[Agent.SKEPTIC]['prompt'](topic, last_response)
292
- stream = skeptic_session.send_message(skeptic_prompt, stream=True)
293
- full_response = ""
294
- for chunk in stream:
295
- full_response += chunk.text
296
- history[-1] = (None, full_response + "▌")
297
- yield {chatbot: history}
298
- history[-1] = (None, full_response)
299
- last_response = full_response
300
- yield {chatbot: history, text_to_speak: full_response, agent_to_speak: Agent.SKEPTIC}
301
-
302
- if turn_counter == image_turn:
303
- history = add_system_message(history, "SYSTEM: Visual data generation is not supported in this Python version.\\n(Imagen 3 is not yet available in the Python SDK).")
304
- yield {chatbot: history}
305
-
306
- except Exception as e:
307
- print(f"An error occurred: {e}")
308
- error_message = f"❌ FATAL ERROR: {e}. Protocol terminated."
309
- history = add_system_message(history, error_message)
310
- yield {chatbot: history, start_btn: gr.update(value="▷ RE-INITIATE DEBATE", interactive=True)}
311
- return
312
-
313
- history = add_system_message(history, "--- DEBATE CONCLUDED ---")
314
- yield {chatbot: history, start_btn: gr.update(value=" START NEW DEBATE", interactive=True)}
315
-
316
- start_btn.click(
317
- fn=run_full_debate_flow,
318
- outputs=[chatbot, start_btn, topic_state, question_state, text_to_speak, agent_to_speak]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  )
320
 
321
- agent_to_speak.change(
322
- fn=None,
323
- js=tts_js,
324
- inputs=[chatbot, text_to_speak, agent_to_speak],
325
- outputs=[chatbot],
326
- api_name=False
327
  )
328
 
329
- demo.load(fn=None, js="() => { window.initSpeech(); }")
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
  return demo
332
 
333
- # --- 5. MAIN EXECUTION BLOCK ---
334
  if __name__ == "__main__":
335
- app_interface = create_interface()
336
- app_interface.queue().launch(debug=True, server_name="0.0.0.0", server_port=7860)
 
 
1
 
2
  import gradio as gr
 
3
  import os
4
+ import google.generativeai as genai
5
+ from google.generativeai.types import HarmCategory, HarmBlockThreshold
6
+ import json
7
+ from PIL import Image
8
+ from io import BytesIO
9
+ import base64
10
+ import uuid
11
+ import concurrent.futures
12
+ import re
13
+
14
+ # --- Configuration and Initialization ---
15
+
16
+ # Configure the Gemini API key from Hugging Face secrets
17
+ try:
18
+ API_KEY = os.environ["API_KEY"]
19
+ genai.configure(api_key=API_KEY)
20
+ except KeyError:
21
+ raise ValueError("API_KEY environment variable not set. Please add it to your Hugging Face Space secrets.")
22
+
23
+ # --- Default Data Structures (Ported from TypeScript) ---
24
+
25
+ DEFAULT_WEBSITE_SETTINGS = {
26
+ "theme": {
27
+ "appBg": "#111827", "panelBg": "#1f2937", "headerBg": "#1f2937",
28
+ "footerBg": "#1f2937", "primaryTextColor": "#f9fafb", "secondaryTextColor": "#d1d5db",
29
+ "primaryAccent": "#4f46e5", "secondaryAccent": "#10b981", "fontFamily": "font-sans",
 
 
 
 
 
 
 
 
 
30
  },
31
+ "header": { "title": "My Awesome Site", "logoUrl": "", "navLinks": [] },
32
+ "footer": {
33
+ "text": "© 2024 My Awesome Site. All rights reserved.",
34
+ "links": [
35
+ {"text": "Privacy Policy", "href": "#"},
36
+ {"text": "Terms of Service", "href": "#"},
37
+ ],
38
  },
39
+ "pages": [{
40
+ "path": "index.html", "name": "Home",
41
+ "contentBlocks": [{
42
+ "id": str(uuid.uuid4()), "type": "hero", "headline": "Welcome to Your AI-Generated Website",
43
+ "subheadline": "Describe your vision and watch it come to life.",
44
+ "bgImage": "", "headlineColor": "#ffffff", "subheadlineColor": "#d1d5db",
45
+ }],
46
+ }],
47
  }
48
 
49
+ # --- Core AI and Helper Functions ---
50
+
51
+ def create_prompt_from_answers(answers):
52
+ # This prompt is a direct port from the React app, instructing the AI on how to generate the website layout.
53
+ return f"""
54
+ You are a world-class creative director, UI/UX designer, and content strategist tasked with building a complete multi-page website.
55
+ 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.
56
+
57
+ **User's Website Requirements:**
58
+ - **Website Name:** {answers.get('name', 'AI Generated Website')}
59
+ - **Core Purpose:** {answers.get('purpose', 'Not specified')}
60
+ - **Target Audience:** {answers.get('audience', 'General audience')}
61
+ - **Desired Style/Vibe:** {answers.get('style', 'A modern, clean design')}
62
+ - **Color Palette:** {answers.get('colors', 'A balanced color scheme based on the style')}
63
+ - **Theme Preference:** {answers.get('theme', 'dark')}
64
+ - **Required Pages:** {answers.get('pages', 'Home, About, Contact')}
65
+ - **Home Page Headline/Message:** {answers.get('homeMessage', f"Welcome to {answers.get('name', 'Our Website')}")}
66
+ - **Key Features/Services:** {answers.get('features', 'Feature 1, Feature 2, Feature 3')}
67
+ - **About Us Content:** {answers.get('about', 'A brief description of the company.')}
68
+ - **Contact Info:** {answers.get('contact', 'A contact form.')}
69
+ - **Brand Tone of Voice:** {answers.get('tone', 'Professional and friendly')}
70
+ - **Tagline:** {answers.get('tagline', '')}
71
+ - **Additional Instructions:** {answers.get('extra', 'None')}
72
+
73
+ **Your Mission:**
74
+ Based *only* on the requirements above, generate a single, comprehensive JSON object for this website. Adhere strictly to the provided JSON schema.
75
+
76
+ **Mandatory Requirements:**
77
+ 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).
78
+ 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')}).
79
+ 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')}).
80
+ 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.
81
+ 5. **Navigation:** Populate the 'navLinks' array in the header settings. The links must correspond to the pages you create.
82
+ 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.
83
+ """
84
+
85
+ def generate_image(prompt: str, aspect_ratio: str):
86
+ """Generates an image using Gemini and returns a base64 data URL."""
87
+ print(f"Generating image for prompt: {prompt}")
88
+ try:
89
+ model = genai.GenerativeModel("gemini-2.0-flash-preview-image-generation")
90
+ full_prompt = f'Generate a single, high-quality image. Description: "{prompt}". The image should have a {aspect_ratio} aspect ratio.'
91
+
92
+ response = model.generate_content(
93
+ contents=full_prompt,
94
+ generation_config=genai.types.GenerateContentConfig(response_modalities=['TEXT', 'IMAGE'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  )
96
+
97
+ image_part = next((part for part in response.candidates[0].content.parts if part.inline_data), None)
98
+
99
+ if image_part:
100
+ mime_type = image_part.inline_data.mime_type
101
+ data = base64.b64encode(image_part.inline_data.data).decode("utf-8")
102
+ return f"data:{mime_type};base64,{data}"
103
+ else:
104
+ print(f"Warning: Image generation failed for prompt: {prompt}. No image data in response.")
105
+ return ""
106
+ except Exception as e:
107
+ print(f"Error generating image for prompt '{prompt}': {e}")
108
+ return ""
109
+
110
+ def generate_page_html(page, settings):
111
+ """Generates the HTML for a single page based on the website settings."""
112
+ if not page or not settings:
113
+ return "<p>Error: Page or settings not found.</p>"
114
+
115
+ theme = settings.get("theme", {})
116
+ header = settings.get("header", {})
117
+ footer = settings.get("footer", {})
118
+ nav_links = header.get("navLinks", [])
119
+
120
+ # Header HTML
121
+ header_html = f"""
122
+ <header style="background-color: {theme.get('headerBg')}; color: {theme.get('primaryTextColor')}; padding: 1rem 2rem;">
123
+ <div class="container mx-auto flex justify-between items-center">
124
+ <div class="flex items-center space-x-4">
125
+ {'<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 ''}
126
+ <a href="index.html" class="text-2xl font-bold">{header.get('title', '')}</a>
127
+ </div>
128
+ <div class="hidden md:flex items-center space-x-6">
129
+ {''.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])}
130
+ </div>
131
+ </div>
132
+ </header>
133
  """
134
 
135
+ # Content Blocks HTML
136
+ main_content_html = ""
137
+ for block in page.get("contentBlocks", []):
138
+ block_type = block.get("type")
139
+ if block_type == 'hero':
140
+ main_content_html += f"""
141
+ <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;">
142
+ <h2 class="text-4xl md:text-6xl font-bold" style="color: {block.get('headlineColor', '#ffffff')};">{block.get('headline', '')}</h2>
143
+ <p class="mt-4 text-lg md:text-xl max-w-2xl" style="color: {block.get('subheadlineColor', '#d1d5db')};">{block.get('subheadline', '')}</p>
144
+ </section>"""
145
+ elif block_type == 'textWithImage':
146
+ text_html = (block.get('text', '') or '').replace('\n', '<br/>')
147
+ main_content_html += f"""
148
+ <section class="container mx-auto py-12 md:py-20 px-6">
149
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
150
+ <div class="{'md:order-1' if block.get('imagePosition') == 'right' else 'md:order-2'}">
151
+ <h3 class="text-3xl font-bold mb-4" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h3>
152
+ <p style="color: {theme.get('secondaryTextColor')};">{text_html}</p>
153
+ </div>
154
+ <div class="{'md:order-2' if block.get('imagePosition') == 'right' else 'md:order-1'}">
155
+ <img src="{block.get('image', '')}" alt="{block.get('headline', 'content image')}" class="rounded-lg shadow-xl w-full h-auto object-cover aspect-square">
156
+ </div>
157
+ </div>
158
+ </section>"""
159
+ elif block_type == 'featureList':
160
+ features_html = ''.join([f"""
161
+ <div class="p-6 rounded-lg" style="background-color: {theme.get('panelBg')};">
162
+ <h4 class="text-xl font-bold mb-2" style="color: {theme.get('primaryAccent')};">{f.get('title', '')}</h4>
163
+ <p style="color: {theme.get('secondaryTextColor')};">{f.get('description', '')}</p>
164
+ </div>""" for f in block.get('features', [])])
165
+ main_content_html += f"""
166
+ <section class="container mx-auto py-12 md:py-20 px-6">
167
+ <h2 class="text-4xl font-bold text-center mb-12" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
168
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-8">{features_html}</div>
169
+ </section>"""
170
+ elif block_type == 'text':
171
+ paragraphs_html = ''.join([f'<p>{p}</p>' for p in block.get('paragraphs', [])])
172
+ main_content_html += f"""
173
+ <section class="container mx-auto py-12 md:py-20 px-6 max-w-4xl">
174
+ <h2 class="text-4xl font-bold text-center mb-12" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
175
+ <div class="prose prose-invert lg:prose-xl mx-auto" style="color: {theme.get('secondaryTextColor')};">{paragraphs_html}</div>
176
+ </section>
177
+ """
178
+ elif block_type == 'form':
179
+ fields_html = ''.join([f"""
180
+ <div>
181
+ <label for="{f.get('name')}" class="block text-sm font-medium mb-1" style="color: {theme.get('secondaryTextColor')};">{f.get('label', '')}</label>
182
+ <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')};" />
183
+ </div>""" for f in block.get('fields', [])])
184
+ main_content_html += f"""
185
+ <section class="container mx-auto py-12 md:py-20 px-6 flex justify-center">
186
+ <div class="w-full max-w-md p-8 rounded-lg" style="background-color: {theme.get('panelBg')};">
187
+ <h2 class="text-3xl font-bold text-center mb-8" style="color: {theme.get('primaryTextColor')};">{block.get('headline', '')}</h2>
188
+ <form class="space-y-6" onsubmit="event.preventDefault(); alert('Form submitted! (This is a preview)');">
189
+ {fields_html}
190
+ <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>
191
+ </form>
192
+ </div>
193
+ </section>"""
194
+
195
+ # Footer HTML
196
+ footer_html = f"""
197
+ <footer style="background-color: {theme.get('footerBg')}; color: {theme.get('secondaryTextColor')};" class="py-6 px-4 mt-auto">
198
+ <div class="container mx-auto text-center">
199
+ <p>{footer.get('text', '')}</p>
200
+ <div class="mt-2 flex justify-center space-x-4">
201
+ {''.join([f'<a href="{link.get("href")}" class="hover:text-white transition-colors">{link.get("text")}</a>' for link in footer.get("links", [])])}
202
  </div>
203
+ </div>
204
+ </footer>
205
+ """
 
 
 
 
 
 
 
 
 
206
 
207
+ # Final Page Assembly
208
+ return f"""
209
+ <!DOCTYPE html>
210
+ <html lang="en" class="{theme.get('fontFamily', 'font-sans')}">
211
+ <head>
212
+ <meta charset="UTF-8">
213
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
214
+ <title>{header.get('title', '')} - {page.get('name', '')}</title>
215
+ <script src="https://cdn.tailwindcss.com"></script>
216
+ <script>
217
+ tailwind.config = {{
218
+ theme: {{
219
+ extend: {{
220
+ typography: ({{ theme }}) => ({{
221
+ invert: {{
222
+ css: {{
223
+ '--tw-prose-body': theme('colors.gray[300]'),
224
+ '--tw-prose-headings': theme('colors.white'),
225
+ '--tw-prose-links': theme('colors.white'),
226
+ '--tw-prose-bold': theme('colors.white'),
227
+ }},
228
+ }},
229
+ }}),
230
+ }},
231
+ }}
232
+ }}
233
+ </script>
234
+ </head>
235
+ <body style="background-color: {theme.get('appBg')};" class="flex flex-col min-h-screen">
236
+ {header_html}
237
+ <main>{main_content_html}</main>
238
+ {footer_html}
239
+ <script>
240
+ // Prevent iframe navigation from redirecting the whole page
241
+ document.addEventListener('click', function (e) {{
242
+ let target = e.target;
243
+ while (target && target.tagName !== 'A') {{
244
+ target = target.parentElement;
245
+ }}
246
+ if (target && target.tagName === 'A') {{
247
+ const href = target.getAttribute('href');
248
+ if (href && !href.startsWith('#') && !href.startsWith('http')) {{
249
+ e.preventDefault();
250
+ // In a real scenario, you'd postMessage to the parent,
251
+ // but Gradio's buttons will handle navigation instead.
252
+ alert(`Preview navigation to: ${href}`);
253
+ }}
254
+ }}
255
+ }});
256
+ </script>
257
+ </body>
258
+ </html>
259
+ """.strip()
260
+
261
+
262
+ # --- Gradio UI Definition ---
263
+
264
+ def create_gradio_app():
265
+
266
+ # --- UI Helper Functions ---
267
+ def get_page_by_path(path, settings):
268
+ return next((p for p in settings.get("pages", []) if p.get("path") == path), None)
269
 
270
+ def get_block_by_id(page, block_id):
271
+ if not page: return None
272
+ return next((b for b in page.get("contentBlocks", []) if b.get("id") == block_id), None)
273
 
274
+ # --- Event Handlers ---
 
275
 
276
+ def handle_start():
277
+ return {
278
+ welcome_view: gr.update(visible=False),
279
+ questionnaire_view: gr.update(visible=True),
280
+ }
281
 
282
+ def handle_generate_layout(*questionnaire_answers):
283
+ yield {
284
+ questionnaire_view: gr.update(visible=False),
285
+ generating_view: gr.update(visible=True),
286
+ status_text: "Generating website structure..."
287
+ }
288
+
289
+ # Map flat answer list to dictionary
290
+ question_ids = [
291
+ 'name', 'purpose', 'audience', 'style', 'colors', 'theme', 'pages',
292
+ 'homeMessage', 'features', 'about', 'tone', 'tagline', 'extra'
293
+ ]
294
+ answers = dict(zip(question_ids, questionnaire_answers))
295
+
296
+ # --- Step 1: Generate Layout ---
297
+ try:
298
+ model = genai.GenerativeModel('gemini-2.5-flash')
299
+ prompt = create_prompt_from_answers(answers)
300
+ schema = {
301
+ "type": "object",
302
+ "properties": {
303
+ "websiteSettings": {
304
+ "type": "object",
305
+ "properties": {
306
+ "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"}}},
307
+ "header": {"type": "object", "properties": {"title": {"type": "string"},"navLinks": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
308
+ "footer": {"type": "object", "properties": {"text": {"type": "string"},"links": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
309
+ "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"}}}}}}}},
310
+ },
311
+ },
312
+ "imagePrompts": {
313
+ "type": "object",
314
+ "properties": {
315
+ "logoPrompts": {"type": "array", "items": {"type": "string"}},
316
+ "blockImagePrompts": {"type": "array", "items": {"type": "object", "properties": {"pagePath": {"type": "string"},"blockId": {"type": "string"},"prompt": {"type": "string"}}}}
317
+ }
318
+ }
319
+ }
320
+ }
321
+
322
+ response = model.generate_content(
323
+ prompt,
324
+ generation_config=genai.GenerationConfig(
325
+ response_mime_type="application/json",
326
+ response_schema=schema
327
+ ),
328
+ safety_settings={
329
+ HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
330
+ HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
331
+ HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
332
+ HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
333
+ }
334
+ )
335
+ design_plan = json.loads(response.text)
336
+ except Exception as e:
337
+ print(f"Layout generation failed: {e}")
338
+ yield {
339
+ generating_view: gr.update(visible=False),
340
+ questionnaire_view: gr.update(visible=True),
341
+ error_box: gr.update(value=f"Error generating layout: {e}", visible=True)
342
+ }
343
+ return
344
 
345
+ new_settings = design_plan['websiteSettings']
346
+ image_prompts = design_plan['imagePrompts']
347
+
348
+ # Add unique IDs and empty image fields to the settings
349
+ original_id_map = {}
350
+ for page in new_settings.get('pages', []):
351
+ new_blocks = []
352
+ for block in page.get('contentBlocks', []):
353
+ new_id = str(uuid.uuid4())
354
+ original_id = block.get('id')
355
+ if original_id:
356
+ original_id_map[original_id] = new_id
357
 
358
+ block['id'] = new_id
359
+ if block.get('type') == 'hero':
360
+ block['bgImage'] = ''
361
+ if block.get('type') == 'textWithImage':
362
+ block['image'] = ''
363
+ new_blocks.append(block)
364
+ page['contentBlocks'] = new_blocks
365
+ new_settings['header']['logoUrl'] = ''
366
+
367
+ active_page_path = new_settings['pages'][0]['path'] if new_settings.get('pages') else ''
368
+
369
+ # --- Step 2: Generate Images in Parallel ---
370
+ tasks = []
371
+ logo_prompts = image_prompts.get('logoPrompts', [])
372
+ block_prompts = image_prompts.get('blockImagePrompts', [])
373
+
374
+ if logo_prompts:
375
+ yield {status_text: f"Generating {len(logo_prompts)} logos..."}
376
+ for prompt in logo_prompts:
377
+ tasks.append(("logo", prompt))
378
+
379
+ if block_prompts:
380
+ yield {status_text: f"Generating {len(block_prompts)} content images..."}
381
+ for p_info in block_prompts:
382
+ tasks.append(("block", p_info))
383
+
384
+ with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
385
+ 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}
386
+
387
+ logo_results = []
388
+ for i, future in enumerate(concurrent.futures.as_completed(future_to_task)):
389
+ task_type, task_data = future_to_task[future]
390
+ try:
391
+ image_url = future.result()
392
+ if not image_url: continue
393
+
394
+ if task_type == "logo":
395
+ logo_results.append(image_url)
396
+
397
+ elif task_type == "block":
398
+ original_block_id = task_data['blockId']
399
+ new_block_id = original_id_map.get(original_block_id)
400
+ if not new_block_id: continue
401
+
402
+ page_path = task_data['pagePath']
403
+ page_to_update = get_page_by_path(page_path, new_settings)
404
+ if not page_to_update: continue
405
+
406
+ block_to_update = get_block_by_id(page_to_update, new_block_id)
407
+ if not block_to_update: continue
408
+
409
+ key_to_update = 'bgImage' if block_to_update.get('type') == 'hero' else 'image'
410
+ block_to_update[key_to_update] = image_url
411
+
412
+ yield {status_text: f"Processing images... ({i+1}/{len(tasks)})"}
413
+
414
+ except Exception as exc:
415
+ print(f'{task_data} generated an exception: {exc}')
416
+
417
+ yield {
418
+ generating_view: gr.update(visible=False),
419
+ logo_picker_view: gr.update(visible=True, value=logo_results if logo_results else None),
420
+ website_settings_state: new_settings,
421
+ active_page_path_state: active_page_path
422
+ }
423
+
424
+ def handle_logo_select(evt: gr.SelectData, settings, active_path):
425
+ logo_url = evt.value
426
+ settings['header']['logoUrl'] = logo_url
427
+
428
+ page = get_page_by_path(active_path, settings)
429
+ html = generate_page_html(page, settings)
430
+
431
+ # Update controls with the new settings
432
+ page_names = [p['name'] for p in settings['pages']]
433
+ page_paths = [p['path'] for p in settings['pages']]
434
+ active_page_name = get_page_by_path(active_path, settings)['name']
435
+
436
+ return {
437
+ logo_picker_view: gr.update(visible=False),
438
+ preview_view: gr.update(visible=True),
439
+ website_settings_state: settings,
440
+ html_preview: html,
441
+ code_preview: html,
442
+ # Update controls
443
+ page_selector: gr.update(choices=page_names, value=active_page_name),
444
+ header_title_input: settings['header']['title'],
445
+ footer_text_input: settings['footer']['text'],
446
+ theme_app_bg_input: settings['theme']['appBg'],
447
+ theme_panel_bg_input: settings['theme']['panelBg'],
448
+ theme_header_bg_input: settings['theme']['headerBg'],
449
+ theme_footer_bg_input: settings['theme']['footerBg'],
450
+ theme_primary_text_input: settings['theme']['primaryTextColor'],
451
+ theme_secondary_text_input: settings['theme']['secondaryTextColor'],
452
+ theme_primary_accent_input: settings['theme']['primaryAccent'],
453
+ theme_secondary_accent_input: settings['theme']['secondaryAccent'],
454
+ theme_font_family_input: settings['theme']['fontFamily'],
455
+ }
456
 
457
+ def handle_page_change(page_name, settings):
458
+ path = next((p['path'] for p in settings['pages'] if p['name'] == page_name), None)
459
+ if not path: return {}
460
+
461
+ page = get_page_by_path(path, settings)
462
+ html = generate_page_html(page, settings)
463
+
464
+ # Create dynamic controls for the selected page's content blocks
465
+ content_controls = []
466
+ for i, block in enumerate(page.get('contentBlocks', [])):
467
+ with gr.Accordion(f"{block.get('type', 'Block').capitalize()} Block #{i+1}", open=True):
468
+ if 'headline' in block:
469
+ gr.Textbox(value=block.get('headline'), label="Headline", elem_id=f"{path}_{block['id']}_headline")
470
+ if 'subheadline' in block:
471
+ gr.Textbox(value=block.get('subheadline'), label="Subheadline", elem_id=f"{path}_{block['id']}_subheadline")
472
+ if 'text' in block:
473
+ gr.Textbox(value=block.get('text'), label="Text", lines=4, elem_id=f"{path}_{block['id']}_text")
474
+ # Add more controls for other block types (features, paragraphs, etc.) if needed
475
+
476
+ return {
477
+ active_page_path_state: path,
478
+ html_preview: html,
479
+ code_preview: html,
480
+ page_content_controls: gr.update(value=content_controls)
481
+ }
482
+
483
+ def handle_setting_change(settings, path, *values):
484
+ # This is a simplified handler. It assumes the order of values matches the controls.
485
+ # A more robust solution would use elem_id to map values to settings.
486
+ settings['header']['title'] = values[0]
487
+ settings['footer']['text'] = values[1]
488
+ theme_keys = list(settings['theme'].keys())
489
+ for i, key in enumerate(theme_keys):
490
+ settings['theme'][key] = values[2 + i]
491
+
492
+ page = get_page_by_path(path, settings)
493
+ html = generate_page_html(page, settings)
494
+ return settings, html, html
495
+
496
+ def handle_content_change(request: gr.Request, settings, active_path, *content_values):
497
+ # A very basic content handler based on elem_id.
498
+ # Gradio's current API makes granular updates complex.
499
+ # This is a placeholder for a more robust implementation.
500
+
501
+ # Example of how one would update:
502
+ # changed_elem_id = request.elem_id
503
+ # new_value = content_values[??]
504
+ # path, block_id, field = changed_elem_id.split('_')
505
+ # page = get_page_by_path(path, settings)
506
+ # block = get_block_by_id(page, block_id)
507
+ # block[field] = new_value
508
+
509
+ page = get_page_by_path(active_path, settings)
510
+ html = generate_page_html(page, settings)
511
+ return settings, html, html
512
+
513
+ # --- Gradio Blocks ---
514
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="indigo"), css="""
515
+ .gradio-container { background-color: #0d1117; }
516
+ #logo-picker .thumbnail-item { height: 120px !important; width: 120px !important; }
517
+ """) as demo:
518
+ # --- State Management ---
519
+ website_settings_state = gr.State(DEFAULT_WEBSITE_SETTINGS)
520
+ active_page_path_state = gr.State("index.html")
521
+
522
+ # --- View: Welcome ---
523
+ with gr.Column(visible=True) as welcome_view:
524
+ 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.",)
525
+ start_button = gr.Button("Start Building Your Website", variant="primary")
526
+
527
+ # --- View: Questionnaire ---
528
+ with gr.Column(visible=False) as questionnaire_view:
529
+ gr.Markdown("# 📝 Build Your Website\nAnswer these questions to give the AI a creative brief for your project.")
530
+ error_box = gr.Textbox(label="Error", visible=False, interactive=False)
531
+
532
+ # Pre-filled answers for quick testing
533
+ default_answers = {
534
+ 'name': 'QuantumBank', 'purpose': 'A high-tech financial company exploring the intersection of quantum computing and finance.',
535
+ 'audience': 'Investors, researchers, and tech enthusiasts interested in fintech.', 'style': 'Dark, futuristic, and professional with a high-tech feel.',
536
+ 'colors': 'Deep blues, purples, with bright cyan accents.', 'theme': 'dark', 'pages': 'Home, About Us, Research, Philosophy, Contact',
537
+ 'homeMessage': 'QuantumBank: The Future of Finance is Here.', 'features': '- Quantum-secured transactions\n- AI-powered investment analysis\n- Decentralized financial instruments',
538
+ 'about': 'Founded by leading quantum physicists and financial experts, QuantumBank is pioneering the next generation of financial technology.',
539
+ '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.'
540
+ }
541
 
542
+ with gr.Accordion("Core Identity", open=True):
543
+ q_name = gr.Textbox(label="Website/Company Name", value=default_answers['name'])
544
+ q_purpose = gr.Textbox(label="Primary Purpose", lines=3, value=default_answers['purpose'])
545
+ q_audience = gr.Textbox(label="Target Audience", lines=3, value=default_answers['audience'])
546
+ with gr.Accordion("Aesthetics", open=True):
547
+ q_style = gr.Textbox(label="Overall Style/Vibe", value=default_answers['style'])
548
+ q_colors = gr.Textbox(label="Preferred Colors", value=default_answers['colors'])
549
+ q_theme = gr.Radio(label="Theme", choices=["dark", "light"], value=default_answers['theme'])
550
+ with gr.Accordion("Content & Structure", open=True):
551
+ q_pages = gr.Textbox(label="Required Pages", value=default_answers['pages'])
552
+ q_homeMessage = gr.Textbox(label="Home Page Message/Headline", value=default_answers['homeMessage'])
553
+ q_features = gr.Textbox(label="Key Features/Services", lines=3, value=default_answers['features'])
554
+ q_about = gr.Textbox(label="About Us Content", lines=4, value=default_answers['about'])
555
+ with gr.Accordion("Brand Voice & Final Touches", open=True):
556
+ q_tone = gr.Textbox(label="Tone of Voice", value=default_answers['tone'])
557
+ q_tagline = gr.Textbox(label="Tagline/Slogan", value=default_answers['tagline'])
558
+ q_extra = gr.Textbox(label="Additional Instructions", lines=3, value=default_answers['extra'])
559
+
560
+ 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]
561
+ generate_button = gr.Button("Generate My Website", variant="primary")
562
+
563
+ # --- View: Generating ---
564
+ with gr.Column(visible=False, elem_id="generating-view") as generating_view:
565
+ gr.Markdown("## Generating Your Website...")
566
+ status_text = gr.Textbox("Initializing...", label="Status", interactive=False)
567
+
568
+ # --- View: Logo Picker ---
569
+ with gr.Column(visible=False) as logo_picker_view:
570
+ gr.Markdown("## Choose Your Logo\nThe AI has generated these logo options. Pick your favorite to continue.")
571
+ logo_gallery = gr.Gallery(label="Logo Options", columns=5, object_fit="contain", elem_id="logo-picker")
572
+
573
+ # --- View: Preview & Editor ---
574
+ with gr.Row(visible=False) as preview_view:
575
+ with gr.Column(scale=1):
576
+ gr.Markdown("## 🛠️ Website Editor")
577
+ with gr.Tabs():
578
+ with gr.TabItem("Pages & Content"):
579
+ page_selector = gr.Radio(label="Select Page to Edit", choices=["Home"], value="Home")
580
+ with gr.Column() as page_content_controls:
581
+ gr.Markdown("Page content controls will appear here.")
582
+ with gr.TabItem("Global Settings"):
583
+ with gr.Accordion("Header & Footer", open=True):
584
+ header_title_input = gr.Textbox(label="Header: Site Title")
585
+ footer_text_input = gr.Textbox(label="Footer: Copyright Text")
586
+ with gr.TabItem("Theme"):
587
+ with gr.Accordion("Colors", open=True):
588
+ theme_app_bg_input = gr.ColorPicker(label="App Background")
589
+ theme_panel_bg_input = gr.ColorPicker(label="Panel Background")
590
+ theme_header_bg_input = gr.ColorPicker(label="Header Background")
591
+ theme_footer_bg_input = gr.ColorPicker(label="Footer Background")
592
+ theme_primary_text_input = gr.ColorPicker(label="Primary Text")
593
+ theme_secondary_text_input = gr.ColorPicker(label="Secondary Text")
594
+ theme_primary_accent_input = gr.ColorPicker(label="Primary Accent")
595
+ theme_secondary_accent_input = gr.ColorPicker(label="Secondary Accent")
596
+ with gr.Accordion("Font", open=True):
597
+ theme_font_family_input = gr.Dropdown(label="Font Family", choices=['font-sans', 'font-serif', 'font-mono'])
598
+ with gr.Column(scale=3):
599
+ with gr.Tabs():
600
+ with gr.TabItem("Live Preview"):
601
+ html_preview = gr.HTML(value="<p>Your website preview will appear here.</p>",)
602
+ with gr.TabItem("Embed Code"):
603
+ code_preview = gr.Code(language="html", label="HTML Code")
604
+
605
+ # --- Event Wiring ---
606
+ start_button.click(handle_start, outputs=[welcome_view, questionnaire_view])
607
+
608
+ generate_button.click(
609
+ handle_generate_layout,
610
+ inputs=questionnaire_inputs,
611
+ outputs=[
612
+ questionnaire_view, generating_view, status_text, error_box,
613
+ logo_picker_view, website_settings_state, active_page_path_state
614
+ ]
615
+ )
616
+
617
+ logo_gallery.select(
618
+ handle_logo_select,
619
+ inputs=[website_settings_state, active_page_path_state],
620
+ outputs=[
621
+ logo_picker_view, preview_view, website_settings_state, html_preview, code_preview,
622
+ page_selector, header_title_input, footer_text_input,
623
+ theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
624
+ theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
625
+ theme_secondary_accent_input, theme_font_family_input
626
+ ]
627
  )
628
 
629
+ page_selector.change(
630
+ handle_page_change,
631
+ inputs=[page_selector, website_settings_state],
632
+ outputs=[active_page_path_state, html_preview, code_preview, page_content_controls]
 
 
633
  )
634
 
635
+ # Consolidate setting controls for easier handling
636
+ setting_controls = [
637
+ header_title_input, footer_text_input,
638
+ theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
639
+ theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
640
+ theme_secondary_accent_input, theme_font_family_input
641
+ ]
642
+
643
+ for control in setting_controls:
644
+ control.change(
645
+ handle_setting_change,
646
+ inputs=[website_settings_state, active_page_path_state] + setting_controls,
647
+ outputs=[website_settings_state, html_preview, code_preview]
648
+ )
649
 
650
  return demo
651
 
652
+
653
  if __name__ == "__main__":
654
+ app = create_gradio_app()
655
+ # To run in a Hugging Face Space, the port must be 7860
656
+ app.launch(server_name="0.0.0.0", server_port=7860)