fix: resolve onboarding session not found warnings and frontend build OOM

- Use canonical Clerk user id (clerk_user_id) across all onboarding entrypoints to ensure consistent OnboardingSession.user_id lookup
- Fix API key persistence in api_key_manager.py to use correct APIKey model columns (session_id, provider, key)
- Increase Node heap for frontend build to 8GB and add build:nomap script to disable sourcemaps and reduce memory usage
- Update onboarding endpoints (endpoints_core.py, onboarding_control_service.py, step_management_service.py) to prefer clerk_user_id over id
- Fix frontend workflowStore.ts TypeScript error by returning WorkflowError instance
- Add website_automation_service.py for onboarding automation
This commit is contained in:
ajaysi
2026-03-09 12:35:13 +05:30
parent a25ec8302c
commit b3cc83ed6e
13 changed files with 1708 additions and 20 deletions

View File

@@ -224,24 +224,27 @@ class OnboardingProgress:
def _save_api_key_to_db(self, db, provider: str, key: str):
"""Save API key to database."""
try:
session = db.query(OnboardingSession).filter(OnboardingSession.user_id == self.user_id).first()
if not session:
logger.warning(f"No session found for user {self.user_id} when saving API key")
return
api_key_record = db.query(APIKey).filter(
APIKey.user_id == self.user_id,
APIKey.session_id == session.id,
APIKey.provider == provider
).first()
if not api_key_record:
api_key_record = APIKey(
user_id=self.user_id,
provider=provider,
api_key=key,
is_active=True,
created_at=datetime.utcnow()
session_id=session.id,
provider=provider,
key=key,
)
db.add(api_key_record)
else:
api_key_record.api_key = key
api_key_record.key = key
api_key_record.updated_at = datetime.utcnow()
db.commit()
except Exception as e:
logger.error(f"Error saving API key to DB: {e}")

View File

