heraspec 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +188 -103
  3. package/bin/heraspec.js +4805 -1122
  4. package/bin/heraspec.js.map +4 -4
  5. package/dist/core/templates/skills/CHANGELOG.md +117 -117
  6. package/dist/core/templates/skills/README-template.md +58 -58
  7. package/dist/core/templates/skills/README.md +38 -38
  8. package/dist/core/templates/skills/content-optimization-skill.md +104 -104
  9. package/dist/core/templates/skills/data/design-systems.csv +54 -0
  10. package/dist/core/templates/skills/data/pages-proposed.csv +21 -21
  11. package/dist/core/templates/skills/data/pages.csv +9 -9
  12. package/dist/core/templates/skills/data/typography.csv +57 -57
  13. package/dist/core/templates/skills/deploy-documentation-skill.md +408 -0
  14. package/dist/core/templates/skills/design-system-skill.md +176 -0
  15. package/dist/core/templates/skills/documents/templates/documentation-landing-page.html +63 -63
  16. package/dist/core/templates/skills/documents/templates/documentation.html +49 -49
  17. package/dist/core/templates/skills/documents/templates/landing-script.js +38 -38
  18. package/dist/core/templates/skills/documents/templates/landing-style.css +158 -158
  19. package/dist/core/templates/skills/documents/templates/script.js +56 -56
  20. package/dist/core/templates/skills/documents/templates/style.css +155 -155
  21. package/dist/core/templates/skills/documents/templates/technical-doc-template.md +16 -16
  22. package/dist/core/templates/skills/documents/templates/user-guide-template.md +16 -16
  23. package/dist/core/templates/skills/documents-skill.md +104 -104
  24. package/dist/core/templates/skills/e2e-test-skill.md +119 -119
  25. package/dist/core/templates/skills/git-embed-skill.md +57 -0
  26. package/dist/core/templates/skills/integration-test-skill.md +118 -118
  27. package/dist/core/templates/skills/knowledge/README.md +63 -0
  28. package/dist/core/templates/skills/knowledge/design-systems/airbnb/DESIGN.md +246 -0
  29. package/dist/core/templates/skills/knowledge/design-systems/airtable/DESIGN.md +89 -0
  30. package/dist/core/templates/skills/knowledge/design-systems/apple/DESIGN.md +313 -0
  31. package/dist/core/templates/skills/knowledge/design-systems/bmw/DESIGN.md +180 -0
  32. package/dist/core/templates/skills/knowledge/design-systems/cal/DESIGN.md +259 -0
  33. package/dist/core/templates/skills/knowledge/design-systems/claude/DESIGN.md +312 -0
  34. package/dist/core/templates/skills/knowledge/design-systems/clay/DESIGN.md +304 -0
  35. package/dist/core/templates/skills/knowledge/design-systems/clickhouse/DESIGN.md +281 -0
  36. package/dist/core/templates/skills/knowledge/design-systems/cohere/DESIGN.md +266 -0
  37. package/dist/core/templates/skills/knowledge/design-systems/coinbase/DESIGN.md +129 -0
  38. package/dist/core/templates/skills/knowledge/design-systems/composio/DESIGN.md +307 -0
  39. package/dist/core/templates/skills/knowledge/design-systems/cursor/DESIGN.md +309 -0
  40. package/dist/core/templates/skills/knowledge/design-systems/elevenlabs/DESIGN.md +265 -0
  41. package/dist/core/templates/skills/knowledge/design-systems/expo/DESIGN.md +281 -0
  42. package/dist/core/templates/skills/knowledge/design-systems/figma/DESIGN.md +220 -0
  43. package/dist/core/templates/skills/knowledge/design-systems/framer/DESIGN.md +246 -0
  44. package/dist/core/templates/skills/knowledge/design-systems/hashicorp/DESIGN.md +278 -0
  45. package/dist/core/templates/skills/knowledge/design-systems/ibm/DESIGN.md +332 -0
  46. package/dist/core/templates/skills/knowledge/design-systems/index.json +72 -0
  47. package/dist/core/templates/skills/knowledge/design-systems/intercom/DESIGN.md +146 -0
  48. package/dist/core/templates/skills/knowledge/design-systems/kraken/DESIGN.md +125 -0
  49. package/dist/core/templates/skills/knowledge/design-systems/linear.app/DESIGN.md +367 -0
  50. package/dist/core/templates/skills/knowledge/design-systems/lovable/DESIGN.md +298 -0
  51. package/dist/core/templates/skills/knowledge/design-systems/minimax/DESIGN.md +257 -0
  52. package/dist/core/templates/skills/knowledge/design-systems/mintlify/DESIGN.md +326 -0
  53. package/dist/core/templates/skills/knowledge/design-systems/miro/DESIGN.md +108 -0
  54. package/dist/core/templates/skills/knowledge/design-systems/mistral.ai/DESIGN.md +261 -0
  55. package/dist/core/templates/skills/knowledge/design-systems/mongodb/DESIGN.md +266 -0
  56. package/dist/core/templates/skills/knowledge/design-systems/notion/DESIGN.md +309 -0
  57. package/dist/core/templates/skills/knowledge/design-systems/nvidia/DESIGN.md +293 -0
  58. package/dist/core/templates/skills/knowledge/design-systems/ollama/DESIGN.md +267 -0
  59. package/dist/core/templates/skills/knowledge/design-systems/opencode.ai/DESIGN.md +281 -0
  60. package/dist/core/templates/skills/knowledge/design-systems/pinterest/DESIGN.md +230 -0
  61. package/dist/core/templates/skills/knowledge/design-systems/posthog/DESIGN.md +256 -0
  62. package/dist/core/templates/skills/knowledge/design-systems/raycast/DESIGN.md +268 -0
  63. package/dist/core/templates/skills/knowledge/design-systems/replicate/DESIGN.md +261 -0
  64. package/dist/core/templates/skills/knowledge/design-systems/resend/DESIGN.md +303 -0
  65. package/dist/core/templates/skills/knowledge/design-systems/revolut/DESIGN.md +185 -0
  66. package/dist/core/templates/skills/knowledge/design-systems/runwayml/DESIGN.md +244 -0
  67. package/dist/core/templates/skills/knowledge/design-systems/sanity/DESIGN.md +357 -0
  68. package/dist/core/templates/skills/knowledge/design-systems/sentry/DESIGN.md +262 -0
  69. package/dist/core/templates/skills/knowledge/design-systems/spacex/DESIGN.md +194 -0
  70. package/dist/core/templates/skills/knowledge/design-systems/spotify/DESIGN.md +246 -0
  71. package/dist/core/templates/skills/knowledge/design-systems/stripe/DESIGN.md +322 -0
  72. package/dist/core/templates/skills/knowledge/design-systems/supabase/DESIGN.md +255 -0
  73. package/dist/core/templates/skills/knowledge/design-systems/superhuman/DESIGN.md +252 -0
  74. package/dist/core/templates/skills/knowledge/design-systems/together.ai/DESIGN.md +263 -0
  75. package/dist/core/templates/skills/knowledge/design-systems/uber/DESIGN.md +295 -0
  76. package/dist/core/templates/skills/knowledge/design-systems/vercel/DESIGN.md +310 -0
  77. package/dist/core/templates/skills/knowledge/design-systems/voltagent/DESIGN.md +323 -0
  78. package/dist/core/templates/skills/knowledge/design-systems/warp/DESIGN.md +253 -0
  79. package/dist/core/templates/skills/knowledge/design-systems/webflow/DESIGN.md +92 -0
  80. package/dist/core/templates/skills/knowledge/design-systems/wise/DESIGN.md +173 -0
  81. package/dist/core/templates/skills/knowledge/design-systems/x.ai/DESIGN.md +257 -0
  82. package/dist/core/templates/skills/knowledge/design-systems/zapier/DESIGN.md +328 -0
  83. package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/profile.json +27 -0
  84. package/dist/core/templates/skills/knowledge/frameworks/php/codeigniter/rise-cms/structure.md +137 -0
  85. package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/profile.json +39 -0
  86. package/dist/core/templates/skills/knowledge/frameworks/php/laravel/botble/structure.md +208 -0
  87. package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/profile.json +51 -0
  88. package/dist/core/templates/skills/knowledge/frameworks/php/wordpress/core/structure.md +369 -0
  89. package/dist/core/templates/skills/knowledge/index.json +65 -0
  90. package/dist/core/templates/skills/module-codebase-skill.md +110 -110
  91. package/dist/core/templates/skills/plugin-directory-skill.md +396 -396
  92. package/dist/core/templates/skills/project-memory-skill.md +222 -0
  93. package/dist/core/templates/skills/project-memory-skill.vi.md +223 -0
  94. package/dist/core/templates/skills/scripts/CODE_EXPLANATION.md +394 -394
  95. package/dist/core/templates/skills/scripts/SEARCH_ALGORITHMS_COMPARISON.md +421 -421
  96. package/dist/core/templates/skills/scripts/SEARCH_MODES_GUIDE.md +238 -238
  97. package/dist/core/templates/skills/scripts/__pycache__/core.cpython-311.pyc +0 -0
  98. package/dist/core/templates/skills/scripts/core.py +391 -385
  99. package/dist/core/templates/skills/scripts/search.py +1 -1
  100. package/dist/core/templates/skills/smart-explore-skill.md +141 -0
  101. package/dist/core/templates/skills/sourcecode-analyzer-skill.md +210 -0
  102. package/dist/core/templates/skills/sourcecode-analyzer-skill.vi.md +210 -0
  103. package/dist/core/templates/skills/suggestion-skill.md +118 -118
  104. package/dist/core/templates/skills/templates/accessibility-checklist.md +40 -40
  105. package/dist/core/templates/skills/templates/example-prompt-full-theme.md +333 -333
  106. package/dist/core/templates/skills/templates/page-types-guide.md +338 -338
  107. package/dist/core/templates/skills/templates/pages-proposed-summary.md +273 -273
  108. package/dist/core/templates/skills/templates/pre-delivery-checklist.md +42 -42
  109. package/dist/core/templates/skills/templates/prompt-template-full-theme.md +313 -313
  110. package/dist/core/templates/skills/templates/responsive-design.md +40 -40
  111. package/dist/core/templates/skills/ui-ux-skill.md +595 -584
  112. package/dist/core/templates/skills/unit-test-skill.md +111 -111
  113. package/dist/core/templates/skills/ux-element/templates/Controller.php +50 -50
  114. package/dist/core/templates/skills/ux-element/templates/Shortcode.php +23 -23
  115. package/dist/core/templates/skills/ux-element/templates/Template.html +20 -20
  116. package/dist/core/templates/skills/ux-element/templates/Thumbnail.svg +8 -8
  117. package/dist/core/templates/skills/ux-element/templates/View.php +21 -21
  118. package/dist/core/templates/skills/ux-element-skill.md +83 -83
  119. package/dist/core/templates/skills/wordpress-plugin-check-skill.md +151 -76
  120. package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-dashboard.php +47 -47
  121. package/dist/core/templates/skills/wordpress-plugin-standard/templates/admin-settings.php +60 -60
  122. package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-css.css +22 -22
  123. package/dist/core/templates/skills/wordpress-plugin-standard/templates/assets/admin-js.js +15 -15
  124. package/dist/core/templates/skills/wordpress-plugin-standard/templates/plugin-main.php +169 -169
  125. package/dist/core/templates/skills/wordpress-plugin-standard/templates/readme.txt +41 -41
  126. package/dist/core/templates/skills/wordpress-plugin-standard/templates/uninstall.php +21 -21
  127. package/dist/core/templates/skills/wordpress-plugin-standard-skill.md +100 -100
  128. package/dist/index.js +4068 -278
  129. package/package.json +75 -72
