design_system.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Design System Generator - Aggregates search results and applies reasoning
  5. to generate comprehensive design system recommendations.
  6. Usage:
  7. from design_system import generate_design_system
  8. result = generate_design_system("SaaS dashboard", "My Project")
  9. """
  10. import csv
  11. import json
  12. from pathlib import Path
  13. from core import search, DATA_DIR
  14. # ============ CONFIGURATION ============
  15. REASONING_FILE = "ui-reasoning.csv"
  16. SEARCH_CONFIG = {
  17. "product": {"max_results": 1},
  18. "style": {"max_results": 3},
  19. "color": {"max_results": 2},
  20. "landing": {"max_results": 2},
  21. "typography": {"max_results": 2}
  22. }
  23. # ============ DESIGN SYSTEM GENERATOR ============
  24. class DesignSystemGenerator:
  25. """Generates design system recommendations from aggregated searches."""
  26. def __init__(self):
  27. self.reasoning_data = self._load_reasoning()
  28. def _load_reasoning(self) -> list:
  29. """Load reasoning rules from CSV."""
  30. filepath = DATA_DIR / REASONING_FILE
  31. if not filepath.exists():
  32. return []
  33. with open(filepath, 'r', encoding='utf-8') as f:
  34. return list(csv.DictReader(f))
  35. def _multi_domain_search(self, query: str, style_priority: list = None) -> dict:
  36. """Execute searches across multiple domains."""
  37. results = {}
  38. for domain, config in SEARCH_CONFIG.items():
  39. if domain == "style" and style_priority:
  40. # For style, also search with priority keywords
  41. priority_query = " ".join(style_priority[:2]) if style_priority else query
  42. combined_query = f"{query} {priority_query}"
  43. results[domain] = search(combined_query, domain, config["max_results"])
  44. else:
  45. results[domain] = search(query, domain, config["max_results"])
  46. return results
  47. def _find_reasoning_rule(self, category: str) -> dict:
  48. """Find matching reasoning rule for a category."""
  49. category_lower = category.lower()
  50. # Try exact match first
  51. for rule in self.reasoning_data:
  52. if rule.get("UI_Category", "").lower() == category_lower:
  53. return rule
  54. # Try partial match
  55. for rule in self.reasoning_data:
  56. ui_cat = rule.get("UI_Category", "").lower()
  57. if ui_cat in category_lower or category_lower in ui_cat:
  58. return rule
  59. # Try keyword match
  60. for rule in self.reasoning_data:
  61. ui_cat = rule.get("UI_Category", "").lower()
  62. keywords = ui_cat.replace("/", " ").replace("-", " ").split()
  63. if any(kw in category_lower for kw in keywords):
  64. return rule
  65. return {}
  66. def _apply_reasoning(self, category: str, search_results: dict) -> dict:
  67. """Apply reasoning rules to search results."""
  68. rule = self._find_reasoning_rule(category)
  69. if not rule:
  70. return {
  71. "pattern": "Hero + Features + CTA",
  72. "style_priority": ["Minimalism", "Flat Design"],
  73. "color_mood": "Professional",
  74. "typography_mood": "Clean",
  75. "key_effects": "Subtle hover transitions",
  76. "anti_patterns": "",
  77. "decision_rules": {},
  78. "severity": "MEDIUM"
  79. }
  80. # Parse decision rules JSON
  81. decision_rules = {}
  82. try:
  83. decision_rules = json.loads(rule.get("Decision_Rules", "{}"))
  84. except json.JSONDecodeError:
  85. pass
  86. return {
  87. "pattern": rule.get("Recommended_Pattern", ""),
  88. "style_priority": [s.strip() for s in rule.get("Style_Priority", "").split("+")],
  89. "color_mood": rule.get("Color_Mood", ""),
  90. "typography_mood": rule.get("Typography_Mood", ""),
  91. "key_effects": rule.get("Key_Effects", ""),
  92. "anti_patterns": rule.get("Anti_Patterns", ""),
  93. "decision_rules": decision_rules,
  94. "severity": rule.get("Severity", "MEDIUM")
  95. }
  96. def _select_best_match(self, results: list, priority_keywords: list) -> dict:
  97. """Select best matching result based on priority keywords."""
  98. if not results:
  99. return {}
  100. if not priority_keywords:
  101. return results[0]
  102. # First: try exact style name match
  103. for priority in priority_keywords:
  104. priority_lower = priority.lower().strip()
  105. for result in results:
  106. style_name = result.get("Style Category", "").lower()
  107. if priority_lower in style_name or style_name in priority_lower:
  108. return result
  109. # Second: score by keyword match in all fields
  110. scored = []
  111. for result in results:
  112. result_str = str(result).lower()
  113. score = 0
  114. for kw in priority_keywords:
  115. kw_lower = kw.lower().strip()
  116. # Higher score for style name match
  117. if kw_lower in result.get("Style Category", "").lower():
  118. score += 10
  119. # Lower score for keyword field match
  120. elif kw_lower in result.get("Keywords", "").lower():
  121. score += 3
  122. # Even lower for other field matches
  123. elif kw_lower in result_str:
  124. score += 1
  125. scored.append((score, result))
  126. scored.sort(key=lambda x: x[0], reverse=True)
  127. return scored[0][1] if scored and scored[0][0] > 0 else results[0]
  128. def _extract_results(self, search_result: dict) -> list:
  129. """Extract results list from search result dict."""
  130. return search_result.get("results", [])
  131. def generate(self, query: str, project_name: str = None) -> dict:
  132. """Generate complete design system recommendation."""
  133. # Step 1: First search product to get category
  134. product_result = search(query, "product", 1)
  135. product_results = product_result.get("results", [])
  136. category = "General"
  137. if product_results:
  138. category = product_results[0].get("Product Type", "General")
  139. # Step 2: Get reasoning rules for this category
  140. reasoning = self._apply_reasoning(category, {})
  141. style_priority = reasoning.get("style_priority", [])
  142. # Step 3: Multi-domain search with style priority hints
  143. search_results = self._multi_domain_search(query, style_priority)
  144. search_results["product"] = product_result # Reuse product search
  145. # Step 4: Select best matches from each domain using priority
  146. style_results = self._extract_results(search_results.get("style", {}))
  147. color_results = self._extract_results(search_results.get("color", {}))
  148. typography_results = self._extract_results(search_results.get("typography", {}))
  149. landing_results = self._extract_results(search_results.get("landing", {}))
  150. best_style = self._select_best_match(style_results, reasoning.get("style_priority", []))
  151. best_color = color_results[0] if color_results else {}
  152. best_typography = typography_results[0] if typography_results else {}
  153. best_landing = landing_results[0] if landing_results else {}
  154. # Step 5: Build final recommendation
  155. # Combine effects from both reasoning and style search
  156. style_effects = best_style.get("Effects & Animation", "")
  157. reasoning_effects = reasoning.get("key_effects", "")
  158. combined_effects = style_effects if style_effects else reasoning_effects
  159. return {
  160. "project_name": project_name or query.upper(),
  161. "category": category,
  162. "pattern": {
  163. "name": best_landing.get("Pattern Name", reasoning.get("pattern", "Hero + Features + CTA")),
  164. "sections": best_landing.get("Section Order", "Hero > Features > CTA"),
  165. "cta_placement": best_landing.get("Primary CTA Placement", "Above fold"),
  166. "color_strategy": best_landing.get("Color Strategy", ""),
  167. "conversion": best_landing.get("Conversion Optimization", "")
  168. },
  169. "style": {
  170. "name": best_style.get("Style Category", "Minimalism"),
  171. "type": best_style.get("Type", "General"),
  172. "effects": style_effects,
  173. "keywords": best_style.get("Keywords", ""),
  174. "best_for": best_style.get("Best For", ""),
  175. "performance": best_style.get("Performance", ""),
  176. "accessibility": best_style.get("Accessibility", "")
  177. },
  178. "colors": {
  179. "primary": best_color.get("Primary (Hex)", "#2563EB"),
  180. "secondary": best_color.get("Secondary (Hex)", "#3B82F6"),
  181. "cta": best_color.get("CTA (Hex)", "#F97316"),
  182. "background": best_color.get("Background (Hex)", "#F8FAFC"),
  183. "text": best_color.get("Text (Hex)", "#1E293B"),
  184. "notes": best_color.get("Notes", "")
  185. },
  186. "typography": {
  187. "heading": best_typography.get("Heading Font", "Inter"),
  188. "body": best_typography.get("Body Font", "Inter"),
  189. "mood": best_typography.get("Mood/Style Keywords", reasoning.get("typography_mood", "")),
  190. "best_for": best_typography.get("Best For", ""),
  191. "google_fonts_url": best_typography.get("Google Fonts URL", ""),
  192. "css_import": best_typography.get("CSS Import", "")
  193. },
  194. "key_effects": combined_effects,
  195. "anti_patterns": reasoning.get("anti_patterns", ""),
  196. "decision_rules": reasoning.get("decision_rules", {}),
  197. "severity": reasoning.get("severity", "MEDIUM")
  198. }
  199. # ============ OUTPUT FORMATTERS ============
  200. BOX_WIDTH = 90 # Wider box for more content
  201. def format_ascii_box(design_system: dict) -> str:
  202. """Format design system as ASCII box with emojis (MCP-style)."""
  203. project = design_system.get("project_name", "PROJECT")
  204. pattern = design_system.get("pattern", {})
  205. style = design_system.get("style", {})
  206. colors = design_system.get("colors", {})
  207. typography = design_system.get("typography", {})
  208. effects = design_system.get("key_effects", "")
  209. anti_patterns = design_system.get("anti_patterns", "")
  210. def wrap_text(text: str, prefix: str, width: int) -> list:
  211. """Wrap long text into multiple lines."""
  212. if not text:
  213. return []
  214. words = text.split()
  215. lines = []
  216. current_line = prefix
  217. for word in words:
  218. if len(current_line) + len(word) + 1 <= width - 2:
  219. current_line += (" " if current_line != prefix else "") + word
  220. else:
  221. if current_line != prefix:
  222. lines.append(current_line)
  223. current_line = prefix + word
  224. if current_line != prefix:
  225. lines.append(current_line)
  226. return lines
  227. # Build sections from pattern
  228. sections = pattern.get("sections", "").split(">")
  229. sections = [s.strip() for s in sections if s.strip()]
  230. # Build output lines
  231. lines = []
  232. w = BOX_WIDTH - 1
  233. lines.append("+" + "-" * w + "+")
  234. lines.append(f"| TARGET: {project} - RECOMMENDED DESIGN SYSTEM".ljust(BOX_WIDTH) + "|")
  235. lines.append("+" + "-" * w + "+")
  236. lines.append("|" + " " * BOX_WIDTH + "|")
  237. # Pattern section
  238. lines.append(f"| PATTERN: {pattern.get('name', '')}".ljust(BOX_WIDTH) + "|")
  239. if pattern.get('conversion'):
  240. lines.append(f"| Conversion: {pattern.get('conversion', '')}".ljust(BOX_WIDTH) + "|")
  241. if pattern.get('cta_placement'):
  242. lines.append(f"| CTA: {pattern.get('cta_placement', '')}".ljust(BOX_WIDTH) + "|")
  243. lines.append("| Sections:".ljust(BOX_WIDTH) + "|")
  244. for i, section in enumerate(sections, 1):
  245. lines.append(f"| {i}. {section}".ljust(BOX_WIDTH) + "|")
  246. lines.append("|" + " " * BOX_WIDTH + "|")
  247. # Style section
  248. lines.append(f"| STYLE: {style.get('name', '')}".ljust(BOX_WIDTH) + "|")
  249. if style.get("keywords"):
  250. for line in wrap_text(f"Keywords: {style.get('keywords', '')}", "| ", BOX_WIDTH):
  251. lines.append(line.ljust(BOX_WIDTH) + "|")
  252. if style.get("best_for"):
  253. for line in wrap_text(f"Best For: {style.get('best_for', '')}", "| ", BOX_WIDTH):
  254. lines.append(line.ljust(BOX_WIDTH) + "|")
  255. if style.get("performance") or style.get("accessibility"):
  256. perf_a11y = f"Performance: {style.get('performance', '')} | Accessibility: {style.get('accessibility', '')}"
  257. lines.append(f"| {perf_a11y}".ljust(BOX_WIDTH) + "|")
  258. lines.append("|" + " " * BOX_WIDTH + "|")
  259. # Colors section
  260. lines.append("| COLORS:".ljust(BOX_WIDTH) + "|")
  261. lines.append(f"| Primary: {colors.get('primary', '')}".ljust(BOX_WIDTH) + "|")
  262. lines.append(f"| Secondary: {colors.get('secondary', '')}".ljust(BOX_WIDTH) + "|")
  263. lines.append(f"| CTA: {colors.get('cta', '')}".ljust(BOX_WIDTH) + "|")
  264. lines.append(f"| Background: {colors.get('background', '')}".ljust(BOX_WIDTH) + "|")
  265. lines.append(f"| Text: {colors.get('text', '')}".ljust(BOX_WIDTH) + "|")
  266. if colors.get("notes"):
  267. for line in wrap_text(f"Notes: {colors.get('notes', '')}", "| ", BOX_WIDTH):
  268. lines.append(line.ljust(BOX_WIDTH) + "|")
  269. lines.append("|" + " " * BOX_WIDTH + "|")
  270. # Typography section
  271. lines.append(f"| TYPOGRAPHY: {typography.get('heading', '')} / {typography.get('body', '')}".ljust(BOX_WIDTH) + "|")
  272. if typography.get("mood"):
  273. for line in wrap_text(f"Mood: {typography.get('mood', '')}", "| ", BOX_WIDTH):
  274. lines.append(line.ljust(BOX_WIDTH) + "|")
  275. if typography.get("best_for"):
  276. for line in wrap_text(f"Best For: {typography.get('best_for', '')}", "| ", BOX_WIDTH):
  277. lines.append(line.ljust(BOX_WIDTH) + "|")
  278. if typography.get("google_fonts_url"):
  279. lines.append(f"| Google Fonts: {typography.get('google_fonts_url', '')}".ljust(BOX_WIDTH) + "|")
  280. if typography.get("css_import"):
  281. lines.append(f"| CSS Import: {typography.get('css_import', '')[:70]}...".ljust(BOX_WIDTH) + "|")
  282. lines.append("|" + " " * BOX_WIDTH + "|")
  283. # Key Effects section
  284. if effects:
  285. lines.append("| KEY EFFECTS:".ljust(BOX_WIDTH) + "|")
  286. for line in wrap_text(effects, "| ", BOX_WIDTH):
  287. lines.append(line.ljust(BOX_WIDTH) + "|")
  288. lines.append("|" + " " * BOX_WIDTH + "|")
  289. # Anti-patterns section
  290. if anti_patterns:
  291. lines.append("| AVOID (Anti-patterns):".ljust(BOX_WIDTH) + "|")
  292. for line in wrap_text(anti_patterns, "| ", BOX_WIDTH):
  293. lines.append(line.ljust(BOX_WIDTH) + "|")
  294. lines.append("|" + " " * BOX_WIDTH + "|")
  295. # Pre-Delivery Checklist section
  296. lines.append("| PRE-DELIVERY CHECKLIST:".ljust(BOX_WIDTH) + "|")
  297. checklist_items = [
  298. "[ ] No emojis as icons (use SVG: Heroicons/Lucide)",
  299. "[ ] cursor-pointer on all clickable elements",
  300. "[ ] Hover states with smooth transitions (150-300ms)",
  301. "[ ] Light mode: text contrast 4.5:1 minimum",
  302. "[ ] Focus states visible for keyboard nav",
  303. "[ ] prefers-reduced-motion respected",
  304. "[ ] Responsive: 375px, 768px, 1024px, 1440px"
  305. ]
  306. for item in checklist_items:
  307. lines.append(f"| {item}".ljust(BOX_WIDTH) + "|")
  308. lines.append("|" + " " * BOX_WIDTH + "|")
  309. lines.append("+" + "-" * w + "+")
  310. return "\n".join(lines)
  311. def format_markdown(design_system: dict) -> str:
  312. """Format design system as markdown."""
  313. project = design_system.get("project_name", "PROJECT")
  314. pattern = design_system.get("pattern", {})
  315. style = design_system.get("style", {})
  316. colors = design_system.get("colors", {})
  317. typography = design_system.get("typography", {})
  318. effects = design_system.get("key_effects", "")
  319. anti_patterns = design_system.get("anti_patterns", "")
  320. lines = []
  321. lines.append(f"## Design System: {project}")
  322. lines.append("")
  323. # Pattern section
  324. lines.append("### Pattern")
  325. lines.append(f"- **Name:** {pattern.get('name', '')}")
  326. if pattern.get('conversion'):
  327. lines.append(f"- **Conversion Focus:** {pattern.get('conversion', '')}")
  328. if pattern.get('cta_placement'):
  329. lines.append(f"- **CTA Placement:** {pattern.get('cta_placement', '')}")
  330. if pattern.get('color_strategy'):
  331. lines.append(f"- **Color Strategy:** {pattern.get('color_strategy', '')}")
  332. lines.append(f"- **Sections:** {pattern.get('sections', '')}")
  333. lines.append("")
  334. # Style section
  335. lines.append("### Style")
  336. lines.append(f"- **Name:** {style.get('name', '')}")
  337. if style.get('keywords'):
  338. lines.append(f"- **Keywords:** {style.get('keywords', '')}")
  339. if style.get('best_for'):
  340. lines.append(f"- **Best For:** {style.get('best_for', '')}")
  341. if style.get('performance') or style.get('accessibility'):
  342. lines.append(f"- **Performance:** {style.get('performance', '')} | **Accessibility:** {style.get('accessibility', '')}")
  343. lines.append("")
  344. # Colors section
  345. lines.append("### Colors")
  346. lines.append(f"| Role | Hex |")
  347. lines.append(f"|------|-----|")
  348. lines.append(f"| Primary | {colors.get('primary', '')} |")
  349. lines.append(f"| Secondary | {colors.get('secondary', '')} |")
  350. lines.append(f"| CTA | {colors.get('cta', '')} |")
  351. lines.append(f"| Background | {colors.get('background', '')} |")
  352. lines.append(f"| Text | {colors.get('text', '')} |")
  353. if colors.get("notes"):
  354. lines.append(f"\n*Notes: {colors.get('notes', '')}*")
  355. lines.append("")
  356. # Typography section
  357. lines.append("### Typography")
  358. lines.append(f"- **Heading:** {typography.get('heading', '')}")
  359. lines.append(f"- **Body:** {typography.get('body', '')}")
  360. if typography.get("mood"):
  361. lines.append(f"- **Mood:** {typography.get('mood', '')}")
  362. if typography.get("best_for"):
  363. lines.append(f"- **Best For:** {typography.get('best_for', '')}")
  364. if typography.get("google_fonts_url"):
  365. lines.append(f"- **Google Fonts:** {typography.get('google_fonts_url', '')}")
  366. if typography.get("css_import"):
  367. lines.append(f"- **CSS Import:**")
  368. lines.append(f"```css")
  369. lines.append(f"{typography.get('css_import', '')}")
  370. lines.append(f"```")
  371. lines.append("")
  372. # Key Effects section
  373. if effects:
  374. lines.append("### Key Effects")
  375. lines.append(f"{effects}")
  376. lines.append("")
  377. # Anti-patterns section
  378. if anti_patterns:
  379. lines.append("### Avoid (Anti-patterns)")
  380. lines.append(f"- {anti_patterns.replace(' + ', '\n- ')}")
  381. lines.append("")
  382. # Pre-Delivery Checklist section
  383. lines.append("### Pre-Delivery Checklist")
  384. lines.append("- [ ] No emojis as icons (use SVG: Heroicons/Lucide)")
  385. lines.append("- [ ] cursor-pointer on all clickable elements")
  386. lines.append("- [ ] Hover states with smooth transitions (150-300ms)")
  387. lines.append("- [ ] Light mode: text contrast 4.5:1 minimum")
  388. lines.append("- [ ] Focus states visible for keyboard nav")
  389. lines.append("- [ ] prefers-reduced-motion respected")
  390. lines.append("- [ ] Responsive: 375px, 768px, 1024px, 1440px")
  391. lines.append("")
  392. return "\n".join(lines)
  393. # ============ MAIN ENTRY POINT ============
  394. def generate_design_system(query: str, project_name: str = None, output_format: str = "ascii") -> str:
  395. """
  396. Main entry point for design system generation.
  397. Args:
  398. query: Search query (e.g., "SaaS dashboard", "e-commerce luxury")
  399. project_name: Optional project name for output header
  400. output_format: "ascii" (default) or "markdown"
  401. Returns:
  402. Formatted design system string
  403. """
  404. generator = DesignSystemGenerator()
  405. design_system = generator.generate(query, project_name)
  406. if output_format == "markdown":
  407. return format_markdown(design_system)
  408. return format_ascii_box(design_system)
  409. # ============ CLI SUPPORT ============
  410. if __name__ == "__main__":
  411. import argparse
  412. parser = argparse.ArgumentParser(description="Generate Design System")
  413. parser.add_argument("query", help="Search query (e.g., 'SaaS dashboard')")
  414. parser.add_argument("--project-name", "-p", type=str, default=None, help="Project name")
  415. parser.add_argument("--format", "-f", choices=["ascii", "markdown"], default="ascii", help="Output format")
  416. args = parser.parse_args()
  417. result = generate_design_system(args.query, args.project_name, args.format)
  418. print(result)