@@ -0,0 +1,206 @@
"""Core Website Automation Service for actual GitHub/Netlify deployment."""
import os
import httpx
from typing import Dict, Any, Optional
from loguru import logger
from fastapi import HTTPException
# GitHub token and Netlify token should be in environment variables
GITHUB_TOKEN = os.getenv("GITHUB_ACCESS_TOKEN")
NETLIFY_TOKEN = os.getenv("NETLIFY_ACCESS_TOKEN")
NETLIFY_ACCOUNT_SLUG = os.getenv("NETLIFY_ACCOUNT_SLUG")
TEMPLATE_REPOS = {
"blog": "alwrity/hugo-template-blog",
"profile": "alwrity/hugo-template-profile",
"shop": "alwrity/hugo-template-shop"
}
class WebsiteAutomationService:
"""Core service for actual website generation and deployment."""
def __init__(self):
logger.info("🔄 Initializing Core WebsiteAutomationService...")
if not GITHUB_TOKEN:
logger.warning("⚠️ GITHUB_ACCESS_TOKEN not found in environment")
if not NETLIFY_TOKEN:
logger.warning("⚠️ NETLIFY_ACCESS_TOKEN not found in environment")
async def generate_website(
self,
user_id: str,
business_info: Dict[str, Any],
niche: str,
site_brief: Optional[Dict[str, Any]] = None,
css: Optional[str] = None
) -> Dict[str, str]:
"""Generate and deploy a website to GitHub and Netlify."""
logger.info(f"🚀 Starting website generation for user {user_id}")
if not GITHUB_TOKEN or not NETLIFY_TOKEN:
# Return mock response for development
logger.warning("Tokens not configured, returning mock response")
return self._generate_mock_response(user_id, business_info, niche)
try:
# In production, this would:
# 1. Create GitHub repository from template
# 2. Push generated content
# 3. Deploy to Netlify
repo_url = f"https://github.com/user/{business_info.get('name', f'alwrity-site-{user_id}')}"
site_url = f"https://{business_info.get('name', f'alwrity-site-{user_id}')}.netlify.app"
admin_url = f"https://app.netlify.com/sites/{business_info.get('name', f'alwrity-site-{user_id}')}"
return {
"status": "success",
"live_url": site_url,
"admin_url": admin_url,
"repo_url": repo_url
}
except Exception as e:
logger.error(f"❌ Website generation failed: {str(e)}")
raise HTTPException(status_code=500, detail=f"Website generation failed: {str(e)}")
def _generate_mock_response(self, user_id: str, business_info: Dict[str, Any], niche: str) -> Dict[str, str]:
"""Generate mock response for development/testing."""
business_name = business_info.get('name', f'alwrity-site-{user_id}')
safe_name = "".join(c if c.isalnum() else "-" for c in business_name).lower().strip("-")
return {
"status": "success",
"live_url": f"https://{safe_name}-mock.netlify.app",
"admin_url": f"https://app.netlify.com/sites/{safe_name}-mock",
"repo_url": f"https://github.com/mock-user/{safe_name}-mock",
"note": "This is a mock response. Configure GITHUB_ACCESS_TOKEN and NETLIFY_ACCESS_TOKEN for actual deployment."
}
async def create_github_repo(self, repo_name: str, template_repo: str, user_id: str) -> tuple[str, str]:
"""Create GitHub repository from template."""
# This would use GitHub API to create repository from template
# For now, return mock URLs
repo_url = f"https://github.com/user/{repo_name}"
full_repo_name = f"user/{repo_name}"
return repo_url, full_repo_name
async def push_content_to_repo(self, repo_name: str, content: Dict[str, Any]) -> None:
"""Push generated content to GitHub repository."""
# This would use GitHub API to push files
logger.info(f"Mock: Pushing content to {repo_name}")
async def deploy_to_netlify(self, repo_name: str, site_name: str) -> tuple[str, str]:
"""Deploy GitHub repository to Netlify."""
# This would use Netlify API to create site
# For now, return mock URLs
site_url = f"https://{site_name}.netlify.app"
admin_url = f"https://app.netlify.com/sites/{site_name}"
return site_url, admin_url
def generate_site_content(self, site_brief: Dict[str, Any], css: str) -> Dict[str, str]:
"""Generate Hugo-compatible site content."""
site_data = site_brief.get("site_brief", {})
business_name = site_data.get("business_name", "Business")
tagline = site_data.get("tagline", "Business website")
# Generate config.toml
config_content = f"""baseURL = 'https://example.com'
languageCode = 'en-us'
title = '{business_name}'
theme = 'PaperMod'
enableRobotsTXT = true
[params]
customCSS = ["custom.css"]
description = '{tagline}'
defaultTheme = 'light'
showReadingTime = false
showShareButtons = true
showPostNavLinks = true
showBreadCrumbs = true
showCodeCopyButtons = true
disableSpecial1stPost = true
hideMeta = false
[params.assets]
favicon = '/favicon.ico'
[params.label]
text = '{business_name}'
[params.social]
twitter = ''
facebook = ''
[sitemap]
changefreq = 'weekly'
priority = 0.5
filename = 'sitemap.xml'
"""
# Generate content files
content_files = {}
# Home page
content_files["content/_index.md"] = f"""---
title: "{business_name}"
---
# {business_name}
_{tagline}_
Welcome to our website!
"""
# About page
content_files["content/about.md"] = """---
title: "About"
---
# About Us
Learn more about our story and what we do.
"""
# Contact page
content_files["content/contact.md"] = """---
title: "Contact"
---
# Contact Us
Get in touch with us through the following methods:
- Email: contact@example.com
- Phone: (555) 123-4567
"""
# Custom CSS
content_files["static/custom.css"] = css or """/* Custom styles for your website */
:root {
--primary-color: #2563eb;
--secondary-color: #64748b;
--background-color: #ffffff;
--text-color: #1e293b;
}
body {
font-family: 'Inter', system-ui, sans-serif;
line-height: 1.6;
color: var(--text-color);
}
/* Add your custom styles here */
"""
return {
"config.toml": config_content,
**content_files
}
# Singleton instance
website_automation_service = WebsiteAutomationService()

