import gradio as gr
import os
# Updated import to use the new 'google.genai' SDK
from google import genai
from google.genai import types
import json
from PIL import Image
from io import BytesIO
import base64
import uuid
import concurrent.futures
import re
import tempfile
# --- Configuration and Initialization ---
# Get the API key from environment variables
GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise ValueError("GEMINI_API_KEY not found in environment variables. Please set it in your Hugging Face Space secrets.")
# Initialize Gemini client
client = genai.Client(api_key=GEMINI_API_KEY)
# --- Default Data Structures (Ported from TypeScript) ---
DEFAULT_WEBSITE_SETTINGS = {
"theme": {
"appBg": "#111827", "panelBg": "#1f2937", "headerBg": "#1f2937",
"footerBg": "#1f2937", "primaryTextColor": "#f9fafb", "secondaryTextColor": "#d1d5db",
"primaryAccent": "#4f46e5", "secondaryAccent": "#10b981", "fontFamily": "font-sans",
},
"header": { "title": "My Awesome Site", "logoUrl": "", "navLinks": [] },
"footer": {
"text": "© 2024 My Awesome Site. All rights reserved.",
"links": [
{"text": "Privacy Policy", "href": "#"},
{"text": "Terms of Service", "href": "#"},
],
},
"pages": [{
"path": "index.html", "name": "Home",
"contentBlocks": [{
"id": str(uuid.uuid4()), "type": "hero", "headline": "Welcome to Your AI-Generated Website",
"subheadline": "Describe your vision and watch it come to life.",
"bgImage": "", "headlineColor": "#ffffff", "subheadlineColor": "#d1d5db",
}],
}],
}
# --- Core AI and Helper Functions ---
def create_prompt_from_answers(answers):
# This prompt is a direct port from the React app, instructing the AI on how to generate the website layout.
return f"""
You are a world-class creative director, UI/UX designer, and content strategist tasked with building a complete multi-page website.
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.
**User's Website Requirements:**
- **Website Name:** {answers.get('name', 'AI Generated Website')}
- **Core Purpose:** {answers.get('purpose', 'Not specified')}
- **Target Audience:** {answers.get('audience', 'General audience')}
- **Desired Style/Vibe:** {answers.get('style', 'A modern, clean design')}
- **Color Palette:** {answers.get('colors', 'A balanced color scheme based on the style')}
- **Theme Preference:** {answers.get('theme', 'dark')}
- **Required Pages:** {answers.get('pages', 'Home, About, Contact')}
- **Home Page Headline/Message:** {answers.get('homeMessage', f"Welcome to {answers.get('name', 'Our Website')}")}
- **Key Features/Services:** {answers.get('features', 'Feature 1, Feature 2, Feature 3')}
- **About Us Content:** {answers.get('about', 'A brief description of the company.')}
- **Contact Info:** {answers.get('contact', 'A contact form.')}
- **Brand Tone of Voice:** {answers.get('tone', 'Professional and friendly')}
- **Tagline:** {answers.get('tagline', '')}
- **Additional Instructions:** {answers.get('extra', 'None')}
**Your Mission:**
Based *only* on the requirements above, generate a single, comprehensive JSON object for this website. Adhere strictly to the provided JSON schema.
**Mandatory Requirements:**
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).
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')}).
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')}).
4. **Creative Asset Prompts:** Generate 1 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.
5. **Navigation:** Populate the 'navLinks' array in the header settings. The links must correspond to the pages you create.
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.
7. **Image Limit:** The total number of `blockImagePrompts` you generate must **NOT exceed 9**. Be selective and only create images for the most important content blocks to stay within this limit.
"""
def generate_image(prompt: str, aspect_ratio: str):
"""Generates an image using Gemini and returns a file path and a base64 data URL."""
print(f"Generating image for prompt: {prompt}")
try:
# Refactored to use the new client.models.generate_content method
full_prompt = f'Generate a single, high-quality image. Description: "{prompt}". The image should have a {aspect_ratio} aspect ratio.'
response = client.models.generate_content(
model="gemini-2.0-flash-preview-image-generation",
contents=full_prompt,
config=types.GenerateContentConfig(response_modalities=['TEXT', 'IMAGE'])
)
image_part = next((part for part in response.candidates[0].content.parts if part.inline_data), None)
if image_part:
mime_type = image_part.inline_data.mime_type
image_data = image_part.inline_data.data
# Create base64 URL
base64_data = base64.b64encode(image_data).decode("utf-8")
base64_url = f"data:{mime_type};base64,{base64_data}"
# Save to temp file
extension = mime_type.split('/')[-1]
with tempfile.NamedTemporaryFile(delete=False, suffix=f".{extension}") as temp_file:
temp_file.write(image_data)
temp_filepath = temp_file.name
return temp_filepath, base64_url
else:
print(f"Warning: Image generation failed for prompt: {prompt}. No image data in response.")
return None, None
except Exception as e:
print(f"Error generating image for prompt '{prompt}': {e}")
return None, None
def generate_page_html(page, settings):
"""Generates the HTML for a single page based on the website settings."""
if not page or not settings:
return "
Error: Page or settings not found.
"
theme = settings.get("theme", {})
header = settings.get("header", {})
footer = settings.get("footer", {})
nav_links = header.get("navLinks", [])
# Header HTML
header_html = f"""
"""
# Content Blocks HTML
main_content_html = ""
for block in page.get("contentBlocks", []):
block_type = block.get("type")
if block_type == 'hero':
main_content_html += f"""
{block.get('headline', '')}
{block.get('subheadline', '')}
"""
elif block_type == 'textWithImage':
text_html = (block.get('text', '') or '').replace('\n', '
')
main_content_html += f"""
{block.get('headline', '')}
{text_html}
"""
elif block_type == 'featureList':
features_html = ''.join([f"""
{f.get('title', '')}
{f.get('description', '')}
""" for f in block.get('features', [])])
main_content_html += f"""
{block.get('headline', '')}
{features_html}
"""
elif block_type == 'text':
paragraphs_html = ''.join([f'{p}
' for p in block.get('paragraphs', [])])
main_content_html += f"""
{block.get('headline', '')}
{paragraphs_html}
"""
elif block_type == 'form':
fields_html = ''.join([f"""
""" for f in block.get('fields', [])])
main_content_html += f"""
{block.get('headline', '')}
"""
# Footer HTML
footer_html = f"""
"""
# Final Page Assembly
return f"""
{header.get('title', '')} - {page.get('name', '')}
{header_html}
{main_content_html}
{footer_html}
""".strip()
# --- Gradio UI Definition ---
def create_gradio_app():
# --- UI Helper Functions ---
def get_page_by_path(path, settings):
return next((p for p in settings.get("pages", []) if p.get("path") == path), None)
def get_block_by_id(page, block_id):
if not page: return None
return next((b for b in page.get("contentBlocks", []) if b.get("id") == block_id), None)
# --- Event Handlers ---
def handle_start():
return {
welcome_view: gr.update(visible=False),
questionnaire_view: gr.update(visible=True),
}
def handle_generate_layout(*questionnaire_answers):
yield {
questionnaire_view: gr.update(visible=False),
generating_view: gr.update(visible=True),
status_text: "Generating website structure..."
}
# Map flat answer list to dictionary
question_ids = [
'name', 'purpose', 'audience', 'style', 'colors', 'theme', 'pages',
'homeMessage', 'features', 'about', 'tone', 'tagline', 'extra'
]
answers = dict(zip(question_ids, questionnaire_answers))
# --- Step 1: Generate Layout ---
try:
# Refactored to use the new client.models.generate_content method
prompt = create_prompt_from_answers(answers)
schema = {
"type": "object",
"properties": {
"websiteSettings": {
"type": "object",
"properties": {
"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"}}},
"header": {"type": "object", "properties": {"title": {"type": "string"},"navLinks": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
"footer": {"type": "object", "properties": {"text": {"type": "string"},"links": {"type": "array","items": {"type": "object","properties": {"text": {"type": "string"},"href": {"type": "string"}}}}}},
"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"}}}}}}}},
},
},
"imagePrompts": {
"type": "object",
"properties": {
"logoPrompts": {"type": "array", "items": {"type": "string"}},
"blockImagePrompts": {"type": "array", "items": {"type": "object", "properties": {"pagePath": {"type": "string"},"blockId": {"type": "string"},"prompt": {"type": "string"}}}}
}
}
}
}
safety_settings = [
{
"category": types.HarmCategory.HARM_CATEGORY_HARASSMENT,
"threshold": types.HarmBlockThreshold.BLOCK_NONE,
},
{
"category": types.HarmCategory.HARM_CATEGORY_HATE_SPEECH,
"threshold": types.HarmBlockThreshold.BLOCK_NONE,
},
{
"category": types.HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
"threshold": types.HarmBlockThreshold.BLOCK_NONE,
},
{
"category": types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
"threshold": types.HarmBlockThreshold.BLOCK_NONE,
},
]
response = client.models.generate_content(
model='gemini-2.5-flash',
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=schema,
safety_settings=safety_settings
)
)
design_plan = json.loads(response.text)
except Exception as e:
print(f"Layout generation failed: {e}")
yield {
generating_view: gr.update(visible=False),
questionnaire_view: gr.update(visible=True),
error_box: gr.update(value=f"Error generating layout: {e}", visible=True)
}
return
new_settings = design_plan['websiteSettings']
image_prompts = design_plan['imagePrompts']
# Add unique IDs and empty image fields to the settings
original_id_map = {}
for page in new_settings.get('pages', []):
new_blocks = []
for block in page.get('contentBlocks', []):
new_id = str(uuid.uuid4())
original_id = block.get('id')
if original_id:
original_id_map[original_id] = new_id
block['id'] = new_id
if block.get('type') == 'hero':
block['bgImage'] = ''
if block.get('type') == 'textWithImage':
block['image'] = ''
new_blocks.append(block)
page['contentBlocks'] = new_blocks
new_settings['header']['logoUrl'] = ''
active_page_path = new_settings['pages'][0]['path'] if new_settings.get('pages') else ''
# --- Step 2: Generate Images in Parallel ---
tasks = []
logo_prompts = [re.sub(r'import\s.*', '', p).strip() for p in image_prompts.get('logoPrompts', [])]
block_prompts = image_prompts.get('blockImagePrompts', [])
if logo_prompts:
yield {status_text: f"Generating {len(logo_prompts)} logos..."}
for prompt in logo_prompts:
tasks.append(("logo", prompt))
if block_prompts:
yield {status_text: f"Generating {len(block_prompts)} content images..."}
for p_info in block_prompts:
p_info['prompt'] = re.sub(r'import\s.*', '', p_info.get('prompt', '')).strip()
tasks.append(("block", p_info))
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
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}
logo_filepaths = []
logo_base64s = []
for i, future in enumerate(concurrent.futures.as_completed(future_to_task)):
task_type, task_data = future_to_task[future]
try:
filepath, base64_url = future.result()
if not filepath or not base64_url: continue
if task_type == "logo":
logo_filepaths.append(filepath)
logo_base64s.append(base64_url)
elif task_type == "block":
original_block_id = task_data['blockId']
new_block_id = original_id_map.get(original_block_id)
if not new_block_id: continue
page_path = task_data['pagePath']
page_to_update = get_page_by_path(page_path, new_settings)
if not page_to_update: continue
block_to_update = get_block_by_id(page_to_update, new_block_id)
if not block_to_update: continue
key_to_update = 'bgImage' if block_to_update.get('type') == 'hero' else 'image'
block_to_update[key_to_update] = base64_url
yield {status_text: f"Processing images... ({i+1}/{len(tasks)})"}
except Exception as exc:
print(f'{task_data} generated an exception: {exc}')
yield {
generating_view: gr.update(visible=False),
logo_picker_view: gr.update(visible=True),
logo_gallery: gr.update(value=logo_filepaths if logo_filepaths else []),
website_settings_state: new_settings,
active_page_path_state: active_page_path,
generated_logos_state: logo_base64s
}
def handle_logo_select(evt: gr.SelectData, settings, active_path, all_logo_base64s):
selected_index = evt.index
logo_url = all_logo_base64s[selected_index]
settings['header']['logoUrl'] = logo_url
page = get_page_by_path(active_path, settings)
html = generate_page_html(page, settings)
# Update controls with the new settings
page_names = [p['name'] for p in settings['pages']]
active_page_name = get_page_by_path(active_path, settings)['name']
return {
logo_picker_view: gr.update(visible=False),
preview_view: gr.update(visible=True),
website_settings_state: settings,
html_preview: html,
code_preview: html,
# Update controls
page_selector: gr.update(choices=page_names, value=active_page_name),
header_title_input: settings['header']['title'],
footer_text_input: settings['footer']['text'],
theme_app_bg_input: settings['theme']['appBg'],
theme_panel_bg_input: settings['theme']['panelBg'],
theme_header_bg_input: settings['theme']['headerBg'],
theme_footer_bg_input: settings['theme']['footerBg'],
theme_primary_text_input: settings['theme']['primaryTextColor'],
theme_secondary_text_input: settings['theme']['secondaryTextColor'],
theme_primary_accent_input: settings['theme']['primaryAccent'],
theme_secondary_accent_input: settings['theme']['secondaryAccent'],
theme_font_family_input: settings['theme']['fontFamily'],
}
def handle_page_change(page_name, settings):
path = next((p['path'] for p in settings['pages'] if p['name'] == page_name), None)
if not path: return {}
page = get_page_by_path(path, settings)
html = generate_page_html(page, settings)
# Create dynamic controls for the selected page's content blocks
content_controls = []
# This part is complex with Gradio's current API for dynamic updates based on state.
# For this example, we'll keep it simple and not dynamically generate inputs,
# relying on a more complex (and currently absent) handle_content_change function.
return {
active_page_path_state: path,
html_preview: html,
code_preview: html,
# page_content_controls: gr.update(value=content_controls) # Dynamic controls are tricky
}
def handle_setting_change(settings, path, *values):
# This is a simplified handler. It assumes the order of values matches the controls.
# A more robust solution would use elem_id to map values to settings.
settings['header']['title'] = values[0]
settings['footer']['text'] = values[1]
theme_keys = list(settings['theme'].keys())
for i, key in enumerate(theme_keys):
settings['theme'][key] = values[2 + i]
page = get_page_by_path(path, settings)
html = generate_page_html(page, settings)
return settings, html, html
def handle_content_change(request: gr.Request, settings, active_path, *content_values):
# A very basic content handler based on elem_id.
# Gradio's current API makes granular updates complex.
# This is a placeholder for a more robust implementation.
# Example of how one would update:
# changed_elem_id = request.elem_id
# new_value = content_values[??]
# path, block_id, field = changed_elem_id.split('_')
# page = get_page_by_path(path, settings)
# block = get_block_by_id(page, block_id)
# block[field] = new_value
page = get_page_by_path(active_path, settings)
html = generate_page_html(page, settings)
return settings, html, html
# --- Gradio Blocks ---
with gr.Blocks(theme=gr.themes.Default(primary_hue="blue", secondary_hue="indigo"), css="""
.gradio-container { background-color: #0d1117; }
#logo-picker .thumbnail-item { height: 120px !important; width: 120px !important; }
""") as demo:
# --- State Management ---
website_settings_state = gr.State(DEFAULT_WEBSITE_SETTINGS)
active_page_path_state = gr.State("index.html")
generated_logos_state = gr.State([])
# --- View: Welcome ---
with gr.Column(visible=True) as welcome_view:
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.",)
start_button = gr.Button("Start Building Your Website", variant="primary")
# --- View: Questionnaire ---
with gr.Column(visible=False) as questionnaire_view:
gr.Markdown("# 📝 Build Your Website\nAnswer these questions to give the AI a creative brief for your project.")
error_box = gr.Textbox(label="Error", visible=False, interactive=False)
# Pre-filled answers for quick testing
default_answers = {
'name': 'QuantumBank', 'purpose': 'A high-tech financial company exploring the intersection of quantum computing and finance.',
'audience': 'Investors, researchers, and tech enthusiasts interested in fintech.', 'style': 'Dark, futuristic, and professional with a high-tech feel.',
'colors': 'Deep blues, purples, with bright cyan accents.', 'theme': 'dark', 'pages': 'Home, About Us, Research, Philosophy, Contact',
'homeMessage': 'QuantumBank: The Future of Finance is Here.', 'features': '- Quantum-secured transactions\n- AI-powered investment analysis\n- Decentralized financial instruments',
'about': 'Founded by leading quantum physicists and financial experts, QuantumBank is pioneering the next generation of financial technology.',
'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.'
}
with gr.Accordion("Core Identity", open=True):
q_name = gr.Textbox(label="Website/Company Name", value=default_answers['name'])
q_purpose = gr.Textbox(label="Primary Purpose", lines=3, value=default_answers['purpose'])
q_audience = gr.Textbox(label="Target Audience", lines=3, value=default_answers['audience'])
with gr.Accordion("Aesthetics", open=True):
q_style = gr.Textbox(label="Overall Style/Vibe", value=default_answers['style'])
q_colors = gr.Textbox(label="Preferred Colors", value=default_answers['colors'])
q_theme = gr.Radio(label="Theme", choices=["dark", "light"], value=default_answers['theme'])
with gr.Accordion("Content & Structure", open=True):
q_pages = gr.Textbox(label="Required Pages", value=default_answers['pages'])
q_homeMessage = gr.Textbox(label="Home Page Message/Headline", value=default_answers['homeMessage'])
q_features = gr.Textbox(label="Key Features/Services", lines=3, value=default_answers['features'])
q_about = gr.Textbox(label="About Us Content", lines=4, value=default_answers['about'])
with gr.Accordion("Brand Voice & Final Touches", open=True):
q_tone = gr.Textbox(label="Tone of Voice", value=default_answers['tone'])
q_tagline = gr.Textbox(label="Tagline/Slogan", value=default_answers['tagline'])
q_extra = gr.Textbox(label="Additional Instructions", lines=3, value=default_answers['extra'])
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]
generate_button = gr.Button("Generate My Website", variant="primary")
# --- View: Generating ---
with gr.Column(visible=False, elem_id="generating-view") as generating_view:
gr.Markdown("## ⏳ Generating Your Website...")
status_text = gr.Textbox("Initializing...", label="Status", interactive=False)
# --- View: Logo Picker ---
with gr.Column(visible=False) as logo_picker_view:
gr.Markdown("## ✨ Choose Your Logo\nThe AI has generated these logo options. Pick your favorite to continue.")
logo_gallery = gr.Gallery(label="Logo Options", columns=1, object_fit="contain", elem_id="logo-picker")
# --- View: Preview & Editor ---
with gr.Row(visible=False) as preview_view:
with gr.Column(scale=1):
gr.Markdown("## 🛠️ Website Editor")
with gr.Tabs():
with gr.TabItem("Pages & Content"):
page_selector = gr.Radio(label="Select Page to Edit", choices=["Home"], value="Home")
# Dynamic content controls are complex in Gradio, so we'll omit them for simplicity
gr.Markdown("*(Content editing is simplified for this demo. Changes can be made in the generated code.)*")
with gr.TabItem("Global Settings"):
with gr.Accordion("Header & Footer", open=True):
header_title_input = gr.Textbox(label="Header: Site Title")
footer_text_input = gr.Textbox(label="Footer: Copyright Text")
with gr.TabItem("Theme"):
with gr.Accordion("Colors", open=True):
theme_app_bg_input = gr.ColorPicker(label="App Background")
theme_panel_bg_input = gr.ColorPicker(label="Panel Background")
theme_header_bg_input = gr.ColorPicker(label="Header Background")
theme_footer_bg_input = gr.ColorPicker(label="Footer Background")
theme_primary_text_input = gr.ColorPicker(label="Primary Text")
theme_secondary_text_input = gr.ColorPicker(label="Secondary Text")
theme_primary_accent_input = gr.ColorPicker(label="Primary Accent")
theme_secondary_accent_input = gr.ColorPicker(label="Secondary Accent")
with gr.Accordion("Font", open=True):
theme_font_family_input = gr.Dropdown(label="Font Family", choices=['font-sans', 'font-serif', 'font-mono'])
with gr.Column(scale=3):
with gr.Tabs():
with gr.TabItem("Live Preview"):
html_preview = gr.HTML(value="Your website preview will appear here.
",)
with gr.TabItem("Embed Code"):
code_preview = gr.Code(language="html", label="HTML Code")
# --- Event Wiring ---
start_button.click(handle_start, outputs=[welcome_view, questionnaire_view])
generate_button.click(
handle_generate_layout,
inputs=questionnaire_inputs,
outputs=[
questionnaire_view, generating_view, status_text, error_box,
logo_picker_view, logo_gallery, website_settings_state, active_page_path_state,
generated_logos_state
]
)
logo_gallery.select(
handle_logo_select,
inputs=[website_settings_state, active_page_path_state, generated_logos_state],
outputs=[
logo_picker_view, preview_view, website_settings_state, html_preview, code_preview,
page_selector, header_title_input, footer_text_input,
theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
theme_secondary_accent_input, theme_font_family_input
]
)
page_selector.change(
handle_page_change,
inputs=[page_selector, website_settings_state],
outputs=[active_page_path_state, html_preview, code_preview]
)
# Consolidate setting controls for easier handling
setting_controls = [
header_title_input, footer_text_input,
theme_app_bg_input, theme_panel_bg_input, theme_header_bg_input, theme_footer_bg_input,
theme_primary_text_input, theme_secondary_text_input, theme_primary_accent_input,
theme_secondary_accent_input, theme_font_family_input
]
for control in setting_controls:
control.change(
handle_setting_change,
inputs=[website_settings_state, active_page_path_state] + setting_controls,
outputs=[website_settings_state, html_preview, code_preview]
)
return demo
if __name__ == "__main__":
app = create_gradio_app()
# To run in a Hugging Face Space, the port must be 7860
app.launch(server_name="0.0.0.0", server_port=7860)