Files
ALwrity/backend/services/onboarding/website_style_service.py
ajaysi b3cc83ed6e 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
2026-03-09 13:36:34 +05:30

440 lines
18 KiB
Python

"""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()