| | """ |
| | Agent 1C: Semantic Color Analyzer |
| | Design System Extractor v2 |
| | |
| | Persona: Design System Semanticist |
| | |
| | Responsibilities: |
| | - Analyze colors based on their actual CSS usage |
| | - Categorize into semantic roles (brand, text, background, border, feedback) |
| | - Use LLM to understand color relationships and hierarchy |
| | - Provide structured output for Stage 1 UI and Stage 2 analysis |
| | """ |
| |
|
| | import json |
| | import re |
| | from typing import Optional, Callable |
| | from datetime import datetime |
| |
|
| | from core.color_utils import ( |
| | parse_color, |
| | get_contrast_with_white, |
| | get_contrast_with_black, |
| | check_wcag_compliance, |
| | ) |
| |
|
| |
|
| | class SemanticColorAnalyzer: |
| | """ |
| | Analyzes extracted colors and categorizes them by semantic role. |
| | |
| | Uses LLM to understand: |
| | - Which colors are brand/primary colors (used on buttons, CTAs) |
| | - Which colors are for text (used with 'color' property) |
| | - Which colors are backgrounds (used with 'background-color') |
| | - Which colors are borders (used with 'border-color') |
| | - Which colors are feedback states (error, success, warning) |
| | """ |
| | |
| | def __init__(self, llm_provider=None): |
| | """ |
| | Initialize the semantic analyzer. |
| | |
| | Args: |
| | llm_provider: Optional LLM provider for AI analysis. |
| | If None, uses rule-based fallback. |
| | """ |
| | self.llm_provider = llm_provider |
| | self.analysis_result = {} |
| | self.logs = [] |
| | |
| | def log(self, message: str): |
| | """Add timestamped log message.""" |
| | timestamp = datetime.now().strftime("%H:%M:%S") |
| | self.logs.append(f"[{timestamp}] {message}") |
| | |
| | def get_logs(self) -> str: |
| | """Get all logs as string.""" |
| | return "\n".join(self.logs) |
| | |
| | def _prepare_color_data_for_llm(self, colors: dict) -> str: |
| | """ |
| | Prepare color data in a format optimized for LLM analysis. |
| | |
| | Args: |
| | colors: Dict of color tokens with metadata |
| | |
| | Returns: |
| | Formatted string for LLM prompt |
| | """ |
| | color_entries = [] |
| | |
| | for name, token in colors.items(): |
| | |
| | if hasattr(token, 'value'): |
| | hex_val = token.value |
| | frequency = token.frequency |
| | contexts = token.contexts if hasattr(token, 'contexts') else [] |
| | elements = token.elements if hasattr(token, 'elements') else [] |
| | css_props = token.css_properties if hasattr(token, 'css_properties') else [] |
| | else: |
| | hex_val = token.get('value', '#000000') |
| | frequency = token.get('frequency', 0) |
| | contexts = token.get('contexts', []) |
| | elements = token.get('elements', []) |
| | css_props = token.get('css_properties', []) |
| | |
| | |
| | contrast_white = get_contrast_with_white(hex_val) |
| | contrast_black = get_contrast_with_black(hex_val) |
| | |
| | |
| | try: |
| | r = int(hex_val[1:3], 16) |
| | g = int(hex_val[3:5], 16) |
| | b = int(hex_val[5:7], 16) |
| | luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 |
| | |
| | |
| | max_c = max(r, g, b) |
| | min_c = min(r, g, b) |
| | saturation = (max_c - min_c) / 255 if max_c > 0 else 0 |
| | except: |
| | luminance = 0.5 |
| | saturation = 0 |
| | |
| | entry = { |
| | "hex": hex_val, |
| | "name": name, |
| | "frequency": frequency, |
| | "css_properties": css_props[:5], |
| | "elements": elements[:5], |
| | "contexts": contexts[:3], |
| | "luminance": round(luminance, 2), |
| | "saturation": round(saturation, 2), |
| | "contrast_on_white": round(contrast_white, 2), |
| | "contrast_on_black": round(contrast_black, 2), |
| | "aa_compliant_on_white": contrast_white >= 4.5, |
| | } |
| | color_entries.append(entry) |
| | |
| | |
| | color_entries.sort(key=lambda x: -x['frequency']) |
| | |
| | |
| | return json.dumps(color_entries[:50], indent=2) |
| | |
| | def _build_llm_prompt(self, color_data: str) -> str: |
| | """Build the prompt for LLM semantic analysis.""" |
| | |
| | return f"""You are a Design System Analyst specializing in color semantics. |
| | |
| | TASK: Analyze these extracted colors and categorize them by their semantic role in the UI. |
| | |
| | EXTRACTED COLORS (sorted by frequency): |
| | {color_data} |
| | |
| | ANALYSIS RULES: |
| | 1. BRAND/PRIMARY colors are typically: |
| | - Used on buttons, links, CTAs (elements: button, a, input[type=submit]) |
| | - Applied via background-color on interactive elements |
| | - Saturated (saturation > 0.3) and not gray |
| | - High frequency on interactive elements |
| | |
| | 2. TEXT colors are typically: |
| | - Applied via "color" CSS property (not background-color) |
| | - Used on text elements (p, span, h1-h6, label) |
| | - Form a hierarchy: primary (darkest), secondary (medium), muted (lightest) |
| | - Low saturation (grays) |
| | |
| | 3. BACKGROUND colors are typically: |
| | - Applied via "background-color" on containers |
| | - Used on div, section, main, body, card elements |
| | - Light colors (luminance > 0.8) for light themes |
| | - May include dark backgrounds for inverse sections |
| | |
| | 4. BORDER colors are typically: |
| | - Applied via border-color properties |
| | - Often gray/neutral |
| | - Lower frequency than text/background |
| | |
| | 5. FEEDBACK colors are: |
| | - Red variants = error |
| | - Green variants = success |
| | - Yellow/orange = warning |
| | - Blue variants = info |
| | - Often used with specific class contexts |
| | |
| | OUTPUT FORMAT (JSON): |
| | {{ |
| | "brand": {{ |
| | "primary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "secondary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "accent": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}} |
| | }}, |
| | "text": {{ |
| | "primary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "secondary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "muted": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "inverse": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}} |
| | }}, |
| | "background": {{ |
| | "primary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "secondary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "tertiary": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "inverse": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}} |
| | }}, |
| | "border": {{ |
| | "default": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "strong": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}} |
| | }}, |
| | "feedback": {{ |
| | "error": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "success": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "warning": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}}, |
| | "info": {{"hex": "#xxx", "confidence": "high|medium|low", "reason": "..."}} |
| | }}, |
| | "summary": {{ |
| | "total_colors_analyzed": 50, |
| | "brand_colors_found": 2, |
| | "has_clear_hierarchy": true, |
| | "accessibility_notes": "..." |
| | }} |
| | }} |
| | |
| | IMPORTANT: |
| | - Only include roles where you found a matching color |
| | - Set confidence based on how certain you are |
| | - Provide brief reasoning for each categorization |
| | - If no color fits a role, omit that key |
| | |
| | Return ONLY valid JSON, no other text.""" |
| |
|
| | def _rule_based_analysis(self, colors: dict) -> dict: |
| | """ |
| | Fallback rule-based analysis when LLM is not available. |
| | |
| | Uses heuristics based on: |
| | - CSS properties (color vs background-color vs border-color) |
| | - Element types (button, a, p, div, etc.) |
| | - Color characteristics (saturation, luminance) |
| | - Frequency |
| | """ |
| | self.log(" Using rule-based analysis (no LLM)") |
| | |
| | result = { |
| | "brand": {}, |
| | "text": {}, |
| | "background": {}, |
| | "border": {}, |
| | "feedback": {}, |
| | "summary": { |
| | "total_colors_analyzed": len(colors), |
| | "brand_colors_found": 0, |
| | "has_clear_hierarchy": False, |
| | "accessibility_notes": "", |
| | "method": "rule-based" |
| | } |
| | } |
| | |
| | |
| | brand_candidates = [] |
| | text_candidates = [] |
| | background_candidates = [] |
| | border_candidates = [] |
| | feedback_candidates = {"error": [], "success": [], "warning": [], "info": []} |
| | |
| | for name, token in colors.items(): |
| | |
| | if hasattr(token, 'value'): |
| | hex_val = token.value |
| | frequency = token.frequency |
| | contexts = token.contexts if hasattr(token, 'contexts') else [] |
| | elements = token.elements if hasattr(token, 'elements') else [] |
| | css_props = token.css_properties if hasattr(token, 'css_properties') else [] |
| | else: |
| | hex_val = token.get('value', '#000000') |
| | frequency = token.get('frequency', 0) |
| | contexts = token.get('contexts', []) |
| | elements = token.get('elements', []) |
| | css_props = token.get('css_properties', []) |
| | |
| | |
| | try: |
| | r = int(hex_val[1:3], 16) |
| | g = int(hex_val[3:5], 16) |
| | b = int(hex_val[5:7], 16) |
| | luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 |
| | max_c = max(r, g, b) |
| | min_c = min(r, g, b) |
| | saturation = (max_c - min_c) / 255 if max_c > 0 else 0 |
| | |
| | |
| | if max_c == min_c: |
| | hue = 0 |
| | elif max_c == r: |
| | hue = 60 * ((g - b) / (max_c - min_c) % 6) |
| | elif max_c == g: |
| | hue = 60 * ((b - r) / (max_c - min_c) + 2) |
| | else: |
| | hue = 60 * ((r - g) / (max_c - min_c) + 4) |
| | except: |
| | luminance = 0.5 |
| | saturation = 0 |
| | hue = 0 |
| | |
| | color_info = { |
| | "hex": hex_val, |
| | "name": name, |
| | "frequency": frequency, |
| | "luminance": luminance, |
| | "saturation": saturation, |
| | "hue": hue, |
| | "css_props": css_props, |
| | "elements": elements, |
| | "contexts": contexts, |
| | } |
| | |
| | |
| | |
| | |
| | interactive_elements = ['button', 'a', 'input', 'select', 'submit', 'btn', 'cta'] |
| | is_interactive = any(el in str(elements).lower() for el in interactive_elements) |
| | has_bg_prop = any('background' in str(p).lower() for p in css_props) |
| | |
| | |
| | if saturation > 0.25 and is_interactive and has_bg_prop: |
| | brand_candidates.append(color_info) |
| | |
| | elif saturation > 0.35 and frequency > 15: |
| | brand_candidates.append(color_info) |
| | |
| | elif saturation > 0.5 and frequency > 5: |
| | brand_candidates.append(color_info) |
| | |
| | elif 160 <= hue <= 200 and saturation > 0.4 and frequency > 10: |
| | brand_candidates.append(color_info) |
| | |
| | elif 60 <= hue <= 90 and saturation > 0.5 and frequency > 5: |
| | brand_candidates.append(color_info) |
| | |
| | |
| | has_color_prop = any(p == 'color' or (p.endswith('-color') and 'background' not in p and 'border' not in p) |
| | for p in css_props) |
| | text_elements = ['p', 'span', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'label', 'div', 'text'] |
| | is_text_element = any(el in str(elements).lower() for el in text_elements) |
| | |
| | |
| | if saturation < 0.15 and (has_color_prop or 'text' in str(contexts).lower()): |
| | text_candidates.append(color_info) |
| | elif saturation < 0.1 and 0.1 < luminance < 0.8: |
| | text_candidates.append(color_info) |
| | elif saturation < 0.1 and luminance < 0.5 and frequency > 50: |
| | text_candidates.append(color_info) |
| | |
| | if saturation < 0.15 and (has_color_prop or 'text' in str(contexts).lower()): |
| | text_candidates.append(color_info) |
| | elif saturation < 0.1 and luminance < 0.7 and is_text_element: |
| | text_candidates.append(color_info) |
| | |
| | |
| | container_elements = ['div', 'section', 'main', 'body', 'article', 'header', 'footer', 'card'] |
| | is_container = any(el in str(elements).lower() for el in container_elements) |
| | |
| | if has_bg_prop and (is_container or 'background' in str(contexts).lower()): |
| | if saturation < 0.15: |
| | background_candidates.append(color_info) |
| | |
| | |
| | has_border_prop = any('border' in str(p).lower() for p in css_props) |
| | |
| | if has_border_prop or 'border' in str(contexts).lower(): |
| | border_candidates.append(color_info) |
| | |
| | |
| | if saturation > 0.3: |
| | if 0 <= hue <= 30 or 330 <= hue <= 360: |
| | feedback_candidates["error"].append(color_info) |
| | elif 90 <= hue <= 150: |
| | feedback_candidates["success"].append(color_info) |
| | elif 30 <= hue <= 60: |
| | feedback_candidates["warning"].append(color_info) |
| | elif 180 <= hue <= 250: |
| | feedback_candidates["info"].append(color_info) |
| | |
| | |
| | |
| | |
| | brand_candidates.sort(key=lambda x: -(x['frequency'] * x['saturation'])) |
| | if brand_candidates: |
| | result["brand"]["primary"] = { |
| | "hex": brand_candidates[0]["hex"], |
| | "confidence": "high" if brand_candidates[0]["frequency"] > 20 else "medium", |
| | "reason": f"Most frequent saturated color on interactive elements (freq: {brand_candidates[0]['frequency']})" |
| | } |
| | result["summary"]["brand_colors_found"] += 1 |
| | if len(brand_candidates) > 1: |
| | result["brand"]["secondary"] = { |
| | "hex": brand_candidates[1]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Second most frequent brand color (freq: {brand_candidates[1]['frequency']})" |
| | } |
| | result["summary"]["brand_colors_found"] += 1 |
| | |
| | |
| | text_candidates.sort(key=lambda x: x['luminance']) |
| | if text_candidates: |
| | result["text"]["primary"] = { |
| | "hex": text_candidates[0]["hex"], |
| | "confidence": "high" if text_candidates[0]["luminance"] < 0.3 else "medium", |
| | "reason": f"Darkest text color (luminance: {text_candidates[0]['luminance']:.2f})" |
| | } |
| | if len(text_candidates) > 1: |
| | |
| | mid_idx = len(text_candidates) // 2 |
| | result["text"]["secondary"] = { |
| | "hex": text_candidates[mid_idx]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Mid-tone text color (luminance: {text_candidates[mid_idx]['luminance']:.2f})" |
| | } |
| | if len(text_candidates) > 2: |
| | result["text"]["muted"] = { |
| | "hex": text_candidates[-1]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Lightest text color (luminance: {text_candidates[-1]['luminance']:.2f})" |
| | } |
| | |
| | |
| | if len(text_candidates) >= 3: |
| | result["summary"]["has_clear_hierarchy"] = True |
| | |
| | |
| | background_candidates.sort(key=lambda x: -x['luminance']) |
| | if background_candidates: |
| | result["background"]["primary"] = { |
| | "hex": background_candidates[0]["hex"], |
| | "confidence": "high" if background_candidates[0]["luminance"] > 0.9 else "medium", |
| | "reason": f"Lightest background (luminance: {background_candidates[0]['luminance']:.2f})" |
| | } |
| | if len(background_candidates) > 1: |
| | result["background"]["secondary"] = { |
| | "hex": background_candidates[1]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Secondary background (luminance: {background_candidates[1]['luminance']:.2f})" |
| | } |
| | |
| | dark_bgs = [c for c in background_candidates if c['luminance'] < 0.2] |
| | if dark_bgs: |
| | result["background"]["inverse"] = { |
| | "hex": dark_bgs[0]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Dark background for inverse sections (luminance: {dark_bgs[0]['luminance']:.2f})" |
| | } |
| | |
| | |
| | border_candidates.sort(key=lambda x: -x['frequency']) |
| | if border_candidates: |
| | result["border"]["default"] = { |
| | "hex": border_candidates[0]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Most common border color (freq: {border_candidates[0]['frequency']})" |
| | } |
| | |
| | |
| | for feedback_type, candidates in feedback_candidates.items(): |
| | if candidates: |
| | candidates.sort(key=lambda x: -x['frequency']) |
| | result["feedback"][feedback_type] = { |
| | "hex": candidates[0]["hex"], |
| | "confidence": "medium", |
| | "reason": f"Detected {feedback_type} color by hue analysis" |
| | } |
| | |
| | return result |
| | |
| | async def analyze_with_llm(self, colors: dict, log_callback: Optional[Callable] = None) -> dict: |
| | """ |
| | Analyze colors using LLM for semantic categorization. |
| | |
| | Args: |
| | colors: Dict of color tokens |
| | log_callback: Optional callback for logging |
| | |
| | Returns: |
| | Semantic analysis result |
| | """ |
| | def log(msg): |
| | self.log(msg) |
| | if log_callback: |
| | log_callback(msg) |
| | |
| | log("") |
| | log("=" * 60) |
| | log("🧠 SEMANTIC COLOR ANALYSIS (LLM)") |
| | log("=" * 60) |
| | log("") |
| | |
| | |
| | log(" 📊 Preparing color data for analysis...") |
| | color_data = self._prepare_color_data_for_llm(colors) |
| | log(f" ✅ Prepared {min(50, len(colors))} colors for analysis") |
| | |
| | |
| | if self.llm_provider is None: |
| | log(" ⚠️ No LLM provider configured, using rule-based analysis") |
| | self.analysis_result = self._rule_based_analysis(colors) |
| | else: |
| | try: |
| | log(" 🤖 Calling LLM for semantic analysis...") |
| | |
| | prompt = self._build_llm_prompt(color_data) |
| | |
| | |
| | response = await self.llm_provider.generate( |
| | prompt=prompt, |
| | max_tokens=2000, |
| | temperature=0.3, |
| | ) |
| | |
| | log(" ✅ LLM response received") |
| | |
| | |
| | try: |
| | |
| | json_match = re.search(r'\{[\s\S]*\}', response) |
| | if json_match: |
| | self.analysis_result = json.loads(json_match.group()) |
| | self.analysis_result["summary"]["method"] = "llm" |
| | log(" ✅ Successfully parsed LLM analysis") |
| | else: |
| | raise ValueError("No JSON found in response") |
| | |
| | except json.JSONDecodeError as e: |
| | log(f" ⚠️ Failed to parse LLM response: {e}") |
| | log(" 🔄 Falling back to rule-based analysis") |
| | self.analysis_result = self._rule_based_analysis(colors) |
| | |
| | except Exception as e: |
| | log(f" ❌ LLM analysis failed: {str(e)}") |
| | log(" 🔄 Falling back to rule-based analysis") |
| | self.analysis_result = self._rule_based_analysis(colors) |
| | |
| | |
| | self._log_analysis_results(log) |
| | |
| | return self.analysis_result |
| | |
| | def analyze_sync(self, colors: dict, log_callback: Optional[Callable] = None) -> dict: |
| | """ |
| | Synchronous analysis using rule-based approach. |
| | |
| | Args: |
| | colors: Dict of color tokens |
| | log_callback: Optional callback for logging |
| | |
| | Returns: |
| | Semantic analysis result |
| | """ |
| | def log(msg): |
| | self.log(msg) |
| | if log_callback: |
| | log_callback(msg) |
| | |
| | log("") |
| | log("=" * 60) |
| | log("🧠 SEMANTIC COLOR ANALYSIS") |
| | log("=" * 60) |
| | log("") |
| | |
| | log(f" 📊 Analyzing {len(colors)} colors...") |
| | |
| | self.analysis_result = self._rule_based_analysis(colors) |
| | |
| | |
| | self._log_analysis_results(log) |
| | |
| | return self.analysis_result |
| | |
| | def _log_analysis_results(self, log: Callable): |
| | """Log the analysis results in a formatted way.""" |
| | |
| | result = self.analysis_result |
| | |
| | log("") |
| | log("📊 SEMANTIC ANALYSIS RESULTS:") |
| | log("") |
| | |
| | |
| | if result.get("brand"): |
| | log(" 🎨 BRAND COLORS:") |
| | for role, data in result["brand"].items(): |
| | if data: |
| | log(f" {role}: {data['hex']} ({data['confidence']})") |
| | log(f" └─ {data['reason']}") |
| | |
| | |
| | if result.get("text"): |
| | log("") |
| | log(" 📝 TEXT COLORS:") |
| | for role, data in result["text"].items(): |
| | if data: |
| | log(f" {role}: {data['hex']} ({data['confidence']})") |
| | |
| | |
| | if result.get("background"): |
| | log("") |
| | log(" 🖼️ BACKGROUND COLORS:") |
| | for role, data in result["background"].items(): |
| | if data: |
| | log(f" {role}: {data['hex']} ({data['confidence']})") |
| | |
| | |
| | if result.get("border"): |
| | log("") |
| | log(" 📏 BORDER COLORS:") |
| | for role, data in result["border"].items(): |
| | if data: |
| | log(f" {role}: {data['hex']} ({data['confidence']})") |
| | |
| | |
| | if result.get("feedback"): |
| | log("") |
| | log(" 🚨 FEEDBACK COLORS:") |
| | for role, data in result["feedback"].items(): |
| | if data: |
| | log(f" {role}: {data['hex']} ({data['confidence']})") |
| | |
| | |
| | summary = result.get("summary", {}) |
| | log("") |
| | log(" 📈 SUMMARY:") |
| | log(f" Total colors analyzed: {summary.get('total_colors_analyzed', 0)}") |
| | log(f" Brand colors found: {summary.get('brand_colors_found', 0)}") |
| | log(f" Clear hierarchy: {'Yes' if summary.get('has_clear_hierarchy') else 'No'}") |
| | log(f" Analysis method: {summary.get('method', 'unknown')}") |
| | log("") |
| |
|
| |
|
| | def generate_semantic_preview_html(analysis_result: dict) -> str: |
| | """ |
| | Generate HTML preview showing colors organized by semantic role. |
| | |
| | Args: |
| | analysis_result: Output from SemanticColorAnalyzer |
| | |
| | Returns: |
| | HTML string for Gradio HTML component |
| | """ |
| | |
| | |
| | if not analysis_result: |
| | return ''' |
| | <div class="sem-warning-box" style="padding: 40px; text-align: center; background: #fff3cd; border-radius: 8px; border: 1px solid #ffc107;"> |
| | <p style="color: #856404; font-size: 14px; margin: 0;"> |
| | ⚠️ Semantic analysis did not produce results. Check the logs for errors. |
| | </p> |
| | </div> |
| | <style> |
| | .dark .sem-warning-box { background: #422006 !important; border-color: #b45309 !important; } |
| | .dark .sem-warning-box p { color: #fde68a !important; } |
| | </style> |
| | ''' |
| | |
| | def color_card(hex_val: str, role: str, confidence: str, reason: str = "") -> str: |
| | """Generate HTML for a single color card.""" |
| | |
| | try: |
| | r = int(hex_val[1:3], 16) |
| | g = int(hex_val[3:5], 16) |
| | b = int(hex_val[5:7], 16) |
| | luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 |
| | text_color = "#1a1a1a" if luminance > 0.5 else "#ffffff" |
| | except: |
| | text_color = "#1a1a1a" |
| | |
| | confidence_badge = { |
| | "high": '<span class="confidence high">High</span>', |
| | "medium": '<span class="confidence medium">Medium</span>', |
| | "low": '<span class="confidence low">Low</span>', |
| | }.get(confidence, "") |
| | |
| | return f''' |
| | <div class="sem-color-card"> |
| | <div class="sem-color-swatch" style="background-color: {hex_val};"> |
| | <span class="sem-hex-label" style="color: {text_color};">{hex_val}</span> |
| | </div> |
| | <div class="sem-color-details"> |
| | <div class="sem-role-name">{role.replace("_", " ").title()}</div> |
| | {confidence_badge} |
| | </div> |
| | </div> |
| | ''' |
| | |
| | def category_section(title: str, icon: str, colors: dict) -> str: |
| | """Generate HTML for a category section.""" |
| | if not colors: |
| | return "" |
| | |
| | cards_html = "" |
| | for role, data in colors.items(): |
| | if data and isinstance(data, dict) and "hex" in data: |
| | cards_html += color_card( |
| | data["hex"], |
| | role, |
| | data.get("confidence", "medium"), |
| | data.get("reason", "") |
| | ) |
| | |
| | if not cards_html: |
| | return "" |
| | |
| | return f''' |
| | <div class="sem-category-section"> |
| | <h3 class="sem-category-title">{icon} {title}</h3> |
| | <div class="sem-color-grid"> |
| | {cards_html} |
| | </div> |
| | </div> |
| | ''' |
| | |
| | |
| | sections_html = "" |
| | sections_html += category_section("Brand Colors", "🎨", analysis_result.get("brand", {})) |
| | sections_html += category_section("Text Colors", "📝", analysis_result.get("text", {})) |
| | sections_html += category_section("Background Colors", "🖼️", analysis_result.get("background", {})) |
| | sections_html += category_section("Border Colors", "📏", analysis_result.get("border", {})) |
| | sections_html += category_section("Feedback Colors", "🚨", analysis_result.get("feedback", {})) |
| | |
| | |
| | if not sections_html.strip(): |
| | return ''' |
| | <div class="sem-warning-box" style="padding: 40px; text-align: center; background: #fff3cd; border-radius: 8px; border: 1px solid #ffc107;"> |
| | <p style="color: #856404; font-size: 14px; margin: 0;"> |
| | ⚠️ No semantic color categories were detected. The colors may not have enough context data (elements, CSS properties) for classification. |
| | </p> |
| | </div> |
| | <style> |
| | .dark .sem-warning-box { background: #422006 !important; border-color: #b45309 !important; } |
| | .dark .sem-warning-box p { color: #fde68a !important; } |
| | </style> |
| | ''' |
| | |
| | |
| | summary = analysis_result.get("summary", {}) |
| | summary_html = f''' |
| | <div class="sem-summary-section"> |
| | <h3 class="sem-summary-title">📈 Analysis Summary</h3> |
| | <div class="sem-summary-stats"> |
| | <div class="sem-stat"> |
| | <span class="sem-stat-value">{summary.get("total_colors_analyzed", 0)}</span> |
| | <span class="sem-stat-label">Colors Analyzed</span> |
| | </div> |
| | <div class="sem-stat"> |
| | <span class="sem-stat-value">{summary.get("brand_colors_found", 0)}</span> |
| | <span class="sem-stat-label">Brand Colors</span> |
| | </div> |
| | <div class="sem-stat"> |
| | <span class="sem-stat-value">{"✓" if summary.get("has_clear_hierarchy") else "✗"}</span> |
| | <span class="sem-stat-label">Clear Hierarchy</span> |
| | </div> |
| | <div class="sem-stat"> |
| | <span class="sem-stat-value">{summary.get("method", "rule-based").upper()}</span> |
| | <span class="sem-stat-label">Analysis Method</span> |
| | </div> |
| | </div> |
| | </div> |
| | ''' |
| | |
| | html = f''' |
| | <style> |
| | .sem-preview {{ |
| | font-family: system-ui, -apple-system, sans-serif; |
| | padding: 20px; |
| | background: #f5f5f5 !important; |
| | border-radius: 12px; |
| | }} |
| | |
| | .sem-category-section {{ |
| | margin-bottom: 24px; |
| | background: #ffffff !important; |
| | border-radius: 8px; |
| | padding: 16px; |
| | border: 1px solid #d0d0d0 !important; |
| | }} |
| | |
| | .sem-category-title {{ |
| | font-size: 16px; |
| | font-weight: 700; |
| | color: #1a1a1a !important; |
| | margin: 0 0 16px 0; |
| | padding-bottom: 8px; |
| | border-bottom: 2px solid #e0e0e0 !important; |
| | }} |
| | |
| | .sem-color-grid {{ |
| | display: grid; |
| | grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); |
| | gap: 12px; |
| | }} |
| | |
| | .sem-color-card {{ |
| | background: #f0f0f0 !important; |
| | border-radius: 8px; |
| | overflow: hidden; |
| | border: 1px solid #d0d0d0 !important; |
| | }} |
| | |
| | .sem-color-swatch {{ |
| | height: 80px; |
| | display: flex; |
| | align-items: center; |
| | justify-content: center; |
| | }} |
| | |
| | .sem-hex-label {{ |
| | font-family: 'SF Mono', Monaco, monospace; |
| | font-size: 12px; |
| | font-weight: 600; |
| | text-shadow: 0 1px 2px rgba(0,0,0,0.3); |
| | }} |
| | |
| | .sem-color-details {{ |
| | padding: 10px; |
| | text-align: center; |
| | background: #ffffff !important; |
| | }} |
| | |
| | .sem-role-name {{ |
| | font-size: 12px; |
| | font-weight: 600; |
| | color: #1a1a1a !important; |
| | margin-bottom: 4px; |
| | }} |
| | |
| | .sem-preview .confidence {{ |
| | font-size: 10px; |
| | padding: 2px 8px; |
| | border-radius: 10px; |
| | font-weight: 500; |
| | display: inline-block; |
| | }} |
| | |
| | .sem-preview .confidence.high {{ |
| | background: #dcfce7 !important; |
| | color: #166534 !important; |
| | }} |
| | |
| | .sem-preview .confidence.medium {{ |
| | background: #fef9c3 !important; |
| | color: #854d0e !important; |
| | }} |
| | |
| | .sem-preview .confidence.low {{ |
| | background: #fee2e2 !important; |
| | color: #991b1b !important; |
| | }} |
| | |
| | .sem-summary-section {{ |
| | background: #ffffff !important; |
| | border-radius: 8px; |
| | padding: 16px; |
| | border: 1px solid #d0d0d0 !important; |
| | }} |
| | |
| | .sem-summary-title {{ |
| | font-size: 16px; |
| | font-weight: 700; |
| | color: #1a1a1a !important; |
| | margin: 0 0 16px 0; |
| | }} |
| | |
| | .sem-summary-stats {{ |
| | display: grid; |
| | grid-template-columns: repeat(4, 1fr); |
| | gap: 16px; |
| | }} |
| | |
| | .sem-stat {{ |
| | text-align: center; |
| | padding: 12px; |
| | background: #f0f0f0 !important; |
| | border-radius: 8px; |
| | }} |
| | |
| | .sem-stat-value {{ |
| | display: block; |
| | font-size: 24px; |
| | font-weight: 700; |
| | color: #1a1a1a !important; |
| | }} |
| | |
| | .sem-stat-label {{ |
| | display: block; |
| | font-size: 11px; |
| | color: #555 !important; |
| | margin-top: 4px; |
| | }} |
| | |
| | /* Dark mode */ |
| | .dark .sem-preview {{ background: #0f172a !important; }} |
| | .dark .sem-category-section {{ background: #1e293b !important; border-color: #475569 !important; }} |
| | .dark .sem-category-title {{ color: #f1f5f9 !important; border-bottom-color: #475569 !important; }} |
| | .dark .sem-color-card {{ background: #334155 !important; border-color: #475569 !important; }} |
| | .dark .sem-color-details {{ background: #1e293b !important; }} |
| | .dark .sem-role-name {{ color: #f1f5f9 !important; }} |
| | .dark .sem-preview .confidence.high {{ background: #14532d !important; color: #86efac !important; }} |
| | .dark .sem-preview .confidence.medium {{ background: #422006 !important; color: #fde68a !important; }} |
| | .dark .sem-preview .confidence.low {{ background: #450a0a !important; color: #fca5a5 !important; }} |
| | .dark .sem-summary-section {{ background: #1e293b !important; border-color: #475569 !important; }} |
| | .dark .sem-summary-title {{ color: #f1f5f9 !important; }} |
| | .dark .sem-stat {{ background: #334155 !important; }} |
| | .dark .sem-stat-value {{ color: #f1f5f9 !important; }} |
| | .dark .sem-stat-label {{ color: #94a3b8 !important; }} |
| | </style> |
| | |
| | <div class="sem-preview"> |
| | {sections_html} |
| | {summary_html} |
| | </div> |
| | ''' |
| | |
| | return html |
| |
|