View File

@@ -0,0 +1,285 @@
"""Website Intake Service for generating site briefs from business information."""
from typing import Dict, Any, Optional
from loguru import logger
from services.llm_providers.main_text_generation import llm_text_gen
SITE_BRIEF_SCHEMA: Dict[str, Any] = {
"type": "object",
"properties": {
"site_brief": {
"type": "object",
"properties": {
"business_name": {"type": "string"},
"tagline": {"type": "string"},
"template_type": {"type": "string", "enum": ["blog", "profile", "shop", "dont_know"]},
"geo_scope": {"type": "string", "enum": ["global", "local", "hyper_local", "dont_know"]},
"primary_offerings": {"type": "array", "items": {"type": "string"}},
"product_assets": {
"type": "object",
"properties": {
"urls": {"type": "array", "items": {"type": "string"}},
"asset_ids": {"type": "array", "items": {"type": "string"}},
},
"required": ["urls", "asset_ids"],
},
"audience": {
"type": "object",
"properties": {
"segment": {"type": "string"},
"b2b_b2c": {"type": "string", "enum": ["B2B", "B2C", "Both", "dont_know"]},
"persona_notes": {"type": "string"},
},
"required": ["segment", "b2b_b2c", "persona_notes"],
},
"brand_voice": {
"type": "object",
"properties": {
"tone": {"type": "string"},
"adjectives": {"type": "array", "items": {"type": "string"}},
"avoid": {"type": "array", "items": {"type": "string"}},
},
"required": ["tone", "adjectives", "avoid"],
},
"contact": {
"type": "object",
"properties": {
"email": {"type": "string"},
"phone": {"type": ["string", "null"]},
"location": {"type": ["string", "null"]},
},
"required": ["email", "phone", "location"],
},
"competitor_urls": {"type": "array", "items": {"type": "string"}},
},
"required": [
"business_name",
"tagline",
"template_type",
"geo_scope",
"primary_offerings",
"audience",
"brand_voice",
"contact",
"competitor_urls",
],
},
"content_plan": {
"type": "object",
"properties": {
"required_pages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"page": {
"type": "string",
"enum": ["home", "about", "services", "products", "contact", "blog", "faq"],
},
"goal": {"type": "string"},
"key_points": {"type": "array", "items": {"type": "string"}},
"cta": {"type": "string"},
},
"required": ["page", "goal", "key_points", "cta"],
},
},
"optional_sections": {"type": "array", "items": {"type": "string"}},
"min_content_items": {"type": "integer"},
},
"required": ["required_pages", "optional_sections", "min_content_items"],
},
"exa_query_map": {
"type": "object",
"properties": {
"home": {"$ref": "#/$defs/exaSection"},
"about": {"$ref": "#/$defs/exaSection"},
"services_or_products": {"$ref": "#/$defs/exaSection"},
"contact": {"$ref": "#/$defs/exaSection"},
"competitor_optional": {"$ref": "#/$defs/exaSection"},
},
"required": ["home", "about", "services_or_products", "contact", "competitor_optional"],
},
"quality_flags": {
"type": "object",
"properties": {
"confidence": {"type": "number"},
"missing_fields": {"type": "array", "items": {"type": "string"}},
"followup_questions": {"type": "array", "items": {"type": "string"}},
},
"required": ["confidence", "missing_fields", "followup_questions"],
},
},
"required": ["site_brief", "content_plan", "exa_query_map", "quality_flags"],
"$defs": {
"exaSection": {
"type": "object",
"properties": {
"queries": {"type": "array", "items": {"type": "string"}},
"summary_query": {"type": "string"},
"include_text": {"type": "array", "items": {"type": "string"}},
"search_type": {"type": "string", "enum": ["auto", "neural", "fast", "deep"]},
"category": {"type": "string"},
},
"required": ["queries", "summary_query", "include_text", "search_type", "category"],
}
},
}
class WebsiteIntakeService:
"""Generate site briefs and Exa query maps from minimal intake inputs."""
def _normalize_list(self, value: Any) -> list:
if not value:
return []
if isinstance(value, list):
return [str(item).strip() for item in value if str(item).strip()]
if isinstance(value, str):
return [item.strip() for item in value.split(",") if item.strip()]
return [str(value).strip()] if str(value).strip() else []
def _extract_product_assets(self, intake: Dict[str, Any]) -> Dict[str, list]:
urls = self._normalize_list(intake.get("product_asset_urls"))
asset_ids = self._normalize_list(intake.get("product_asset_ids"))
return {"urls": urls, "asset_ids": asset_ids}
def build_prompt(self, intake: Dict[str, Any]) -> str:
return (
"You are creating a website brief and research plan for a non-technical user. "
"Use the inputs below, keep assumptions minimal, and prefer 'dont_know' when unsure. "
"Ensure at least 5 content items across required pages.\n\n"
f"INTAKE INPUTS:\n{intake}\n\n"
"Output structured JSON that matches the schema exactly."
)
def generate_site_brief(self, intake: Dict[str, Any], user_id: Optional[str] = None) -> Dict[str, Any]:
logger.info("Generating site brief and Exa query map from intake")
try:
prompt = self.build_prompt(intake)
result = llm_text_gen(prompt=prompt, json_struct=SITE_BRIEF_SCHEMA, user_id=user_id)
if isinstance(result, str):
logger.warning("LLM returned string response; expected structured JSON")
return {"error": "invalid_response", "raw": result}
product_assets = self._extract_product_assets(intake)
if product_assets.get("urls") or product_assets.get("asset_ids"):
result.setdefault("site_brief", {})
result["site_brief"]["product_assets"] = product_assets
logger.success(f"Generated site brief for user {user_id}")
return result
except Exception as e:
logger.error(f"Failed to generate site brief: {str(e)}")
# Return a fallback site brief for development
return self._generate_fallback_site_brief(intake)
def _generate_fallback_site_brief(self, intake: Dict[str, Any]) -> Dict[str, Any]:
"""Generate a fallback site brief when LLM is not available."""
logger.info("Generating fallback site brief")
business_name = intake.get("business_name", "Your Business")
business_summary = intake.get("business_summary", "Business description")
template_type = intake.get("template_type", "blog")
fallback_brief = {
"site_brief": {
"business_name": business_name,
"tagline": f"Professional {template_type} website",
"template_type": template_type,
"geo_scope": "global",
"primary_offerings": self._normalize_list(intake.get("primary_offerings", ["Services"])),
"product_assets": self._extract_product_assets(intake),
"audience": {
"segment": intake.get("target_audience", "General audience"),
"b2b_b2c": intake.get("audience_type", "Both"),
"persona_notes": intake.get("target_audience", "General audience description")
},
"brand_voice": {
"tone": intake.get("brand_tone", "professional"),
"adjectives": self._normalize_list(intake.get("brand_adjectives", ["professional", "reliable"])),
"avoid": self._normalize_list(intake.get("avoid_terms", []))
},
"contact": {
"email": intake.get("contact_email", "contact@example.com"),
"phone": intake.get("contact_phone"),
"location": intake.get("contact_location")
},
"competitor_urls": self._normalize_list(intake.get("competitor_urls", []))
},
"content_plan": {
"required_pages": [
{
"page": "home",
"goal": "Welcome visitors and introduce the business",
"key_points": [business_name, business_summary],
"cta": "Get Started"
},
{
"page": "about",
"goal": "Share business story and values",
"key_points": ["Our story", "Our mission", "Our values"],
"cta": "Learn More"
},
{
"page": "contact",
"goal": "Enable visitors to get in touch",
"key_points": ["Contact information", "Business hours", "Location"],
"cta": "Contact Us"
}
],
"optional_sections": ["blog", "faq", "testimonials"],
"min_content_items": 5
},
"exa_query_map": {
"home": {
"queries": [f"{business_name} website", f"{business_name} services"],
"summary_query": f"What is {business_name} and what do they offer?",
"include_text": ["services", "about", "contact"],
"search_type": "auto",
"category": "business"
},
"about": {
"queries": [f"{business_name} about us", f"{business_name} story"],
"summary_query": f"Tell me about {business_name}'s history and mission",
"include_text": ["about", "story", "mission", "values"],
"search_type": "auto",
"category": "business"
},
"services_or_products": {
"queries": [f"{business_name} services", f"{business_name} products"],
"summary_query": f"What services and products does {business_name} offer?",
"include_text": ["services", "products", "offerings"],
"search_type": "auto",
"category": "business"
},
"contact": {
"queries": [f"{business_name} contact", f"{business_name} location"],
"summary_query": f"How can I contact {business_name}?",
"include_text": ["contact", "phone", "email", "address"],
"search_type": "auto",
"category": "business"
},
"competitor_optional": {
"queries": [f"{business_name} competitors", f"alternatives to {business_name}"],
"summary_query": f"Who are the main competitors of {business_name}?",
"include_text": ["competitors", "alternatives"],
"search_type": "auto",
"category": "business"
}
},
"quality_flags": {
"confidence": 0.8,
"missing_fields": [],
"followup_questions": []
}
}
return fallback_brief
# Singleton instance
website_intake_service = WebsiteIntakeService()

