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

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