File size: 11,354 Bytes
43b3474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
"""
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__