""" Agent 3: Difference Analyzer (Hybrid Approach) Uses HF Vision Model + Screenshot Analysis to detect visual differences Detects layout, color, spacing, typography, and other visual differences """ from typing import Dict, Any, List from state_schema import WorkflowState, VisualDifference import os from PIL import Image import numpy as np class ScreenshotComparator: """Compares Figma and website screenshots for visual differences.""" def __init__(self): """Initialize the comparator.""" self.differences = [] def compare_screenshots(self, figma_path: str, website_path: str, viewport: str) -> List[VisualDifference]: """ Compare Figma and website screenshots. Args: figma_path: Path to Figma screenshot website_path: Path to website screenshot viewport: Viewport name (desktop/mobile) Returns: List of detected visual differences """ differences = [] if not os.path.exists(figma_path) or not os.path.exists(website_path): return differences try: # Load images figma_img = Image.open(figma_path) website_img = Image.open(website_path) # Analyze different aspects differences.extend(self._analyze_layout(figma_img, website_img, viewport)) differences.extend(self._analyze_colors(figma_img, website_img, viewport)) differences.extend(self._analyze_structure(figma_img, website_img, viewport)) except Exception as e: print(f"Error comparing screenshots: {str(e)}") return differences def _analyze_layout(self, figma_img: Image.Image, website_img: Image.Image, viewport: str) -> List[VisualDifference]: """Analyze layout differences.""" differences = [] figma_size = figma_img.size website_size = website_img.size # Check width differences if figma_size[0] != website_size[0]: width_diff_percent = abs(figma_size[0] - website_size[0]) / figma_size[0] * 100 severity = "High" if width_diff_percent > 10 else "Medium" diff = VisualDifference( category="layout", severity=severity, issue_id="1.1", title="Container width differs", description=f"Design: {figma_size[0]}px vs Website: {website_size[0]}px ({width_diff_percent:.1f}% difference)", design_value=str(figma_size[0]), website_value=str(website_size[0]), viewport=viewport, confidence=0.95, detection_method="screenshot_comparison" ) differences.append(diff) # Check height differences if figma_size[1] != website_size[1]: height_diff_percent = abs(figma_size[1] - website_size[1]) / figma_size[1] * 100 severity = "High" if height_diff_percent > 10 else "Medium" diff = VisualDifference( category="layout", severity=severity, issue_id="1.2", title="Page height differs", description=f"Design: {figma_size[1]}px vs Website: {website_size[1]}px ({height_diff_percent:.1f}% difference)", design_value=str(figma_size[1]), website_value=str(website_size[1]), viewport=viewport, confidence=0.95, detection_method="screenshot_comparison" ) differences.append(diff) return differences def _analyze_colors(self, figma_img: Image.Image, website_img: Image.Image, viewport: str) -> List[VisualDifference]: """Analyze color differences.""" differences = [] try: # Convert to RGB figma_rgb = figma_img.convert('RGB') website_rgb = website_img.convert('RGB') # Resize to same size for comparison (use smaller size for performance) compare_size = (400, 300) figma_resized = figma_rgb.resize(compare_size) website_resized = website_rgb.resize(compare_size) # Convert to numpy arrays figma_array = np.array(figma_resized, dtype=np.float32) website_array = np.array(website_resized, dtype=np.float32) # Calculate color difference (mean absolute difference) color_diff = np.mean(np.abs(figma_array - website_array)) # If significant color difference, flag it if color_diff > 15: severity = "High" if color_diff > 40 else "Medium" diff = VisualDifference( category="colors", severity=severity, issue_id="3.1", title="Color scheme differs significantly", description=f"Significant color difference detected (delta: {color_diff:.1f})", design_value="Design colors", website_value="Website colors", viewport=viewport, confidence=0.8, detection_method="pixel_analysis" ) differences.append(diff) except Exception as e: pass return differences def _analyze_structure(self, figma_img: Image.Image, website_img: Image.Image, viewport: str) -> List[VisualDifference]: """Analyze structural/layout differences.""" differences = [] try: # Convert to grayscale for edge detection figma_gray = figma_img.convert('L') website_gray = website_img.convert('L') # Resize to same size compare_size = (400, 300) figma_resized = figma_gray.resize(compare_size) website_resized = website_gray.resize(compare_size) # Convert to numpy arrays figma_array = np.array(figma_resized, dtype=np.float32) website_array = np.array(website_resized, dtype=np.float32) # Calculate structural difference (MSE) mse = np.mean((figma_array - website_array) ** 2) # Normalize MSE to 0-100 scale structural_diff = min(100, mse / 255) if structural_diff > 10: severity = "High" if structural_diff > 30 else "Medium" diff = VisualDifference( category="layout", severity=severity, issue_id="1.3", title="Layout structure differs", description=f"Visual structure difference detected (score: {structural_diff:.1f})", design_value="Design layout", website_value="Website layout", viewport=viewport, confidence=0.75, detection_method="structural_analysis" ) differences.append(diff) except Exception as e: pass return differences class DifferenceAnalyzer: """ Agent 3: Difference Analyzer Analyzes visual differences between Figma designs and website implementations """ def __init__(self): """Initialize the analyzer.""" self.comparator = ScreenshotComparator() def analyze_differences(self, state: WorkflowState) -> WorkflowState: """ Analyze visual differences between Figma and website screenshots. Args: state: Current workflow state Returns: Updated state with analysis results """ print("\nšŸ” Agent 3: Difference Analyzer - Analyzing Visual Differences...") try: all_differences = [] # Compare screenshots for each viewport for viewport in ["desktop", "mobile"]: figma_key = f"{viewport}" website_key = f"{viewport}" figma_path = state.get("figma_screenshots", {}).get(figma_key) website_path = state.get("website_screenshots", {}).get(website_key) if figma_path and website_path: print(f" šŸ“Š Comparing {viewport} screenshots...") differences = self.comparator.compare_screenshots( figma_path, website_path, viewport ) all_differences.extend(differences) print(f" āœ“ Found {len(differences)} differences") else: print(f" āš ļø Missing screenshots for {viewport}") # Calculate similarity score total_differences = len(all_differences) high_severity = len([d for d in all_differences if d.severity == "High"]) medium_severity = len([d for d in all_differences if d.severity == "Medium"]) low_severity = len([d for d in all_differences if d.severity == "Low"]) # Similarity score: 100 - (differences weighted by severity) severity_weight = (high_severity * 10) + (medium_severity * 5) + (low_severity * 1) similarity_score = max(0, 100 - severity_weight) state["visual_differences"] = [d.to_dict() if hasattr(d, "to_dict") else d for d in all_differences] state["similarity_score"] = similarity_score state["status"] = "analysis_complete" print(f"\n šŸ“ˆ Analysis Summary:") print(f" - Total differences: {total_differences}") print(f" - High severity: {high_severity}") print(f" - Medium severity: {medium_severity}") print(f" - Low severity: {low_severity}") print(f" - Similarity score: {similarity_score:.1f}/100") return state except Exception as e: print(f" āŒ Error analyzing differences: {str(e)}") import traceback traceback.print_exc() state["status"] = "analysis_failed" state["error_message"] = f"Agent 3 Error: {str(e)}" return state def agent_3_node(state: Dict) -> Dict: """ LangGraph node for Agent 3 (Difference Analyzer). Args: state: Current workflow state Returns: Updated state """ # Convert dict to WorkflowState if needed if isinstance(state, dict): workflow_state = WorkflowState(**state) else: workflow_state = state # Create analyzer and analyze differences analyzer = DifferenceAnalyzer() updated_state = analyzer.analyze_differences(workflow_state) # Convert back to dict for LangGraph return updated_state.__dict__