@@ -1,385 +1,391 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- UI/UX Builder Core - BM25 search engine for UI/UX style guides
5
- """
6
-
7
- import csv
8
- import re
9
- from pathlib import Path
10
- from math import log
11
- from collections import defaultdict
12
-
13
- # Optional dependencies for vector search
14
- try:
15
- from sentence_transformers import SentenceTransformer
16
- import numpy as np
17
- from sklearn.metrics.pairwise import cosine_similarity
18
- VECTOR_AVAILABLE = True
19
- except ImportError:
20
- VECTOR_AVAILABLE = False
21
-
22
- # ============ CONFIGURATION ============
23
- DATA_DIR = Path(__file__).parent.parent / "data"
24
- MAX_RESULTS = 3
25
-
26
- CSV_CONFIG = {
27
- "style": {
28
- "file": "styles.csv",
29
- "search_cols": ["Style Category", "Keywords", "Best For", "Type"],
30
- "output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity"]
31
- },
32
- "prompt": {
33
- "file": "prompts.csv",
34
- "search_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords"],
35
- "output_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords", "Implementation Checklist"]
36
- },
37
- "color": {
38
- "file": "colors.csv",
39
- "search_cols": ["Product Type", "Keywords", "Notes"],
40
- "output_cols": ["Product Type", "Keywords", "Primary (Hex)", "Secondary (Hex)", "CTA (Hex)", "Background (Hex)", "Text (Hex)", "Border (Hex)", "Notes"]
41
- },
42
- "chart": {
43
- "file": "charts.csv",
44
- "search_cols": ["Data Type", "Keywords", "Best Chart Type", "Accessibility Notes"],
45
- "output_cols": ["Data Type", "Keywords", "Best Chart Type", "Secondary Options", "Color Guidance", "Accessibility Notes", "Library Recommendation", "Interactive Level"]
46
- },
47
- "landing": {
48
- "file": "landing.csv",
49
- "search_cols": ["Pattern Name", "Keywords", "Conversion Optimization", "Section Order"],
50
- "output_cols": ["Pattern Name", "Keywords", "Section Order", "Primary CTA Placement", "Color Strategy", "Conversion Optimization"]
51
- },
52
- "product": {
53
- "file": "products.csv",
54
- "search_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Key Considerations"],
55
- "output_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Secondary Styles", "Landing Page Pattern", "Dashboard Style (if applicable)", "Color Palette Focus"]
56
- },
57
- "ux": {
58
- "file": "ux-guidelines.csv",
59
- "search_cols": ["Category", "Issue", "Description", "Platform"],
60
- "output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
61
- },
62
- "typography": {
63
- "file": "typography.csv",
64
- "search_cols": ["Font Pairing Name", "Category", "Mood/Style Keywords", "Best For", "Heading Font", "Body Font"],
65
- "output_cols": ["Font Pairing Name", "Category", "Heading Font", "Body Font", "Mood/Style Keywords", "Best For", "Google Fonts URL", "CSS Import", "Tailwind Config", "Notes"]
66
- },
67
- "pages": {
68
- "file": "pages.csv",
69
- "search_cols": ["Page Type", "Keywords", "Section Order", "Key Components", "Layout Pattern", "Best For"],
70
- "output_cols": ["Page Type", "Keywords", "Section Order", "Key Components", "Layout Pattern", "Color Strategy", "Recommended Effects", "Best For", "Considerations"]
71
- }
72
- }
73
-
74
- STACK_CONFIG = {
75
- "html-tailwind": {"file": "stacks/html-tailwind.csv"},
76
- "react": {"file": "stacks/react.csv"},
77
- "nextjs": {"file": "stacks/nextjs.csv"},
78
- "vue": {"file": "stacks/vue.csv"},
79
- "svelte": {"file": "stacks/svelte.csv"},
80
- "swiftui": {"file": "stacks/swiftui.csv"},
81
- "react-native": {"file": "stacks/react-native.csv"},
82
- "flutter": {"file": "stacks/flutter.csv"}
83
- }
84
-
85
- # Common columns for all stacks
86
- _STACK_COLS = {
87
- "search_cols": ["Category", "Guideline", "Description", "Do", "Don't"],
88
- "output_cols": ["Category", "Guideline", "Description", "Do", "Don't", "Code Good", "Code Bad", "Severity", "Docs URL"]
89
- }
90
-
91
- AVAILABLE_STACKS = list(STACK_CONFIG.keys())
92
-
93
-
94
- # ============ BM25 IMPLEMENTATION ============
95
- class BM25:
96
- """BM25 ranking algorithm for text search"""
97
-
98
- def __init__(self, k1=1.5, b=0.75):
99
- self.k1 = k1
100
- self.b = b
101
- self.corpus = []
102
- self.doc_lengths = []
103
- self.avgdl = 0
104
- self.idf = {}
105
- self.doc_freqs = defaultdict(int)
106
- self.N = 0
107
-
108
- def tokenize(self, text):
109
- """Lowercase, split, remove punctuation, filter short words"""
110
- text = re.sub(r'[^\w\s]', ' ', str(text).lower())
111
- return [w for w in text.split() if len(w) > 2]
112
-
113
- def fit(self, documents):
114
- """Build BM25 index from documents"""
115
- self.corpus = [self.tokenize(doc) for doc in documents]
116
- self.N = len(self.corpus)
117
- if self.N == 0:
118
- return
119
- self.doc_lengths = [len(doc) for doc in self.corpus]
120
- self.avgdl = sum(self.doc_lengths) / self.N
121
-
122
- for doc in self.corpus:
123
- seen = set()
124
- for word in doc:
125
- if word not in seen:
126
- self.doc_freqs[word] += 1
127
- seen.add(word)
128
-
129
- for word, freq in self.doc_freqs.items():
130
- self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
131
-
132
- def score(self, query):
133
- """Score all documents against query"""
134
- query_tokens = self.tokenize(query)
135
- scores = []
136
-
137
- for idx, doc in enumerate(self.corpus):
138
- score = 0
139
- doc_len = self.doc_lengths[idx]
140
- term_freqs = defaultdict(int)
141
- for word in doc:
142
- term_freqs[word] += 1
143
-
144
- for token in query_tokens:
145
- if token in self.idf:
146
- tf = term_freqs[token]
147
- idf = self.idf[token]
148
- numerator = tf * (self.k1 + 1)
149
- denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
150
- score += idf * numerator / denominator
151
-
152
- scores.append((idx, score))
153
-
154
- return sorted(scores, key=lambda x: x[1], reverse=True)
155
-
156
-
157
- # ============ VECTOR SEARCH IMPLEMENTATION ============
158
- class VectorSearch:
159
- """Vector-based semantic search using sentence transformers"""
160
-
161
- def __init__(self):
162
- if not VECTOR_AVAILABLE:
163
- raise ImportError(
164
- "Vector search requires sentence-transformers and scikit-learn. "
165
- "Install with: pip install sentence-transformers scikit-learn"
166
- )
167
- # Use lightweight, fast model
168
- self.model = SentenceTransformer('all-MiniLM-L6-v2')
169
- self.embeddings = None
170
- self.documents = None
171
-
172
- def fit(self, documents):
173
- """Encode documents into vectors"""
174
- self.documents = documents
175
- if len(documents) == 0:
176
- self.embeddings = np.array([])
177
- return
178
- # Encode without progress bar for cleaner output
179
- self.embeddings = self.model.encode(documents, show_progress_bar=False, convert_to_numpy=True)
180
-
181
- def search(self, query, top_k=3):
182
- """Search using cosine similarity"""
183
- if self.embeddings is None or len(self.embeddings) == 0:
184
- return []
185
-
186
- # Encode query
187
- query_embedding = self.model.encode([query], show_progress_bar=False, convert_to_numpy=True)
188
-
189
- # Calculate cosine similarity
190
- similarities = cosine_similarity(query_embedding, self.embeddings)[0]
191
-
192
- # Get top k indices
193
- top_indices = np.argsort(similarities)[::-1][:top_k]
194
-
195
- # Return (index, score) tuples
196
- return [(int(idx), float(similarities[idx])) for idx in top_indices if similarities[idx] > 0]
197
-
198
-
199
- # ============ SEARCH FUNCTIONS ============
200
- def _load_csv(filepath):
201
- """Load CSV and return list of dicts"""
202
- with open(filepath, 'r', encoding='utf-8') as f:
203
- return list(csv.DictReader(f))
204
-
205
-
206
- def _search_csv(filepath, search_cols, output_cols, query, max_results, mode='bm25'):
207
- """Core search function using BM25, Vector, or Hybrid"""
208
- if not filepath.exists():
209
- return []
210
-
211
- data = _load_csv(filepath)
212
-
213
- # Build documents from search columns
214
- documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
215
-
216
- if len(documents) == 0:
217
- return []
218
-
219
- # Choose search mode
220
- if mode == 'bm25' or (mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE):
221
- # BM25 search (default or fallback)
222
- bm25 = BM25()
223
- bm25.fit(documents)
224
- ranked = bm25.score(query)
225
-
226
- # Get top results with score > 0
227
- results = []
228
- for idx, score in ranked[:max_results]:
229
- if score > 0:
230
- row = data[idx]
231
- results.append({col: row.get(col, "") for col in output_cols if col in row})
232
-
233
- return results
234
-
235
- elif mode == 'vector':
236
- # Vector search
237
- vector_search = VectorSearch()
238
- vector_search.fit(documents)
239
- ranked = vector_search.search(query, top_k=max_results)
240
-
241
- results = []
242
- for idx, score in ranked:
243
- row = data[idx]
244
- results.append({col: row.get(col, "") for col in output_cols if col in row})
245
-
246
- return results
247
-
248
- elif mode == 'hybrid':
249
- # Hybrid search: combine BM25 + Vector
250
- # Get more results from each to combine
251
- search_count = max_results * 2
252
-
253
- # BM25 results
254
- bm25 = BM25()
255
- bm25.fit(documents)
256
- bm25_ranked = bm25.score(query)
257
- bm25_scores = {idx: score for idx, score in bm25_ranked[:search_count] if score > 0}
258
-
259
- # Vector results
260
- vector_search = VectorSearch()
261
- vector_search.fit(documents)
262
- vector_ranked = vector_search.search(query, top_k=search_count)
263
- vector_scores = {idx: score for idx, score in vector_ranked}
264
-
265
- # Normalize scores to 0-1 range
266
- max_bm25 = max(bm25_scores.values()) if bm25_scores else 1.0
267
- max_vector = max(vector_scores.values()) if vector_scores else 1.0
268
-
269
- # Combine scores (alpha = 0.5 for balanced, can be adjusted)
270
- alpha = 0.5
271
- combined_scores = {}
272
- all_indices = set(bm25_scores.keys()) | set(vector_scores.keys())
273
-
274
- for idx in all_indices:
275
- bm25_norm = (bm25_scores.get(idx, 0) / max_bm25) if max_bm25 > 0 else 0
276
- vector_norm = (vector_scores.get(idx, 0) / max_vector) if max_vector > 0 else 0
277
- combined_scores[idx] = alpha * bm25_norm + (1 - alpha) * vector_norm
278
-
279
- # Sort by combined score
280
- sorted_indices = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
281
-
282
- # Get top results
283
- results = []
284
- for idx, score in sorted_indices[:max_results]:
285
- if score > 0:
286
- row = data[idx]
287
- results.append({col: row.get(col, "") for col in output_cols if col in row})
288
-
289
- return results
290
-
291
- else:
292
- # Unknown mode, fallback to BM25
293
- return _search_csv(filepath, search_cols, output_cols, query, max_results, mode='bm25')
294
-
295
-
296
- def detect_domain(query):
297
- """Auto-detect the most relevant domain from query"""
298
- query_lower = query.lower()
299
-
300
- domain_keywords = {
301
- "color": ["color", "palette", "hex", "#", "rgb"],
302
- "chart": ["chart", "graph", "visualization", "trend", "bar", "pie", "scatter", "heatmap", "funnel"],
303
- "landing": ["landing", "page", "cta", "conversion", "hero", "testimonial", "pricing", "section"],
304
- "product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard"],
305
- "prompt": ["prompt", "css", "implementation", "variable", "checklist", "tailwind"],
306
- "style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora"],
307
- "ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
308
- "typography": ["font", "typography", "heading", "serif", "sans"],
309
- "pages": ["page", "home", "homepage", "about", "post", "article", "blog", "category", "pricing", "faq", "contact", "product", "shop", "catalog", "details", "single"]
310
- }
311
-
312
- scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
313
- best = max(scores, key=scores.get)
314
- return best if scores[best] > 0 else "style"
315
-
316
-
317
- def search(query, domain=None, max_results=MAX_RESULTS, mode='bm25'):
318
- """
319
- Main search function with auto-domain detection
320
-
321
- Args:
322
- query: Search query string
323
- domain: Domain to search (auto-detected if None)
324
- max_results: Maximum number of results
325
- mode: Search mode - 'bm25' (default), 'vector', or 'hybrid'
326
-
327
- Returns:
328
- Dictionary with search results
329
- """
330
- if domain is None:
331
- domain = detect_domain(query)
332
-
333
- config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
334
- filepath = DATA_DIR / config["file"]
335
-
336
- if not filepath.exists():
337
- return {"error": f"File not found: {filepath}", "domain": domain}
338
-
339
- # Validate mode
340
- if mode not in ['bm25', 'vector', 'hybrid']:
341
- mode = 'bm25'
342
-
343
- # Fallback to BM25 if vector dependencies not available
344
- if mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE:
345
- mode = 'bm25'
346
-
347
- results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results, mode)
348
-
349
- return {
350
- "domain": domain,
351
- "query": query,
352
- "file": config["file"],
353
- "mode": mode,
354
- "count": len(results),
355
- "results": results
356
- }
357
-
358
-
359
- def search_stack(query, stack, max_results=MAX_RESULTS, mode='bm25'):
360
- """Search stack-specific guidelines"""
361
- if stack not in STACK_CONFIG:
362
- return {"error": f"Unknown stack: {stack}. Available: {', '.join(AVAILABLE_STACKS)}"}
363
-
364
- filepath = DATA_DIR / STACK_CONFIG[stack]["file"]
365
-
366
- if not filepath.exists():
367
- return {"error": f"Stack file not found: {filepath}", "stack": stack}
368
-
369
- # Validate mode and fallback if needed
370
- if mode not in ['bm25', 'vector', 'hybrid']:
371
- mode = 'bm25'
372
- if mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE:
373
- mode = 'bm25'
374
-
375
- results = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results, mode)
376
-
377
- return {
378
- "domain": "stack",
379
- "stack": stack,
380
- "query": query,
381
- "file": STACK_CONFIG[stack]["file"],
382
- "mode": mode,
383
- "count": len(results),
384
- "results": results
385
- }
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ UI/UX Builder Core - BM25 search engine for UI/UX style guides
5
+ """
6
+
7
+ import csv
8
+ import re
9
+ from pathlib import Path
10
+ from math import log
11
+ from collections import defaultdict
12
+
13
+ # Optional dependencies for vector search
14
+ try:
15
+ from sentence_transformers import SentenceTransformer
16
+ import numpy as np
17
+ from sklearn.metrics.pairwise import cosine_similarity
18
+ VECTOR_AVAILABLE = True
19
+ except ImportError:
20
+ VECTOR_AVAILABLE = False
21
+
22
+ # ============ CONFIGURATION ============
23
+ DATA_DIR = Path(__file__).parent.parent / "data"
24
+ MAX_RESULTS = 3
25
+
26
+ CSV_CONFIG = {
27
+ "style": {
28
+ "file": "styles.csv",
29
+ "search_cols": ["Style Category", "Keywords", "Best For", "Type"],
30
+ "output_cols": ["Style Category", "Type", "Keywords", "Primary Colors", "Effects & Animation", "Best For", "Performance", "Accessibility", "Framework Compatibility", "Complexity"]
31
+ },
32
+ "prompt": {
33
+ "file": "prompts.csv",
34
+ "search_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords"],
35
+ "output_cols": ["Style Category", "AI Prompt Keywords (Copy-Paste Ready)", "CSS/Technical Keywords", "Implementation Checklist"]
36
+ },
37
+ "color": {
38
+ "file": "colors.csv",
39
+ "search_cols": ["Product Type", "Keywords", "Notes"],
40
+ "output_cols": ["Product Type", "Keywords", "Primary (Hex)", "Secondary (Hex)", "CTA (Hex)", "Background (Hex)", "Text (Hex)", "Border (Hex)", "Notes"]
41
+ },
42
+ "chart": {
43
+ "file": "charts.csv",
44
+ "search_cols": ["Data Type", "Keywords", "Best Chart Type", "Accessibility Notes"],
45
+ "output_cols": ["Data Type", "Keywords", "Best Chart Type", "Secondary Options", "Color Guidance", "Accessibility Notes", "Library Recommendation", "Interactive Level"]
46
+ },
47
+ "landing": {
48
+ "file": "landing.csv",
49
+ "search_cols": ["Pattern Name", "Keywords", "Conversion Optimization", "Section Order"],
50
+ "output_cols": ["Pattern Name", "Keywords", "Section Order", "Primary CTA Placement", "Color Strategy", "Conversion Optimization"]
51
+ },
52
+ "product": {
53
+ "file": "products.csv",
54
+ "search_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Key Considerations"],
55
+ "output_cols": ["Product Type", "Keywords", "Primary Style Recommendation", "Secondary Styles", "Landing Page Pattern", "Dashboard Style (if applicable)", "Color Palette Focus"]
56
+ },
57
+ "ux": {
58
+ "file": "ux-guidelines.csv",
59
+ "search_cols": ["Category", "Issue", "Description", "Platform"],
60
+ "output_cols": ["Category", "Issue", "Platform", "Description", "Do", "Don't", "Code Example Good", "Code Example Bad", "Severity"]
61
+ },
62
+ "typography": {
63
+ "file": "typography.csv",
64
+ "search_cols": ["Font Pairing Name", "Category", "Mood/Style Keywords", "Best For", "Heading Font", "Body Font"],
65
+ "output_cols": ["Font Pairing Name", "Category", "Heading Font", "Body Font", "Mood/Style Keywords", "Best For", "Google Fonts URL", "CSS Import", "Tailwind Config", "Notes"]
66
+ },
67
+ "pages": {
68
+ "file": "pages.csv",
69
+ "search_cols": ["Page Type", "Keywords", "Section Order", "Key Components", "Layout Pattern", "Best For"],
70
+ "output_cols": ["Page Type", "Keywords", "Section Order", "Key Components", "Layout Pattern", "Color Strategy", "Recommended Effects", "Best For", "Considerations"]
71
+ },
72
+ "design-system": {
73
+ "file": "design-systems.csv",
74
+ "search_cols": ["Brand", "Category", "Keywords", "Design_Philosophy", "Best_For", "Heading_Font", "Shadow_Style"],
75
+ "output_cols": ["Brand", "Folder", "Category", "Keywords", "Theme", "Primary_Color", "Accent_Color", "Background", "Text_Color", "Heading_Font", "Body_Font", "Mono_Font", "Border_Radius", "Shadow_Style", "Design_Philosophy", "Best_For", "Agent_Quick_Prompt"]
76
+ }
77
+ }
78
+
79
+ STACK_CONFIG = {
80
+ "html-tailwind": {"file": "stacks/html-tailwind.csv"},
81
+ "react": {"file": "stacks/react.csv"},
82
+ "nextjs": {"file": "stacks/nextjs.csv"},
83
+ "vue": {"file": "stacks/vue.csv"},
84
+ "svelte": {"file": "stacks/svelte.csv"},
85
+ "swiftui": {"file": "stacks/swiftui.csv"},
86
+ "react-native": {"file": "stacks/react-native.csv"},
87
+ "flutter": {"file": "stacks/flutter.csv"}
88
+ }
89
+
90
+ # Common columns for all stacks
91
+ _STACK_COLS = {
92
+ "search_cols": ["Category", "Guideline", "Description", "Do", "Don't"],
93
+ "output_cols": ["Category", "Guideline", "Description", "Do", "Don't", "Code Good", "Code Bad", "Severity", "Docs URL"]
94
+ }
95
+
96
+ AVAILABLE_STACKS = list(STACK_CONFIG.keys())
97
+
98
+
99
+ # ============ BM25 IMPLEMENTATION ============
100
+ class BM25:
101
+ """BM25 ranking algorithm for text search"""
102
+
103
+ def __init__(self, k1=1.5, b=0.75):
104
+ self.k1 = k1
105
+ self.b = b
106
+ self.corpus = []
107
+ self.doc_lengths = []
108
+ self.avgdl = 0
109
+ self.idf = {}
110
+ self.doc_freqs = defaultdict(int)
111
+ self.N = 0
112
+
113
+ def tokenize(self, text):
114
+ """Lowercase, split, remove punctuation, filter short words"""
115
+ text = re.sub(r'[^\w\s]', ' ', str(text).lower())
116
+ return [w for w in text.split() if len(w) > 2]
117
+
118
+ def fit(self, documents):
119
+ """Build BM25 index from documents"""
120
+ self.corpus = [self.tokenize(doc) for doc in documents]
121
+ self.N = len(self.corpus)
122
+ if self.N == 0:
123
+ return
124
+ self.doc_lengths = [len(doc) for doc in self.corpus]
125
+ self.avgdl = sum(self.doc_lengths) / self.N
126
+
127
+ for doc in self.corpus:
128
+ seen = set()
129
+ for word in doc:
130
+ if word not in seen:
131
+ self.doc_freqs[word] += 1
132
+ seen.add(word)
133
+
134
+ for word, freq in self.doc_freqs.items():
135
+ self.idf[word] = log((self.N - freq + 0.5) / (freq + 0.5) + 1)
136
+
137
+ def score(self, query):
138
+ """Score all documents against query"""
139
+ query_tokens = self.tokenize(query)
140
+ scores = []
141
+
142
+ for idx, doc in enumerate(self.corpus):
143
+ score = 0
144
+ doc_len = self.doc_lengths[idx]
145
+ term_freqs = defaultdict(int)
146
+ for word in doc:
147
+ term_freqs[word] += 1
148
+
149
+ for token in query_tokens:
150
+ if token in self.idf:
151
+ tf = term_freqs[token]
152
+ idf = self.idf[token]
153
+ numerator = tf * (self.k1 + 1)
154
+ denominator = tf + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl)
155
+ score += idf * numerator / denominator
156
+
157
+ scores.append((idx, score))
158
+
159
+ return sorted(scores, key=lambda x: x[1], reverse=True)
160
+
161
+
162
+ # ============ VECTOR SEARCH IMPLEMENTATION ============
163
+ class VectorSearch:
164
+ """Vector-based semantic search using sentence transformers"""
165
+
166
+ def __init__(self):
167
+ if not VECTOR_AVAILABLE:
168
+ raise ImportError(
169
+ "Vector search requires sentence-transformers and scikit-learn. "
170
+ "Install with: pip install sentence-transformers scikit-learn"
171
+ )
172
+ # Use lightweight, fast model
173
+ self.model = SentenceTransformer('all-MiniLM-L6-v2')
174
+ self.embeddings = None
175
+ self.documents = None
176
+
177
+ def fit(self, documents):
178
+ """Encode documents into vectors"""
179
+ self.documents = documents
180
+ if len(documents) == 0:
181
+ self.embeddings = np.array([])
182
+ return
183
+ # Encode without progress bar for cleaner output
184
+ self.embeddings = self.model.encode(documents, show_progress_bar=False, convert_to_numpy=True)
185
+
186
+ def search(self, query, top_k=3):
187
+ """Search using cosine similarity"""
188
+ if self.embeddings is None or len(self.embeddings) == 0:
189
+ return []
190
+
191
+ # Encode query
192
+ query_embedding = self.model.encode([query], show_progress_bar=False, convert_to_numpy=True)
193
+
194
+ # Calculate cosine similarity
195
+ similarities = cosine_similarity(query_embedding, self.embeddings)[0]
196
+
197
+ # Get top k indices
198
+ top_indices = np.argsort(similarities)[::-1][:top_k]
199
+
200
+ # Return (index, score) tuples
201
+ return [(int(idx), float(similarities[idx])) for idx in top_indices if similarities[idx] > 0]
202
+
203
+
204
+ # ============ SEARCH FUNCTIONS ============
205
+ def _load_csv(filepath):
206
+ """Load CSV and return list of dicts"""
207
+ with open(filepath, 'r', encoding='utf-8') as f:
208
+ return list(csv.DictReader(f))
209
+
210
+
211
+ def _search_csv(filepath, search_cols, output_cols, query, max_results, mode='bm25'):
212
+ """Core search function using BM25, Vector, or Hybrid"""
213
+ if not filepath.exists():
214
+ return []
215
+
216
+ data = _load_csv(filepath)
217
+
218
+ # Build documents from search columns
219
+ documents = [" ".join(str(row.get(col, "")) for col in search_cols) for row in data]
220
+
221
+ if len(documents) == 0:
222
+ return []
223
+
224
+ # Choose search mode
225
+ if mode == 'bm25' or (mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE):
226
+ # BM25 search (default or fallback)
227
+ bm25 = BM25()
228
+ bm25.fit(documents)
229
+ ranked = bm25.score(query)
230
+
231
+ # Get top results with score > 0
232
+ results = []
233
+ for idx, score in ranked[:max_results]:
234
+ if score > 0:
235
+ row = data[idx]
236
+ results.append({col: row.get(col, "") for col in output_cols if col in row})
237
+
238
+ return results
239
+
240
+ elif mode == 'vector':
241
+ # Vector search
242
+ vector_search = VectorSearch()
243
+ vector_search.fit(documents)
244
+ ranked = vector_search.search(query, top_k=max_results)
245
+
246
+ results = []
247
+ for idx, score in ranked:
248
+ row = data[idx]
249
+ results.append({col: row.get(col, "") for col in output_cols if col in row})
250
+
251
+ return results
252
+
253
+ elif mode == 'hybrid':
254
+ # Hybrid search: combine BM25 + Vector
255
+ # Get more results from each to combine
256
+ search_count = max_results * 2
257
+
258
+ # BM25 results
259
+ bm25 = BM25()
260
+ bm25.fit(documents)
261
+ bm25_ranked = bm25.score(query)
262
+ bm25_scores = {idx: score for idx, score in bm25_ranked[:search_count] if score > 0}
263
+
264
+ # Vector results
265
+ vector_search = VectorSearch()
266
+ vector_search.fit(documents)
267
+ vector_ranked = vector_search.search(query, top_k=search_count)
268
+ vector_scores = {idx: score for idx, score in vector_ranked}
269
+
270
+ # Normalize scores to 0-1 range
271
+ max_bm25 = max(bm25_scores.values()) if bm25_scores else 1.0
272
+ max_vector = max(vector_scores.values()) if vector_scores else 1.0
273
+
274
+ # Combine scores (alpha = 0.5 for balanced, can be adjusted)
275
+ alpha = 0.5
276
+ combined_scores = {}
277
+ all_indices = set(bm25_scores.keys()) | set(vector_scores.keys())
278
+
279
+ for idx in all_indices:
280
+ bm25_norm = (bm25_scores.get(idx, 0) / max_bm25) if max_bm25 > 0 else 0
281
+ vector_norm = (vector_scores.get(idx, 0) / max_vector) if max_vector > 0 else 0
282
+ combined_scores[idx] = alpha * bm25_norm + (1 - alpha) * vector_norm
283
+
284
+ # Sort by combined score
285
+ sorted_indices = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
286
+
287
+ # Get top results
288
+ results = []
289
+ for idx, score in sorted_indices[:max_results]:
290
+ if score > 0:
291
+ row = data[idx]
292
+ results.append({col: row.get(col, "") for col in output_cols if col in row})
293
+
294
+ return results
295
+
296
+ else:
297
+ # Unknown mode, fallback to BM25
298
+ return _search_csv(filepath, search_cols, output_cols, query, max_results, mode='bm25')
299
+
300
+
301
+ def detect_domain(query):
302
+ """Auto-detect the most relevant domain from query"""
303
+ query_lower = query.lower()
304
+
305
+ domain_keywords = {
306
+ "color": ["color", "palette", "hex", "#", "rgb"],
307
+ "chart": ["chart", "graph", "visualization", "trend", "bar", "pie", "scatter", "heatmap", "funnel"],
308
+ "landing": ["landing", "page", "cta", "conversion", "hero", "testimonial", "pricing", "section"],
309
+ "product": ["saas", "ecommerce", "e-commerce", "fintech", "healthcare", "gaming", "portfolio", "crypto", "dashboard"],
310
+ "prompt": ["prompt", "css", "implementation", "variable", "checklist", "tailwind"],
311
+ "style": ["style", "design", "ui", "minimalism", "glassmorphism", "neumorphism", "brutalism", "dark mode", "flat", "aurora"],
312
+ "ux": ["ux", "usability", "accessibility", "wcag", "touch", "scroll", "animation", "keyboard", "navigation", "mobile"],
313
+ "typography": ["font", "typography", "heading", "serif", "sans"],
314
+ "pages": ["page", "home", "homepage", "about", "post", "article", "blog", "category", "pricing", "faq", "contact", "product", "shop", "catalog", "details", "single"],
315
+ "design-system": ["design system", "brand", "stripe", "vercel", "linear", "supabase", "notion", "figma", "airbnb", "spotify", "apple", "inspired", "like", "style of", "look of", "design-md", "design.md"]
316
+ }
317
+
318
+ scores = {domain: sum(1 for kw in keywords if kw in query_lower) for domain, keywords in domain_keywords.items()}
319
+ best = max(scores, key=scores.get)
320
+ return best if scores[best] > 0 else "style"
321
+
322
+
323
+ def search(query, domain=None, max_results=MAX_RESULTS, mode='bm25'):
324
+ """
325
+ Main search function with auto-domain detection
326
+
327
+ Args:
328
+ query: Search query string
329
+ domain: Domain to search (auto-detected if None)
330
+ max_results: Maximum number of results
331
+ mode: Search mode - 'bm25' (default), 'vector', or 'hybrid'
332
+
333
+ Returns:
334
+ Dictionary with search results
335
+ """
336
+ if domain is None:
337
+ domain = detect_domain(query)
338
+
339
+ config = CSV_CONFIG.get(domain, CSV_CONFIG["style"])
340
+ filepath = DATA_DIR / config["file"]
341
+
342
+ if not filepath.exists():
343
+ return {"error": f"File not found: {filepath}", "domain": domain}
344
+
345
+ # Validate mode
346
+ if mode not in ['bm25', 'vector', 'hybrid']:
347
+ mode = 'bm25'
348
+
349
+ # Fallback to BM25 if vector dependencies not available
350
+ if mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE:
351
+ mode = 'bm25'
352
+
353
+ results = _search_csv(filepath, config["search_cols"], config["output_cols"], query, max_results, mode)
354
+
355
+ return {
356
+ "domain": domain,
357
+ "query": query,
358
+ "file": config["file"],
359
+ "mode": mode,
360
+ "count": len(results),
361
+ "results": results
362
+ }
363
+
364
+
365
+ def search_stack(query, stack, max_results=MAX_RESULTS, mode='bm25'):
366
+ """Search stack-specific guidelines"""
367
+ if stack not in STACK_CONFIG:
368
+ return {"error": f"Unknown stack: {stack}. Available: {', '.join(AVAILABLE_STACKS)}"}
369
+
370
+ filepath = DATA_DIR / STACK_CONFIG[stack]["file"]
371
+
372
+ if not filepath.exists():
373
+ return {"error": f"Stack file not found: {filepath}", "stack": stack}
374
+
375
+ # Validate mode and fallback if needed
376
+ if mode not in ['bm25', 'vector', 'hybrid']:
377
+ mode = 'bm25'
378
+ if mode in ['vector', 'hybrid'] and not VECTOR_AVAILABLE:
379
+ mode = 'bm25'
380
+
381
+ results = _search_csv(filepath, _STACK_COLS["search_cols"], _STACK_COLS["output_cols"], query, max_results, mode)
382
+
383
+ return {
384
+ "domain": "stack",
385
+ "stack": stack,
386
+ "query": query,
387
+ "file": STACK_CONFIG[stack]["file"],
388
+ "mode": mode,
389
+ "count": len(results),
390
+ "results": results
391
+ }