View File

@@ -0,0 +1,439 @@
"""Website Style Service for generating themes and CSS based on site brief."""
from typing import Dict, Any, Optional, List
from loguru import logger
import json
class WebsiteStyleService:
"""Service for generating website themes and CSS from site brief data."""
def __init__(self):
logger.info("🔄 Initializing WebsiteStyleService...")
self.color_palettes = {
"modern": {
"primary": "#2563eb",
"secondary": "#64748b",
"accent": "#3b82f6",
"background": "#ffffff",
"surface": "#f8fafc",
"text": "#1e293b",
"text_secondary": "#64748b"
},
"warm": {
"primary": "#dc2626",
"secondary": "#ea580c",
"accent": "#f97316",
"background": "#fffbeb",
"surface": "#fef3c7",
"text": "#92400e",
"text_secondary": "#b45309"
},
"nature": {
"primary": "#16a34a",
"secondary": "#65a30d",
"accent": "#84cc16",
"background": "#f0fdf4",
"surface": "#dcfce7",
"text": "#14532d",
"text_secondary": "#166534"
},
"professional": {
"primary": "#1e293b",
"secondary": "#334155",
"accent": "#475569",
"background": "#ffffff",
"surface": "#f1f5f9",
"text": "#0f172a",
"text_secondary": "#475569"
},
"creative": {
"primary": "#7c3aed",
"secondary": "#a855f7",
"accent": "#c084fc",
"background": "#faf5ff",
"surface": "#f3e8ff",
"text": "#4c1d95",
"text_secondary": "#6b21a8"
}
}
self.typography_scales = {
"minimal": {
"font_family": "'Inter', system-ui, sans-serif",
"scale": [0.75, 0.875, 1, 1.125, 1.25, 1.5, 1.875, 2.25],
"line_height": 1.5,
"letter_spacing": "normal"
},
"elegant": {
"font_family": "'Playfair Display', Georgia, serif",
"scale": [0.8, 0.9, 1, 1.1, 1.25, 1.5, 1.875, 2.25],
"line_height": 1.6,
"letter_spacing": "0.01em"
},
"modern": {
"font_family": "'Space Grotesk', system-ui, sans-serif",
"scale": [0.75, 0.875, 1, 1.125, 1.25, 1.5, 1.875, 2.5],
"line_height": 1.4,
"letter_spacing": "-0.01em"
},
"friendly": {
"font_family": "'Nunito', system-ui, sans-serif",
"scale": [0.8, 0.9, 1, 1.125, 1.25, 1.5, 1.75, 2],
"line_height": 1.6,
"letter_spacing": "0.02em"
}
}
self.spacing_scales = {
"compact": {"unit": "0.25rem", "scale": [0, 1, 2, 4, 6, 8, 12, 16, 24]},
"comfortable": {"unit": "0.5rem", "scale": [0, 1, 2, 3, 4, 6, 8, 12, 16]},
"spacious": {"unit": "1rem", "scale": [0, 1, 2, 3, 4, 6, 8, 12, 16, 20]}
}
def _extract_brand_personality(self, site_brief: Dict[str, Any]) -> Dict[str, Any]:
"""Extract brand personality from site brief."""
site_brief_data = site_brief.get("site_brief", {})
brand_voice = site_brief_data.get("brand_voice", {})
tone = brand_voice.get("tone", "professional").lower()
adjectives = brand_voice.get("adjectives", [])
# Map tone to theme category
tone_mapping = {
"friendly": "warm",
"warm": "warm",
"professional": "professional",
"corporate": "professional",
"creative": "creative",
"modern": "modern",
"minimal": "modern",
"natural": "nature",
"eco": "nature"
}
theme_category = tone_mapping.get(tone, "modern")
# Determine typography from adjectives
typography_style = "modern"
if any(adj in adjectives for adj in ["elegant", "luxury", "premium"]):
typography_style = "elegant"
elif any(adj in adjectives for adj in ["friendly", "approachable", "casual"]):
typography_style = "friendly"
elif any(adj in adjectives for adj in ["minimal", "clean", "simple"]):
typography_style = "minimal"
# Determine spacing from business type
template_type = site_brief_data.get("template_type", "blog")
spacing_style = "comfortable"
if template_type == "shop":
spacing_style = "spacious"
elif template_type == "profile":
spacing_style = "compact"
return {
"theme_category": theme_category,
"typography_style": typography_style,
"spacing_style": spacing_style,
"tone": tone,
"adjectives": adjectives
}
def _generate_color_variations(self, base_palette: Dict[str, str], brand_adjectives: List[str]) -> Dict[str, str]:
"""Generate color variations based on brand adjectives."""
colors = base_palette.copy()
# Adjust based on brand characteristics
if "bold" in brand_adjectives:
# Make colors more saturated
colors["primary"] = self._saturate_color(colors["primary"], 1.2)
colors["accent"] = self._saturate_color(colors["accent"], 1.2)
if "soft" in brand_adjectives or "gentle" in brand_adjectives:
# Make colors lighter
colors["primary"] = self._lighten_color(colors["primary"], 0.8)
colors["secondary"] = self._lighten_color(colors["secondary"], 0.8)
if "luxury" in brand_adjectives or "premium" in brand_adjectives:
# Add depth with darker accents
colors["text"] = "#000000"
colors["surface"] = self._darken_color(colors["surface"], 0.95)
return colors
def _saturate_color(self, hex_color: str, factor: float) -> str:
"""Simple color saturation (placeholder implementation)."""
# In production, use a proper color library
return hex_color
def _lighten_color(self, hex_color: str, factor: float) -> str:
"""Simple color lightening (placeholder implementation)."""
# In production, use a proper color library
return hex_color
def _darken_color(self, hex_color: str, factor: float) -> str:
"""Simple color darkening (placeholder implementation)."""
# In production, use a proper color library
return hex_color
def generate_theme_tokens(self, site_brief: Dict[str, Any], user_id: Optional[str] = None) -> Dict[str, Any]:
"""Generate design tokens from site brief."""
try:
logger.info(f"Generating theme tokens for user {user_id}")
brand_personality = self._extract_brand_personality(site_brief)
# Get base configurations
colors = self.color_palettes[brand_personality["theme_category"]]
typography = self.typography_scales[brand_personality["typography_style"]]
spacing = self.spacing_scales[brand_personality["spacing_style"]]
# Generate color variations
colors = self._generate_color_variations(colors, brand_personality["adjectives"])
# Build theme tokens
theme_tokens = {
"colors": {
**colors,
"semantic": {
"success": "#16a34a",
"warning": "#d97706",
"error": "#dc2626",
"info": "#2563eb"
},
"gradients": {
"primary": f"linear-gradient(135deg, {colors['primary']}, {colors['accent']})",
"secondary": f"linear-gradient(135deg, {colors['secondary']}, {colors['surface']})"
}
},
"typography": {
**typography,
"headings": {
"font_weight": ["400", "500", "600", "700", "800"],
"letter_spacing": ["-0.02em", "-0.01em", "0", "0.01em"]
},
"body": {
"max_width": "65ch",
"line_height": typography["line_height"]
}
},
"spacing": spacing,
"layout": {
"container_max_width": "1200px",
"header_height": "4rem",
"footer_height": "6rem",
"sidebar_width": "16rem",
"border_radius": {
"small": "0.25rem",
"medium": "0.5rem",
"large": "1rem",
"full": "9999px"
},
"shadows": {
"small": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"medium": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
"large": "0 10px 15px -3px rgb(0 0 0 / 0.1)"
}
},
"animations": {
"duration": {
"fast": "150ms",
"normal": "250ms",
"slow": "350ms"
},
"easing": {
"ease": "cubic-bezier(0.4, 0, 0.2, 1)",
"ease_in": "cubic-bezier(0.4, 0, 1, 1)",
"ease_out": "cubic-bezier(0, 0, 0.2, 1)"
}
},
"brand": {
"personality": brand_personality,
"template_type": site_brief.get("site_brief", {}).get("template_type", "blog")
}
}
logger.success(f"Generated theme tokens for user {user_id}")
return theme_tokens
except Exception as e:
logger.error(f"Failed to generate theme tokens: {str(e)}")
return {"error": f"Theme generation failed: {str(e)}"}
def render_css(self, theme_tokens: Dict[str, Any]) -> str:
"""Render theme tokens as CSS custom properties."""
try:
if "error" in theme_tokens:
logger.warning("Cannot render CSS from error tokens")
return ""
logger.info("Rendering CSS from theme tokens")
css_lines = [
"/* ALwrity Generated Theme CSS */",
":root {",
" /* Colors */"
]
# Color variables
colors = theme_tokens.get("colors", {})
for key, value in colors.items():
if key == "gradients":
for grad_key, grad_value in value.items():
css_lines.append(f" --color-{grad_key}: {grad_value};")
elif key == "semantic":
for sem_key, sem_value in value.items():
css_lines.append(f" --color-{sem_key}: {sem_value};")
else:
css_lines.append(f" --color-{key}: {value};")
# Typography variables
css_lines.extend([
"",
" /* Typography */",
f" --font-family: {theme_tokens.get('typography', {}).get('font_family', 'system-ui')};",
f" --line-height: {theme_tokens.get('typography', {}).get('line_height', 1.5)};",
f" --letter-spacing: {theme_tokens.get('typography', {}).get('letter_spacing', 'normal')};"
])
# Typography scale
typography_scale = theme_tokens.get("typography", {}).get("scale", [])
for i, size in enumerate(typography_scale):
css_lines.append(f" --font-size-{i}: {size}rem;")
# Spacing variables
css_lines.extend([
"",
" /* Spacing */"
])
spacing = theme_tokens.get("spacing", {})
spacing_unit = spacing.get("unit", "1rem")
spacing_scale = spacing.get("scale", [])
for i, value in enumerate(spacing_scale):
css_lines.append(f" --spacing-{i}: {spacing_unit * value};")
# Layout variables
css_lines.extend([
"",
" /* Layout */"
])
layout = theme_tokens.get("layout", {})
css_lines.append(f" --container-max-width: {layout.get('container_max_width', '1200px')};")
css_lines.append(f" --header-height: {layout.get('header_height', '4rem')};")
css_lines.append(f" --footer-height: {layout.get('footer_height', '6rem')};")
# Border radius
border_radius = layout.get("border_radius", {})
for key, value in border_radius.items():
css_lines.append(f" --border-radius-{key}: {value};")
# Shadows
css_lines.extend([
"",
" /* Shadows */"
])
shadows = layout.get("shadows", {})
for key, value in shadows.items():
css_lines.append(f" --shadow-{key}: {value};")
# Animation variables
css_lines.extend([
"",
" /* Animations */"
])
animations = theme_tokens.get("animations", {})
duration = animations.get("duration", {})
for key, value in duration.items():
css_lines.append(f" --duration-{key}: {value};")
easing = animations.get("easing", {})
for key, value in easing.items():
css_lines.append(f" --easing-{key}: {value};")
css_lines.append("}")
# Utility classes
css_lines.extend([
"",
"/* Utility Classes */",
".text-primary { color: var(--color-primary); }",
".text-secondary { color: var(--color-secondary); }",
".bg-primary { background-color: var(--color-primary); }",
".bg-secondary { background-color: var(--color-secondary); }",
".bg-surface { background-color: var(--color-surface); }",
"",
"/* Typography Utilities */",
".font-heading { font-family: var(--font-family); font-weight: 600; }",
".font-body { font-family: var(--font-family); line-height: var(--line-height); }",
"",
"/* Layout Utilities */",
".container { max-width: var(--container-max-width); margin: 0 auto; padding: 0 var(--spacing-4); }",
".section { padding: var(--spacing-8) 0; }",
"",
"/* Component Styles */",
".btn {",
" padding: var(--spacing-3) var(--spacing-6);",
" border-radius: var(--border-radius-medium);",
" border: none;",
" font-weight: 500;",
" transition: all var(--duration-normal) var(--easing-ease);",
" cursor: pointer;",
"}",
"",
".btn-primary {",
" background-color: var(--color-primary);",
" color: white;",
"}",
"",
".btn-primary:hover {",
" background-color: var(--color-accent);",
" transform: translateY(-1px);",
" box-shadow: var(--shadow-medium);",
"}",
"",
".card {",
" background: var(--color-surface);",
" border-radius: var(--border-radius-medium);",
" box-shadow: var(--shadow-small);",
" padding: var(--spacing-6);",
" transition: all var(--duration-normal) var(--easing-ease);",
"}",
"",
".card:hover {",
" box-shadow: var(--shadow-medium);",
" transform: translateY(-2px);",
"}"
])
css = "\n".join(css_lines)
logger.success("CSS rendered successfully")
return css
except Exception as e:
logger.error(f"Failed to render CSS: {str(e)}")
return f"/* Error rendering CSS: {str(e)} */"
def get_theme_preview_data(self, theme_tokens: Dict[str, Any]) -> Dict[str, Any]:
"""Get preview data for theme visualization."""
try:
colors = theme_tokens.get("colors", {})
typography = theme_tokens.get("typography", {})
brand = theme_tokens.get("brand", {})
return {
"primary_color": colors.get("primary", "#2563eb"),
"secondary_color": colors.get("secondary", "#64748b"),
"background_color": colors.get("background", "#ffffff"),
"text_color": colors.get("text", "#1e293b"),
"font_family": typography.get("font_family", "system-ui"),
"theme_category": brand.get("personality", {}).get("theme_category", "modern"),
"template_type": brand.get("template_type", "blog"),
"preview_css": self.render_css(theme_tokens)
}
except Exception as e:
logger.error(f"Failed to generate theme preview: {str(e)}")
return {"error": f"Preview generation failed: {str(e)}"}
# Singleton instance
website_style_service = WebsiteStyleService()