Spaces:
No application file
No application file
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,336 +1,656 @@
|
|
| 1 |
|
| 2 |
import gradio as gr
|
| 3 |
-
import google as genai
|
| 4 |
import os
|
| 5 |
-
import
|
| 6 |
-
import
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
#
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 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 |
-
|
| 42 |
-
|
| 43 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 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 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
"""
|
| 193 |
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
</div>
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 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 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
|
|
|
| 240 |
|
| 241 |
-
|
| 242 |
-
yield { chatbot: history, text_to_speak: "", agent_to_speak: "" }
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
)
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
outputs=[chatbot],
|
| 326 |
-
api_name=False
|
| 327 |
)
|
| 328 |
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
return demo
|
| 332 |
|
| 333 |
-
|
| 334 |
if __name__ == "__main__":
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
| 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)
|