| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Design System Generator - Aggregates search results and applies reasoning
- to generate comprehensive design system recommendations.
- Usage:
- from design_system import generate_design_system
- result = generate_design_system("SaaS dashboard", "My Project")
- """
- import csv
- import json
- from pathlib import Path
- from core import search, DATA_DIR
- # ============ CONFIGURATION ============
- REASONING_FILE = "ui-reasoning.csv"
- SEARCH_CONFIG = {
- "product": {"max_results": 1},
- "style": {"max_results": 3},
- "color": {"max_results": 2},
- "landing": {"max_results": 2},
- "typography": {"max_results": 2}
- }
- # ============ DESIGN SYSTEM GENERATOR ============
- class DesignSystemGenerator:
- """Generates design system recommendations from aggregated searches."""
- def __init__(self):
- self.reasoning_data = self._load_reasoning()
- def _load_reasoning(self) -> list:
- """Load reasoning rules from CSV."""
- filepath = DATA_DIR / REASONING_FILE
- if not filepath.exists():
- return []
- with open(filepath, 'r', encoding='utf-8') as f:
- return list(csv.DictReader(f))
- def _multi_domain_search(self, query: str, style_priority: list = None) -> dict:
- """Execute searches across multiple domains."""
- results = {}
- for domain, config in SEARCH_CONFIG.items():
- if domain == "style" and style_priority:
- # For style, also search with priority keywords
- priority_query = " ".join(style_priority[:2]) if style_priority else query
- combined_query = f"{query} {priority_query}"
- results[domain] = search(combined_query, domain, config["max_results"])
- else:
- results[domain] = search(query, domain, config["max_results"])
- return results
- def _find_reasoning_rule(self, category: str) -> dict:
- """Find matching reasoning rule for a category."""
- category_lower = category.lower()
- # Try exact match first
- for rule in self.reasoning_data:
- if rule.get("UI_Category", "").lower() == category_lower:
- return rule
- # Try partial match
- for rule in self.reasoning_data:
- ui_cat = rule.get("UI_Category", "").lower()
- if ui_cat in category_lower or category_lower in ui_cat:
- return rule
- # Try keyword match
- for rule in self.reasoning_data:
- ui_cat = rule.get("UI_Category", "").lower()
- keywords = ui_cat.replace("/", " ").replace("-", " ").split()
- if any(kw in category_lower for kw in keywords):
- return rule
- return {}
- def _apply_reasoning(self, category: str, search_results: dict) -> dict:
- """Apply reasoning rules to search results."""
- rule = self._find_reasoning_rule(category)
- if not rule:
- return {
- "pattern": "Hero + Features + CTA",
- "style_priority": ["Minimalism", "Flat Design"],
- "color_mood": "Professional",
- "typography_mood": "Clean",
- "key_effects": "Subtle hover transitions",
- "anti_patterns": "",
- "decision_rules": {},
- "severity": "MEDIUM"
- }
- # Parse decision rules JSON
- decision_rules = {}
- try:
- decision_rules = json.loads(rule.get("Decision_Rules", "{}"))
- except json.JSONDecodeError:
- pass
- return {
- "pattern": rule.get("Recommended_Pattern", ""),
- "style_priority": [s.strip() for s in rule.get("Style_Priority", "").split("+")],
- "color_mood": rule.get("Color_Mood", ""),
- "typography_mood": rule.get("Typography_Mood", ""),
- "key_effects": rule.get("Key_Effects", ""),
- "anti_patterns": rule.get("Anti_Patterns", ""),
- "decision_rules": decision_rules,
- "severity": rule.get("Severity", "MEDIUM")
- }
- def _select_best_match(self, results: list, priority_keywords: list) -> dict:
- """Select best matching result based on priority keywords."""
- if not results:
- return {}
- if not priority_keywords:
- return results[0]
- # First: try exact style name match
- for priority in priority_keywords:
- priority_lower = priority.lower().strip()
- for result in results:
- style_name = result.get("Style Category", "").lower()
- if priority_lower in style_name or style_name in priority_lower:
- return result
- # Second: score by keyword match in all fields
- scored = []
- for result in results:
- result_str = str(result).lower()
- score = 0
- for kw in priority_keywords:
- kw_lower = kw.lower().strip()
- # Higher score for style name match
- if kw_lower in result.get("Style Category", "").lower():
- score += 10
- # Lower score for keyword field match
- elif kw_lower in result.get("Keywords", "").lower():
- score += 3
- # Even lower for other field matches
- elif kw_lower in result_str:
- score += 1
- scored.append((score, result))
- scored.sort(key=lambda x: x[0], reverse=True)
- return scored[0][1] if scored and scored[0][0] > 0 else results[0]
- def _extract_results(self, search_result: dict) -> list:
- """Extract results list from search result dict."""
- return search_result.get("results", [])
- def generate(self, query: str, project_name: str = None) -> dict:
- """Generate complete design system recommendation."""
- # Step 1: First search product to get category
- product_result = search(query, "product", 1)
- product_results = product_result.get("results", [])
- category = "General"
- if product_results:
- category = product_results[0].get("Product Type", "General")
- # Step 2: Get reasoning rules for this category
- reasoning = self._apply_reasoning(category, {})
- style_priority = reasoning.get("style_priority", [])
- # Step 3: Multi-domain search with style priority hints
- search_results = self._multi_domain_search(query, style_priority)
- search_results["product"] = product_result # Reuse product search
- # Step 4: Select best matches from each domain using priority
- style_results = self._extract_results(search_results.get("style", {}))
- color_results = self._extract_results(search_results.get("color", {}))
- typography_results = self._extract_results(search_results.get("typography", {}))
- landing_results = self._extract_results(search_results.get("landing", {}))
- best_style = self._select_best_match(style_results, reasoning.get("style_priority", []))
- best_color = color_results[0] if color_results else {}
- best_typography = typography_results[0] if typography_results else {}
- best_landing = landing_results[0] if landing_results else {}
- # Step 5: Build final recommendation
- # Combine effects from both reasoning and style search
- style_effects = best_style.get("Effects & Animation", "")
- reasoning_effects = reasoning.get("key_effects", "")
- combined_effects = style_effects if style_effects else reasoning_effects
- return {
- "project_name": project_name or query.upper(),
- "category": category,
- "pattern": {
- "name": best_landing.get("Pattern Name", reasoning.get("pattern", "Hero + Features + CTA")),
- "sections": best_landing.get("Section Order", "Hero > Features > CTA"),
- "cta_placement": best_landing.get("Primary CTA Placement", "Above fold"),
- "color_strategy": best_landing.get("Color Strategy", ""),
- "conversion": best_landing.get("Conversion Optimization", "")
- },
- "style": {
- "name": best_style.get("Style Category", "Minimalism"),
- "type": best_style.get("Type", "General"),
- "effects": style_effects,
- "keywords": best_style.get("Keywords", ""),
- "best_for": best_style.get("Best For", ""),
- "performance": best_style.get("Performance", ""),
- "accessibility": best_style.get("Accessibility", "")
- },
- "colors": {
- "primary": best_color.get("Primary (Hex)", "#2563EB"),
- "secondary": best_color.get("Secondary (Hex)", "#3B82F6"),
- "cta": best_color.get("CTA (Hex)", "#F97316"),
- "background": best_color.get("Background (Hex)", "#F8FAFC"),
- "text": best_color.get("Text (Hex)", "#1E293B"),
- "notes": best_color.get("Notes", "")
- },
- "typography": {
- "heading": best_typography.get("Heading Font", "Inter"),
- "body": best_typography.get("Body Font", "Inter"),
- "mood": best_typography.get("Mood/Style Keywords", reasoning.get("typography_mood", "")),
- "best_for": best_typography.get("Best For", ""),
- "google_fonts_url": best_typography.get("Google Fonts URL", ""),
- "css_import": best_typography.get("CSS Import", "")
- },
- "key_effects": combined_effects,
- "anti_patterns": reasoning.get("anti_patterns", ""),
- "decision_rules": reasoning.get("decision_rules", {}),
- "severity": reasoning.get("severity", "MEDIUM")
- }
- # ============ OUTPUT FORMATTERS ============
- BOX_WIDTH = 90 # Wider box for more content
- def format_ascii_box(design_system: dict) -> str:
- """Format design system as ASCII box with emojis (MCP-style)."""
- project = design_system.get("project_name", "PROJECT")
- pattern = design_system.get("pattern", {})
- style = design_system.get("style", {})
- colors = design_system.get("colors", {})
- typography = design_system.get("typography", {})
- effects = design_system.get("key_effects", "")
- anti_patterns = design_system.get("anti_patterns", "")
- def wrap_text(text: str, prefix: str, width: int) -> list:
- """Wrap long text into multiple lines."""
- if not text:
- return []
- words = text.split()
- lines = []
- current_line = prefix
- for word in words:
- if len(current_line) + len(word) + 1 <= width - 2:
- current_line += (" " if current_line != prefix else "") + word
- else:
- if current_line != prefix:
- lines.append(current_line)
- current_line = prefix + word
- if current_line != prefix:
- lines.append(current_line)
- return lines
- # Build sections from pattern
- sections = pattern.get("sections", "").split(">")
- sections = [s.strip() for s in sections if s.strip()]
- # Build output lines
- lines = []
- w = BOX_WIDTH - 1
- lines.append("+" + "-" * w + "+")
- lines.append(f"| TARGET: {project} - RECOMMENDED DESIGN SYSTEM".ljust(BOX_WIDTH) + "|")
- lines.append("+" + "-" * w + "+")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Pattern section
- lines.append(f"| PATTERN: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "|")
- if pattern.get('conversion'):
- lines.append(f"| Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "|")
- if pattern.get('cta_placement'):
- lines.append(f"| CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "|")
- lines.append("| Sections:".ljust(BOX_WIDTH) + "|")
- for i, section in enumerate(sections, 1):
- lines.append(f"| {i}. {section}".ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Style section
- lines.append(f"| STYLE: {style.get('name', '')}".ljust(BOX_WIDTH) + "|")
- if style.get("keywords"):
- for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- if style.get("best_for"):
- for line in wrap_text(f"Best For: {style.get('best_for', '')}", "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- if style.get("performance") or style.get("accessibility"):
- perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"
- lines.append(f"| {perf_a11y}".ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Colors section
- lines.append("| COLORS:".ljust(BOX_WIDTH) + "|")
- lines.append(f"| Primary: {colors.get('primary', '')}".ljust(BOX_WIDTH) + "|")
- lines.append(f"| Secondary: {colors.get('secondary', '')}".ljust(BOX_WIDTH) + "|")
- lines.append(f"| CTA: {colors.get('cta', '')}".ljust(BOX_WIDTH) + "|")
- lines.append(f"| Background: {colors.get('background', '')}".ljust(BOX_WIDTH) + "|")
- lines.append(f"| Text: {colors.get('text', '')}".ljust(BOX_WIDTH) + "|")
- if colors.get("notes"):
- for line in wrap_text(f"Notes: {colors.get('notes', '')}", "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Typography section
- lines.append(f"| TYPOGRAPHY: {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "|")
- if typography.get("mood"):
- for line in wrap_text(f"Mood: {typography.get('mood', '')}", "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- if typography.get("best_for"):
- for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- if typography.get("google_fonts_url"):
- lines.append(f"| Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "|")
- if typography.get("css_import"):
- lines.append(f"| CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Key Effects section
- if effects:
- lines.append("| KEY EFFECTS:".ljust(BOX_WIDTH) + "|")
- for line in wrap_text(effects, "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Anti-patterns section
- if anti_patterns:
- lines.append("| AVOID (Anti-patterns):".ljust(BOX_WIDTH) + "|")
- for line in wrap_text(anti_patterns, "| ", BOX_WIDTH):
- lines.append(line.ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- # Pre-Delivery Checklist section
- lines.append("| PRE-DELIVERY CHECKLIST:".ljust(BOX_WIDTH) + "|")
- checklist_items = [
- "[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
- "[ ] cursor-pointer on all clickable elements",
- "[ ] Hover states with smooth transitions (150-300ms)",
- "[ ] Light mode: text contrast 4.5:1 minimum",
- "[ ] Focus states visible for keyboard nav",
- "[ ] prefers-reduced-motion respected",
- "[ ] Responsive: 375px, 768px, 1024px, 1440px"
- ]
- for item in checklist_items:
- lines.append(f"| {item}".ljust(BOX_WIDTH) + "|")
- lines.append("|" + " " * BOX_WIDTH + "|")
- lines.append("+" + "-" * w + "+")
- return "\n".join(lines)
- def format_markdown(design_system: dict) -> str:
- """Format design system as markdown."""
- project = design_system.get("project_name", "PROJECT")
- pattern = design_system.get("pattern", {})
- style = design_system.get("style", {})
- colors = design_system.get("colors", {})
- typography = design_system.get("typography", {})
- effects = design_system.get("key_effects", "")
- anti_patterns = design_system.get("anti_patterns", "")
- lines = []
- lines.append(f"## Design System: {project}")
- lines.append("")
- # Pattern section
- lines.append("### Pattern")
- lines.append(f"- **Name:** {pattern.get('name', '')}")
- if pattern.get('conversion'):
- lines.append(f"- **Conversion Focus:** {pattern.get('conversion', '')}")
- if pattern.get('cta_placement'):
- lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}")
- if pattern.get('color_strategy'):
- lines.append(f"- **Color Strategy:** {pattern.get('color_strategy', '')}")
- lines.append(f"- **Sections:** {pattern.get('sections', '')}")
- lines.append("")
- # Style section
- lines.append("### Style")
- lines.append(f"- **Name:** {style.get('name', '')}")
- if style.get('keywords'):
- lines.append(f"- **Keywords:** {style.get('keywords', '')}")
- if style.get('best_for'):
- lines.append(f"- **Best For:** {style.get('best_for', '')}")
- if style.get('performance') or style.get('accessibility'):
- lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}")
- lines.append("")
- # Colors section
- lines.append("### Colors")
- lines.append(f"| Role | Hex |")
- lines.append(f"|------|-----|")
- lines.append(f"| Primary | {colors.get('primary', '')} |")
- lines.append(f"| Secondary | {colors.get('secondary', '')} |")
- lines.append(f"| CTA | {colors.get('cta', '')} |")
- lines.append(f"| Background | {colors.get('background', '')} |")
- lines.append(f"| Text | {colors.get('text', '')} |")
- if colors.get("notes"):
- lines.append(f"\n*Notes: {colors.get('notes', '')}*")
- lines.append("")
- # Typography section
- lines.append("### Typography")
- lines.append(f"- **Heading:** {typography.get('heading', '')}")
- lines.append(f"- **Body:** {typography.get('body', '')}")
- if typography.get("mood"):
- lines.append(f"- **Mood:** {typography.get('mood', '')}")
- if typography.get("best_for"):
- lines.append(f"- **Best For:** {typography.get('best_for', '')}")
- if typography.get("google_fonts_url"):
- lines.append(f"- **Google Fonts:** {typography.get('google_fonts_url', '')}")
- if typography.get("css_import"):
- lines.append(f"- **CSS Import:**")
- lines.append(f"```css")
- lines.append(f"{typography.get('css_import', '')}")
- lines.append(f"```")
- lines.append("")
- # Key Effects section
- if effects:
- lines.append("### Key Effects")
- lines.append(f"{effects}")
- lines.append("")
- # Anti-patterns section
- if anti_patterns:
- lines.append("### Avoid (Anti-patterns)")
- lines.append(f"- {anti_patterns.replace(' + ', '\n- ')}")
- lines.append("")
- # Pre-Delivery Checklist section
- lines.append("### Pre-Delivery Checklist")
- lines.append("- [ ] No emojis as icons (use SVG: Heroicons/Lucide)")
- lines.append("- [ ] cursor-pointer on all clickable elements")
- lines.append("- [ ] Hover states with smooth transitions (150-300ms)")
- lines.append("- [ ] Light mode: text contrast 4.5:1 minimum")
- lines.append("- [ ] Focus states visible for keyboard nav")
- lines.append("- [ ] prefers-reduced-motion respected")
- lines.append("- [ ] Responsive: 375px, 768px, 1024px, 1440px")
- lines.append("")
- return "\n".join(lines)
- # ============ MAIN ENTRY POINT ============
- def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii") -> str:
- """
- Main entry point for design system generation.
- Args:
- query: Search query (e.g., "SaaS dashboard", "e-commerce luxury")
- project_name: Optional project name for output header
- output_format: "ascii" (default) or "markdown"
- Returns:
- Formatted design system string
- """
- generator = DesignSystemGenerator()
- design_system = generator.generate(query, project_name)
- if output_format == "markdown":
- return format_markdown(design_system)
- return format_ascii_box(design_system)
- # ============ CLI SUPPORT ============
- if __name__ == "__main__":
- import argparse
- parser = argparse.ArgumentParser(description="Generate Design System")
- parser.add_argument("query", help="Search query (e.g., 'SaaS dashboard')")
- parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name")
- parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format")
- args = parser.parse_args()
- result = generate_design_system(args.query, args.project_name, args.format)
- print(result)
|