cursorflow 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

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.
@@ -0,0 +1,1316 @@
1
+ """
2
+ Mockup Comparator
3
+
4
+ Visual comparison system for mockup vs work-in-progress URLs.
5
+ Enables rapid iteration to match UI implementation to design mockups.
6
+ """
7
+
8
+ import asyncio
9
+ import time
10
+ import json
11
+ from typing import Dict, List, Optional, Any, Tuple
12
+ from pathlib import Path
13
+ import logging
14
+ from PIL import Image, ImageDraw, ImageChops
15
+ import numpy as np
16
+
17
+ from .browser_controller import BrowserController
18
+ from .css_iterator import CSSIterator
19
+
20
+
21
+ class MockupComparator:
22
+ """
23
+ Compare mockup designs with work-in-progress implementations
24
+
25
+ Provides visual diff analysis and iteration guidance for UI matching.
26
+ """
27
+
28
+ def __init__(self):
29
+ """Initialize mockup comparator"""
30
+ self.logger = logging.getLogger(__name__)
31
+
32
+ # Create artifacts in current working directory
33
+ self.artifacts_base = Path.cwd() / ".cursorflow" / "artifacts"
34
+ self.artifacts_base.mkdir(parents=True, exist_ok=True)
35
+
36
+ # Ensure subdirectories exist
37
+ (self.artifacts_base / "mockup_comparisons").mkdir(exist_ok=True)
38
+ (self.artifacts_base / "visual_diffs").mkdir(exist_ok=True)
39
+ (self.artifacts_base / "iteration_progress").mkdir(exist_ok=True)
40
+
41
+ # Initialize CSS iterator for implementation testing
42
+ self.css_iterator = CSSIterator()
43
+
44
+ async def compare_mockup_to_implementation(
45
+ self,
46
+ mockup_url: str,
47
+ implementation_url: str,
48
+ mockup_actions: Optional[List[Dict]] = None,
49
+ implementation_actions: Optional[List[Dict]] = None,
50
+ comparison_config: Optional[Dict] = None
51
+ ) -> Dict[str, Any]:
52
+ """
53
+ Compare mockup design to current implementation
54
+
55
+ Args:
56
+ mockup_url: URL of the design mockup/reference
57
+ implementation_url: URL of the work-in-progress implementation
58
+ mockup_actions: Optional actions to perform on mockup (clicks, scrolls, etc.)
59
+ implementation_actions: Optional actions to perform on implementation
60
+ comparison_config: {
61
+ "viewports": [{"width": 1440, "height": 900, "name": "desktop"}],
62
+ "diff_threshold": 0.1, # Sensitivity for visual differences
63
+ "ignore_regions": [{"x": 0, "y": 0, "width": 100, "height": 50}],
64
+ "focus_regions": [{"x": 100, "y": 100, "width": 800, "height": 600}]
65
+ }
66
+
67
+ Returns:
68
+ Complete comparison analysis for Cursor to analyze
69
+ """
70
+ try:
71
+ timestamp = int(time.time())
72
+ comparison_name = f"mockup_comparison_{timestamp}"
73
+
74
+ config = comparison_config or {}
75
+ viewports = config.get("viewports", [{"width": 1440, "height": 900, "name": "desktop"}])
76
+
77
+ self.logger.info(f"Starting mockup comparison: {mockup_url} vs {implementation_url}")
78
+
79
+ # Initialize dual browser sessions
80
+ mockup_browser = BrowserController(mockup_url, {"headless": True})
81
+ implementation_browser = BrowserController(implementation_url, {"headless": True})
82
+
83
+ await mockup_browser.initialize()
84
+ await implementation_browser.initialize()
85
+
86
+ try:
87
+ comparison_results = []
88
+
89
+ # Test across all specified viewports
90
+ for viewport in viewports:
91
+ viewport_name = viewport.get("name", f"{viewport['width']}x{viewport['height']}")
92
+ self.logger.info(f"Testing viewport: {viewport_name}")
93
+
94
+ # Set viewport for both browsers
95
+ await mockup_browser.set_viewport(viewport["width"], viewport["height"])
96
+ await implementation_browser.set_viewport(viewport["width"], viewport["height"])
97
+
98
+ # Navigate to initial pages
99
+ await mockup_browser.navigate("/")
100
+ await implementation_browser.navigate("/")
101
+
102
+ # Execute any required actions on mockup
103
+ if mockup_actions:
104
+ await self._execute_actions_on_browser(mockup_browser, mockup_actions)
105
+
106
+ # Execute any required actions on implementation
107
+ if implementation_actions:
108
+ await self._execute_actions_on_browser(implementation_browser, implementation_actions)
109
+
110
+ # Capture screenshots
111
+ mockup_screenshot = await self._capture_comparison_screenshot(
112
+ mockup_browser, f"{comparison_name}_mockup_{viewport_name}"
113
+ )
114
+ implementation_screenshot = await self._capture_comparison_screenshot(
115
+ implementation_browser, f"{comparison_name}_implementation_{viewport_name}"
116
+ )
117
+
118
+ # Perform visual comparison
119
+ visual_diff = await self._create_visual_diff(
120
+ mockup_screenshot, implementation_screenshot,
121
+ f"{comparison_name}_{viewport_name}", config
122
+ )
123
+
124
+ # Analyze layout differences
125
+ layout_analysis = await self._analyze_layout_differences(
126
+ mockup_browser, implementation_browser, viewport_name
127
+ )
128
+
129
+ # Capture element-level differences
130
+ element_analysis = await self._analyze_element_differences(
131
+ mockup_browser, implementation_browser, viewport_name
132
+ )
133
+
134
+ viewport_result = {
135
+ "viewport": viewport,
136
+ "mockup_screenshot": mockup_screenshot,
137
+ "implementation_screenshot": implementation_screenshot,
138
+ "visual_diff": visual_diff,
139
+ "layout_analysis": layout_analysis,
140
+ "element_analysis": element_analysis,
141
+ "timestamp": time.time()
142
+ }
143
+
144
+ comparison_results.append(viewport_result)
145
+
146
+ # Create comprehensive comparison report
147
+ comparison_report = {
148
+ "comparison_id": comparison_name,
149
+ "timestamp": time.time(),
150
+ "mockup_url": mockup_url,
151
+ "implementation_url": implementation_url,
152
+ "viewports_tested": len(viewports),
153
+ "results": comparison_results,
154
+ "summary": self._create_comparison_summary(comparison_results),
155
+ "recommendations": self._generate_improvement_recommendations(comparison_results)
156
+ }
157
+
158
+ # Save comparison report
159
+ report_path = self.artifacts_base / "mockup_comparisons" / f"{comparison_name}.json"
160
+ with open(report_path, 'w') as f:
161
+ json.dump(comparison_report, f, indent=2, default=str)
162
+
163
+ self.logger.info(f"Mockup comparison completed: {comparison_name}")
164
+ return comparison_report
165
+
166
+ finally:
167
+ await mockup_browser.cleanup()
168
+ await implementation_browser.cleanup()
169
+
170
+ except Exception as e:
171
+ self.logger.error(f"Mockup comparison failed: {e}")
172
+ return {"error": str(e), "comparison_id": comparison_name if 'comparison_name' in locals() else None}
173
+
174
+ async def iterative_ui_matching(
175
+ self,
176
+ mockup_url: str,
177
+ implementation_url: str,
178
+ css_improvements: List[Dict],
179
+ base_actions: Optional[List[Dict]] = None,
180
+ comparison_config: Optional[Dict] = None
181
+ ) -> Dict[str, Any]:
182
+ """
183
+ Iteratively improve implementation to match mockup
184
+
185
+ Args:
186
+ mockup_url: Reference mockup URL
187
+ implementation_url: Work-in-progress implementation URL
188
+ css_improvements: [
189
+ {
190
+ "name": "fix-header-spacing",
191
+ "css": ".header { padding: 2rem 0; }",
192
+ "rationale": "Match mockup header spacing"
193
+ }
194
+ ]
195
+ base_actions: Actions to perform before each comparison
196
+ comparison_config: Configuration for comparison sensitivity
197
+
198
+ Returns:
199
+ Iteration results with progress tracking toward mockup match
200
+ """
201
+ try:
202
+ timestamp = int(time.time())
203
+ iteration_session_id = f"ui_matching_{timestamp}"
204
+
205
+ self.logger.info(f"Starting iterative UI matching session: {iteration_session_id}")
206
+
207
+ # Capture baseline mockup state
208
+ baseline_comparison = await self.compare_mockup_to_implementation(
209
+ mockup_url, implementation_url,
210
+ base_actions, base_actions, comparison_config
211
+ )
212
+
213
+ if "error" in baseline_comparison:
214
+ return baseline_comparison
215
+
216
+ # Initialize implementation browser for CSS iteration
217
+ implementation_browser = BrowserController(implementation_url, {"headless": True})
218
+ await implementation_browser.initialize()
219
+
220
+ try:
221
+ # Execute base actions to set up page state
222
+ if base_actions:
223
+ await self._execute_actions_on_browser(implementation_browser, base_actions)
224
+
225
+ # Capture implementation baseline for CSS iteration
226
+ implementation_baseline = await self.css_iterator.capture_baseline(implementation_browser.page)
227
+
228
+ iteration_results = []
229
+
230
+ # Apply each CSS improvement and compare to mockup
231
+ for i, css_improvement in enumerate(css_improvements):
232
+ self.logger.info(f"Testing CSS improvement {i+1}/{len(css_improvements)}: {css_improvement.get('name', 'unnamed')}")
233
+
234
+ # Apply CSS change to implementation
235
+ css_result = await self.css_iterator.apply_css_and_capture(
236
+ implementation_browser.page, css_improvement, implementation_baseline,
237
+ suffix=f"_iteration_{i+1}"
238
+ )
239
+
240
+ # Compare improved implementation to mockup
241
+ improved_comparison = await self._compare_current_state_to_mockup(
242
+ mockup_url, implementation_browser,
243
+ f"{iteration_session_id}_iteration_{i+1}", comparison_config
244
+ )
245
+
246
+ # Calculate improvement metrics
247
+ improvement_metrics = self._calculate_improvement_metrics(
248
+ baseline_comparison, improved_comparison
249
+ )
250
+
251
+ iteration_result = {
252
+ "iteration_number": i + 1,
253
+ "css_change": css_improvement,
254
+ "css_result": css_result,
255
+ "mockup_comparison": improved_comparison,
256
+ "improvement_metrics": improvement_metrics,
257
+ "timestamp": time.time()
258
+ }
259
+
260
+ iteration_results.append(iteration_result)
261
+
262
+ # Small delay to let changes settle
263
+ await asyncio.sleep(0.2)
264
+
265
+ # Create comprehensive iteration report
266
+ iteration_report = {
267
+ "session_id": iteration_session_id,
268
+ "timestamp": time.time(),
269
+ "mockup_url": mockup_url,
270
+ "implementation_url": implementation_url,
271
+ "baseline_comparison": baseline_comparison,
272
+ "iterations": iteration_results,
273
+ "total_iterations": len(css_improvements),
274
+ "summary": self._create_iteration_summary(baseline_comparison, iteration_results),
275
+ "best_iteration": self._find_best_iteration(iteration_results),
276
+ "final_recommendations": self._generate_final_recommendations(iteration_results)
277
+ }
278
+
279
+ # Save iteration report
280
+ report_path = self.artifacts_base / "iteration_progress" / f"{iteration_session_id}.json"
281
+ with open(report_path, 'w') as f:
282
+ json.dump(iteration_report, f, indent=2, default=str)
283
+
284
+ self.logger.info(f"UI matching iteration completed: {iteration_session_id}")
285
+ return iteration_report
286
+
287
+ finally:
288
+ await implementation_browser.cleanup()
289
+
290
+ except Exception as e:
291
+ self.logger.error(f"Iterative UI matching failed: {e}")
292
+ return {"error": str(e), "session_id": iteration_session_id if 'iteration_session_id' in locals() else None}
293
+
294
+ async def _execute_actions_on_browser(self, browser: BrowserController, actions: List[Dict]):
295
+ """Execute actions on a specific browser instance"""
296
+ for action in actions:
297
+ if "navigate" in action:
298
+ path = action["navigate"]
299
+ await browser.navigate(path)
300
+ elif "click" in action:
301
+ selector = action["click"]
302
+ if isinstance(selector, dict):
303
+ selector = selector["selector"]
304
+ await browser.click(selector)
305
+ elif "wait_for" in action:
306
+ selector = action["wait_for"]
307
+ if isinstance(selector, dict):
308
+ selector = selector["selector"]
309
+ await browser.wait_for_element(selector)
310
+ elif "wait" in action:
311
+ await asyncio.sleep(action["wait"])
312
+ elif "scroll" in action:
313
+ scroll_config = action["scroll"]
314
+ await browser.page.evaluate(f"window.scrollTo({scroll_config.get('x', 0)}, {scroll_config.get('y', 0)})")
315
+
316
+ async def _capture_comparison_screenshot(self, browser: BrowserController, name: str) -> str:
317
+ """Capture screenshot for comparison"""
318
+ screenshot_path = str(self.artifacts_base / "mockup_comparisons" / f"{name}.png")
319
+ await browser.page.screenshot(path=screenshot_path, full_page=True)
320
+ return screenshot_path
321
+
322
+ async def _create_visual_diff(
323
+ self,
324
+ mockup_path: str,
325
+ implementation_path: str,
326
+ diff_name: str,
327
+ config: Dict
328
+ ) -> Dict[str, Any]:
329
+ """Create visual difference analysis between mockup and implementation"""
330
+ try:
331
+ # Load images
332
+ mockup_img = Image.open(mockup_path)
333
+ implementation_img = Image.open(implementation_path)
334
+
335
+ # Ensure images are same size for comparison
336
+ if mockup_img.size != implementation_img.size:
337
+ # Resize to match the larger dimension
338
+ max_width = max(mockup_img.width, implementation_img.width)
339
+ max_height = max(mockup_img.height, implementation_img.height)
340
+
341
+ mockup_img = mockup_img.resize((max_width, max_height), Image.Resampling.LANCZOS)
342
+ implementation_img = implementation_img.resize((max_width, max_height), Image.Resampling.LANCZOS)
343
+
344
+ # Create difference image
345
+ diff_img = ImageChops.difference(mockup_img, implementation_img)
346
+
347
+ # Create highlighted difference image
348
+ highlighted_diff = self._create_highlighted_diff(mockup_img, implementation_img, diff_img, config)
349
+
350
+ # Save difference images
351
+ diff_path = str(self.artifacts_base / "visual_diffs" / f"{diff_name}_diff.png")
352
+ highlighted_path = str(self.artifacts_base / "visual_diffs" / f"{diff_name}_highlighted.png")
353
+
354
+ diff_img.save(diff_path)
355
+ highlighted_diff.save(highlighted_path)
356
+
357
+ # Calculate difference metrics
358
+ diff_metrics = self._calculate_visual_diff_metrics(mockup_img, implementation_img, diff_img, config)
359
+
360
+ return {
361
+ "diff_image": diff_path,
362
+ "highlighted_diff": highlighted_path,
363
+ "metrics": diff_metrics,
364
+ "similarity_score": diff_metrics.get("similarity_percentage", 0),
365
+ "major_differences": diff_metrics.get("major_difference_regions", [])
366
+ }
367
+
368
+ except Exception as e:
369
+ self.logger.error(f"Visual diff creation failed: {e}")
370
+ return {"error": str(e)}
371
+
372
+ def _create_highlighted_diff(self, mockup_img: Image.Image, implementation_img: Image.Image, diff_img: Image.Image, config: Dict) -> Image.Image:
373
+ """Create highlighted difference image with colored regions"""
374
+ # Convert to RGBA for overlay
375
+ highlighted = implementation_img.convert("RGBA")
376
+ overlay = Image.new("RGBA", highlighted.size, (0, 0, 0, 0))
377
+
378
+ # Convert difference to numpy array for analysis
379
+ diff_array = np.array(diff_img.convert("L"))
380
+
381
+ # Find significant differences
382
+ threshold = config.get("diff_threshold", 0.1) * 255
383
+ significant_diff = diff_array > threshold
384
+
385
+ # Create overlay for differences
386
+ overlay_array = np.array(overlay)
387
+ overlay_array[significant_diff] = [255, 0, 0, 100] # Red highlight with transparency
388
+
389
+ overlay = Image.fromarray(overlay_array, "RGBA")
390
+
391
+ # Composite the overlay onto the implementation
392
+ highlighted = Image.alpha_composite(highlighted, overlay)
393
+
394
+ return highlighted.convert("RGB")
395
+
396
+ def _calculate_visual_diff_metrics(self, mockup_img: Image.Image, implementation_img: Image.Image, diff_img: Image.Image, config: Dict) -> Dict[str, Any]:
397
+ """Calculate detailed visual difference metrics"""
398
+ try:
399
+ # Convert to numpy arrays for analysis
400
+ mockup_array = np.array(mockup_img.convert("RGB"))
401
+ implementation_array = np.array(implementation_img.convert("RGB"))
402
+ diff_array = np.array(diff_img.convert("L"))
403
+
404
+ # Calculate overall similarity
405
+ total_pixels = diff_array.size
406
+ threshold = config.get("diff_threshold", 0.1) * 255
407
+ different_pixels = np.sum(diff_array > threshold)
408
+ similarity_percentage = ((total_pixels - different_pixels) / total_pixels) * 100
409
+
410
+ # Find major difference regions
411
+ major_diff_regions = self._find_difference_regions(diff_array, threshold)
412
+
413
+ # Calculate color difference metrics
414
+ color_metrics = self._calculate_color_metrics(mockup_array, implementation_array)
415
+
416
+ return {
417
+ "similarity_percentage": round(similarity_percentage, 2),
418
+ "different_pixels": int(different_pixels),
419
+ "total_pixels": int(total_pixels),
420
+ "major_difference_regions": major_diff_regions,
421
+ "color_metrics": color_metrics,
422
+ "threshold_used": threshold
423
+ }
424
+
425
+ except Exception as e:
426
+ self.logger.error(f"Visual diff metrics calculation failed: {e}")
427
+ return {"error": str(e)}
428
+
429
+ def _find_difference_regions(self, diff_array: np.ndarray, threshold: float) -> List[Dict]:
430
+ """Find regions with significant visual differences"""
431
+ # This is a simplified implementation - could be enhanced with computer vision
432
+ significant_diff = diff_array > threshold
433
+
434
+ # Find bounding boxes of difference regions
435
+ regions = []
436
+
437
+ # Simple region detection (could be improved with connected components)
438
+ if np.any(significant_diff):
439
+ rows, cols = np.where(significant_diff)
440
+ if len(rows) > 0:
441
+ regions.append({
442
+ "x": int(np.min(cols)),
443
+ "y": int(np.min(rows)),
444
+ "width": int(np.max(cols) - np.min(cols)),
445
+ "height": int(np.max(rows) - np.min(rows)),
446
+ "area": int(np.sum(significant_diff))
447
+ })
448
+
449
+ return regions
450
+
451
+ def _calculate_color_metrics(self, mockup_array: np.ndarray, implementation_array: np.ndarray) -> Dict[str, Any]:
452
+ """Calculate color-based difference metrics"""
453
+ try:
454
+ # Calculate average colors
455
+ mockup_avg_color = np.mean(mockup_array, axis=(0, 1))
456
+ implementation_avg_color = np.mean(implementation_array, axis=(0, 1))
457
+
458
+ # Calculate color distance
459
+ color_distance = np.linalg.norm(mockup_avg_color - implementation_avg_color)
460
+
461
+ return {
462
+ "mockup_avg_color": mockup_avg_color.tolist(),
463
+ "implementation_avg_color": implementation_avg_color.tolist(),
464
+ "color_distance": float(color_distance)
465
+ }
466
+
467
+ except Exception as e:
468
+ return {"error": str(e)}
469
+
470
+ async def _analyze_layout_differences(self, mockup_browser: BrowserController, implementation_browser: BrowserController, viewport_name: str) -> Dict[str, Any]:
471
+ """Analyze layout differences between mockup and implementation"""
472
+ try:
473
+ # Capture layout metrics from both pages
474
+ mockup_layout = await mockup_browser.page.evaluate("""
475
+ () => {
476
+ const elements = [];
477
+ const selectors = ['h1', 'h2', 'h3', 'nav', 'main', 'header', 'footer', '.btn', '.button', 'form', 'input'];
478
+
479
+ selectors.forEach(selector => {
480
+ document.querySelectorAll(selector).forEach((el, index) => {
481
+ const rect = el.getBoundingClientRect();
482
+ const styles = window.getComputedStyle(el);
483
+ elements.push({
484
+ selector: selector + (index > 0 ? `[${index}]` : ''),
485
+ x: rect.x,
486
+ y: rect.y,
487
+ width: rect.width,
488
+ height: rect.height,
489
+ fontSize: styles.fontSize,
490
+ color: styles.color,
491
+ backgroundColor: styles.backgroundColor,
492
+ margin: styles.margin,
493
+ padding: styles.padding
494
+ });
495
+ });
496
+ });
497
+
498
+ return elements;
499
+ }
500
+ """)
501
+
502
+ implementation_layout = await implementation_browser.page.evaluate("""
503
+ () => {
504
+ const elements = [];
505
+ const selectors = ['h1', 'h2', 'h3', 'nav', 'main', 'header', 'footer', '.btn', '.button', 'form', 'input'];
506
+
507
+ selectors.forEach(selector => {
508
+ document.querySelectorAll(selector).forEach((el, index) => {
509
+ const rect = el.getBoundingClientRect();
510
+ const styles = window.getComputedStyle(el);
511
+ elements.push({
512
+ selector: selector + (index > 0 ? `[${index}]` : ''),
513
+ x: rect.x,
514
+ y: rect.y,
515
+ width: rect.width,
516
+ height: rect.height,
517
+ fontSize: styles.fontSize,
518
+ color: styles.color,
519
+ backgroundColor: styles.backgroundColor,
520
+ margin: styles.margin,
521
+ padding: styles.padding
522
+ });
523
+ });
524
+ });
525
+
526
+ return elements;
527
+ }
528
+ """)
529
+
530
+ # Compare layouts
531
+ layout_differences = self._compare_layouts(mockup_layout, implementation_layout)
532
+
533
+ return {
534
+ "mockup_elements": len(mockup_layout),
535
+ "implementation_elements": len(implementation_layout),
536
+ "differences": layout_differences,
537
+ "viewport": viewport_name
538
+ }
539
+
540
+ except Exception as e:
541
+ self.logger.error(f"Layout analysis failed: {e}")
542
+ return {"error": str(e)}
543
+
544
+ def _compare_layouts(self, mockup_layout: List[Dict], implementation_layout: List[Dict]) -> List[Dict]:
545
+ """Compare layout elements between mockup and implementation"""
546
+ differences = []
547
+
548
+ # Create lookup dictionaries
549
+ mockup_elements = {el["selector"]: el for el in mockup_layout}
550
+ implementation_elements = {el["selector"]: el for el in implementation_layout}
551
+
552
+ # Find differences
553
+ all_selectors = set(mockup_elements.keys()) | set(implementation_elements.keys())
554
+
555
+ for selector in all_selectors:
556
+ mockup_el = mockup_elements.get(selector)
557
+ impl_el = implementation_elements.get(selector)
558
+
559
+ if not mockup_el:
560
+ differences.append({
561
+ "type": "missing_in_mockup",
562
+ "selector": selector,
563
+ "implementation_element": impl_el
564
+ })
565
+ elif not impl_el:
566
+ differences.append({
567
+ "type": "missing_in_implementation",
568
+ "selector": selector,
569
+ "mockup_element": mockup_el
570
+ })
571
+ else:
572
+ # Compare element properties
573
+ element_diff = self._compare_element_properties(mockup_el, impl_el)
574
+ if element_diff:
575
+ differences.append({
576
+ "type": "property_differences",
577
+ "selector": selector,
578
+ "differences": element_diff
579
+ })
580
+
581
+ return differences
582
+
583
+ def _compare_element_properties(self, mockup_el: Dict, impl_el: Dict) -> Dict[str, Any]:
584
+ """Compare properties of individual elements"""
585
+ differences = {}
586
+
587
+ # Position differences
588
+ position_threshold = 10 # pixels
589
+ if abs(mockup_el["x"] - impl_el["x"]) > position_threshold:
590
+ differences["x_position"] = {
591
+ "mockup": mockup_el["x"],
592
+ "implementation": impl_el["x"],
593
+ "difference": impl_el["x"] - mockup_el["x"]
594
+ }
595
+
596
+ if abs(mockup_el["y"] - impl_el["y"]) > position_threshold:
597
+ differences["y_position"] = {
598
+ "mockup": mockup_el["y"],
599
+ "implementation": impl_el["y"],
600
+ "difference": impl_el["y"] - mockup_el["y"]
601
+ }
602
+
603
+ # Size differences
604
+ size_threshold = 5 # pixels
605
+ if abs(mockup_el["width"] - impl_el["width"]) > size_threshold:
606
+ differences["width"] = {
607
+ "mockup": mockup_el["width"],
608
+ "implementation": impl_el["width"],
609
+ "difference": impl_el["width"] - mockup_el["width"]
610
+ }
611
+
612
+ if abs(mockup_el["height"] - impl_el["height"]) > size_threshold:
613
+ differences["height"] = {
614
+ "mockup": mockup_el["height"],
615
+ "implementation": impl_el["height"],
616
+ "difference": impl_el["height"] - mockup_el["height"]
617
+ }
618
+
619
+ # Style differences
620
+ if mockup_el["color"] != impl_el["color"]:
621
+ differences["color"] = {
622
+ "mockup": mockup_el["color"],
623
+ "implementation": impl_el["color"]
624
+ }
625
+
626
+ if mockup_el["backgroundColor"] != impl_el["backgroundColor"]:
627
+ differences["backgroundColor"] = {
628
+ "mockup": mockup_el["backgroundColor"],
629
+ "implementation": impl_el["backgroundColor"]
630
+ }
631
+
632
+ if mockup_el["fontSize"] != impl_el["fontSize"]:
633
+ differences["fontSize"] = {
634
+ "mockup": mockup_el["fontSize"],
635
+ "implementation": impl_el["fontSize"]
636
+ }
637
+
638
+ return differences
639
+
640
+ async def _analyze_element_differences(self, mockup_browser: BrowserController, implementation_browser: BrowserController, viewport_name: str) -> Dict[str, Any]:
641
+ """Analyze specific element-level differences with detailed DOM and CSS data"""
642
+ try:
643
+ # Capture detailed DOM structure and CSS from both pages
644
+ mockup_dom_data = await self._capture_detailed_dom_analysis(mockup_browser)
645
+ implementation_dom_data = await self._capture_detailed_dom_analysis(implementation_browser)
646
+
647
+ # Compare DOM structures
648
+ dom_comparison = self._compare_dom_structures(mockup_dom_data, implementation_dom_data)
649
+
650
+ # Compare CSS properties
651
+ css_comparison = self._compare_css_properties(mockup_dom_data, implementation_dom_data)
652
+
653
+ # Generate specific change recommendations
654
+ change_recommendations = self._generate_css_change_recommendations(dom_comparison, css_comparison)
655
+
656
+ return {
657
+ "viewport": viewport_name,
658
+ "mockup_dom_data": mockup_dom_data,
659
+ "implementation_dom_data": implementation_dom_data,
660
+ "dom_structure_comparison": dom_comparison,
661
+ "css_property_comparison": css_comparison,
662
+ "change_recommendations": change_recommendations,
663
+ "analysis_timestamp": time.time()
664
+ }
665
+
666
+ except Exception as e:
667
+ self.logger.error(f"Element analysis failed: {e}")
668
+ return {"error": str(e), "viewport": viewport_name}
669
+
670
+ async def _capture_detailed_dom_analysis(self, browser: BrowserController) -> Dict[str, Any]:
671
+ """Capture comprehensive DOM structure and CSS data"""
672
+ try:
673
+ dom_analysis = await browser.page.evaluate("""
674
+ () => {
675
+ // Helper function to get element path
676
+ function getElementPath(element) {
677
+ const path = [];
678
+ while (element && element.nodeType === Node.ELEMENT_NODE) {
679
+ let selector = element.nodeName.toLowerCase();
680
+ if (element.id) {
681
+ selector += '#' + element.id;
682
+ } else if (element.className) {
683
+ selector += '.' + element.className.split(' ').join('.');
684
+ }
685
+ path.unshift(selector);
686
+ element = element.parentNode;
687
+ }
688
+ return path.join(' > ');
689
+ }
690
+
691
+ // Helper function to get comprehensive computed styles
692
+ function getComputedStylesDetailed(element) {
693
+ const computed = window.getComputedStyle(element);
694
+ return {
695
+ // Layout properties
696
+ display: computed.display,
697
+ position: computed.position,
698
+ top: computed.top,
699
+ left: computed.left,
700
+ right: computed.right,
701
+ bottom: computed.bottom,
702
+ width: computed.width,
703
+ height: computed.height,
704
+ minWidth: computed.minWidth,
705
+ maxWidth: computed.maxWidth,
706
+ minHeight: computed.minHeight,
707
+ maxHeight: computed.maxHeight,
708
+
709
+ // Flexbox properties
710
+ flexDirection: computed.flexDirection,
711
+ flexWrap: computed.flexWrap,
712
+ justifyContent: computed.justifyContent,
713
+ alignItems: computed.alignItems,
714
+ alignContent: computed.alignContent,
715
+ flex: computed.flex,
716
+ flexGrow: computed.flexGrow,
717
+ flexShrink: computed.flexShrink,
718
+ flexBasis: computed.flexBasis,
719
+
720
+ // Grid properties
721
+ gridTemplateColumns: computed.gridTemplateColumns,
722
+ gridTemplateRows: computed.gridTemplateRows,
723
+ gridGap: computed.gridGap,
724
+ gridArea: computed.gridArea,
725
+
726
+ // Spacing
727
+ margin: computed.margin,
728
+ marginTop: computed.marginTop,
729
+ marginRight: computed.marginRight,
730
+ marginBottom: computed.marginBottom,
731
+ marginLeft: computed.marginLeft,
732
+ padding: computed.padding,
733
+ paddingTop: computed.paddingTop,
734
+ paddingRight: computed.paddingRight,
735
+ paddingBottom: computed.paddingBottom,
736
+ paddingLeft: computed.paddingLeft,
737
+
738
+ // Typography
739
+ fontFamily: computed.fontFamily,
740
+ fontSize: computed.fontSize,
741
+ fontWeight: computed.fontWeight,
742
+ fontStyle: computed.fontStyle,
743
+ lineHeight: computed.lineHeight,
744
+ letterSpacing: computed.letterSpacing,
745
+ textAlign: computed.textAlign,
746
+ textDecoration: computed.textDecoration,
747
+ textTransform: computed.textTransform,
748
+
749
+ // Colors and backgrounds
750
+ color: computed.color,
751
+ backgroundColor: computed.backgroundColor,
752
+ backgroundImage: computed.backgroundImage,
753
+ backgroundSize: computed.backgroundSize,
754
+ backgroundPosition: computed.backgroundPosition,
755
+ backgroundRepeat: computed.backgroundRepeat,
756
+
757
+ // Borders
758
+ border: computed.border,
759
+ borderTop: computed.borderTop,
760
+ borderRight: computed.borderRight,
761
+ borderBottom: computed.borderBottom,
762
+ borderLeft: computed.borderLeft,
763
+ borderRadius: computed.borderRadius,
764
+ borderWidth: computed.borderWidth,
765
+ borderStyle: computed.borderStyle,
766
+ borderColor: computed.borderColor,
767
+
768
+ // Visual effects
769
+ boxShadow: computed.boxShadow,
770
+ opacity: computed.opacity,
771
+ transform: computed.transform,
772
+ transition: computed.transition,
773
+ animation: computed.animation,
774
+
775
+ // Z-index and overflow
776
+ zIndex: computed.zIndex,
777
+ overflow: computed.overflow,
778
+ overflowX: computed.overflowX,
779
+ overflowY: computed.overflowY
780
+ };
781
+ }
782
+
783
+ // Get all significant elements
784
+ const elements = [];
785
+ const selectors = [
786
+ // Structural elements
787
+ 'body', 'main', 'header', 'nav', 'aside', 'footer', 'section', 'article',
788
+ // Common containers
789
+ '.container', '.wrapper', '.content', '.sidebar', '.header', '.footer',
790
+ '.navbar', '.nav', '.menu', '.main', '.page', '.app',
791
+ // Interactive elements
792
+ 'button', '.btn', '.button', 'a', '.link', 'input', 'form', '.form',
793
+ // Content elements
794
+ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', '.title', '.heading',
795
+ // Layout elements
796
+ '.row', '.col', '.column', '.grid', '.flex', '.card', '.panel',
797
+ // Common UI components
798
+ '.modal', '.dropdown', '.tooltip', '.alert', '.badge', '.tab'
799
+ ];
800
+
801
+ selectors.forEach(selector => {
802
+ try {
803
+ document.querySelectorAll(selector).forEach((element, index) => {
804
+ const rect = element.getBoundingClientRect();
805
+ const computedStyles = getComputedStylesDetailed(element);
806
+
807
+ // Only include visible elements
808
+ if (rect.width > 0 && rect.height > 0) {
809
+ elements.push({
810
+ selector: selector,
811
+ index: index,
812
+ uniqueSelector: selector + (index > 0 ? `:nth-of-type(${index + 1})` : ''),
813
+ elementPath: getElementPath(element),
814
+ tagName: element.tagName.toLowerCase(),
815
+ id: element.id || null,
816
+ className: element.className || null,
817
+ textContent: element.textContent ? element.textContent.trim().substring(0, 100) : null,
818
+
819
+ // Bounding box
820
+ boundingBox: {
821
+ x: rect.x,
822
+ y: rect.y,
823
+ width: rect.width,
824
+ height: rect.height,
825
+ top: rect.top,
826
+ left: rect.left,
827
+ right: rect.right,
828
+ bottom: rect.bottom
829
+ },
830
+
831
+ // All computed styles
832
+ computedStyles: computedStyles,
833
+
834
+ // Element attributes
835
+ attributes: Array.from(element.attributes).reduce((attrs, attr) => {
836
+ attrs[attr.name] = attr.value;
837
+ return attrs;
838
+ }, {}),
839
+
840
+ // Children count
841
+ childrenCount: element.children.length,
842
+
843
+ // Visibility
844
+ isVisible: rect.width > 0 && rect.height > 0 && computedStyles.display !== 'none' && computedStyles.visibility !== 'hidden'
845
+ });
846
+ }
847
+ });
848
+ } catch (e) {
849
+ console.warn(`Failed to analyze selector ${selector}:`, e);
850
+ }
851
+ });
852
+
853
+ // Get page-level information
854
+ const pageInfo = {
855
+ title: document.title,
856
+ url: window.location.href,
857
+ viewport: {
858
+ width: window.innerWidth,
859
+ height: window.innerHeight
860
+ },
861
+ documentSize: {
862
+ width: Math.max(
863
+ document.body.scrollWidth,
864
+ document.body.offsetWidth,
865
+ document.documentElement.clientWidth,
866
+ document.documentElement.scrollWidth,
867
+ document.documentElement.offsetWidth
868
+ ),
869
+ height: Math.max(
870
+ document.body.scrollHeight,
871
+ document.body.offsetHeight,
872
+ document.documentElement.clientHeight,
873
+ document.documentElement.scrollHeight,
874
+ document.documentElement.offsetHeight
875
+ )
876
+ }
877
+ };
878
+
879
+ return {
880
+ pageInfo: pageInfo,
881
+ elements: elements,
882
+ totalElements: elements.length,
883
+ captureTimestamp: Date.now()
884
+ };
885
+ }
886
+ """)
887
+
888
+ return dom_analysis
889
+
890
+ except Exception as e:
891
+ self.logger.error(f"DOM analysis capture failed: {e}")
892
+ return {"error": str(e)}
893
+
894
+ def _compare_dom_structures(self, mockup_data: Dict, implementation_data: Dict) -> Dict[str, Any]:
895
+ """Compare DOM structures between mockup and implementation"""
896
+ try:
897
+ mockup_elements = {el['uniqueSelector']: el for el in mockup_data.get('elements', [])}
898
+ implementation_elements = {el['uniqueSelector']: el for el in implementation_data.get('elements', [])}
899
+
900
+ # Find missing, extra, and common elements
901
+ mockup_selectors = set(mockup_elements.keys())
902
+ implementation_selectors = set(implementation_elements.keys())
903
+
904
+ missing_in_implementation = mockup_selectors - implementation_selectors
905
+ extra_in_implementation = implementation_selectors - mockup_selectors
906
+ common_elements = mockup_selectors & implementation_selectors
907
+
908
+ # Analyze structural differences
909
+ structural_differences = []
910
+
911
+ for selector in missing_in_implementation:
912
+ element = mockup_elements[selector]
913
+ structural_differences.append({
914
+ "type": "missing_element",
915
+ "selector": selector,
916
+ "element_path": element.get('elementPath'),
917
+ "mockup_element": {
918
+ "tagName": element.get('tagName'),
919
+ "className": element.get('className'),
920
+ "textContent": element.get('textContent'),
921
+ "boundingBox": element.get('boundingBox')
922
+ },
923
+ "severity": "high" if element.get('tagName') in ['nav', 'header', 'main', 'footer'] else "medium"
924
+ })
925
+
926
+ for selector in extra_in_implementation:
927
+ element = implementation_elements[selector]
928
+ structural_differences.append({
929
+ "type": "extra_element",
930
+ "selector": selector,
931
+ "element_path": element.get('elementPath'),
932
+ "implementation_element": {
933
+ "tagName": element.get('tagName'),
934
+ "className": element.get('className'),
935
+ "textContent": element.get('textContent'),
936
+ "boundingBox": element.get('boundingBox')
937
+ },
938
+ "severity": "low"
939
+ })
940
+
941
+ return {
942
+ "missing_in_implementation": list(missing_in_implementation),
943
+ "extra_in_implementation": list(extra_in_implementation),
944
+ "common_elements": list(common_elements),
945
+ "structural_differences": structural_differences,
946
+ "similarity_score": len(common_elements) / max(len(mockup_selectors), len(implementation_selectors)) * 100 if mockup_selectors or implementation_selectors else 100
947
+ }
948
+
949
+ except Exception as e:
950
+ self.logger.error(f"DOM structure comparison failed: {e}")
951
+ return {"error": str(e)}
952
+
953
+ def _compare_css_properties(self, mockup_data: Dict, implementation_data: Dict) -> Dict[str, Any]:
954
+ """Compare CSS properties between matching elements"""
955
+ try:
956
+ mockup_elements = {el['uniqueSelector']: el for el in mockup_data.get('elements', [])}
957
+ implementation_elements = {el['uniqueSelector']: el for el in implementation_data.get('elements', [])}
958
+
959
+ css_differences = []
960
+
961
+ # Compare common elements
962
+ common_selectors = set(mockup_elements.keys()) & set(implementation_elements.keys())
963
+
964
+ for selector in common_selectors:
965
+ mockup_el = mockup_elements[selector]
966
+ impl_el = implementation_elements[selector]
967
+
968
+ mockup_styles = mockup_el.get('computedStyles', {})
969
+ impl_styles = impl_el.get('computedStyles', {})
970
+
971
+ # Compare key CSS properties
972
+ property_differences = {}
973
+
974
+ # Important properties to compare
975
+ key_properties = [
976
+ 'display', 'position', 'width', 'height', 'top', 'left', 'right', 'bottom',
977
+ 'margin', 'marginTop', 'marginRight', 'marginBottom', 'marginLeft',
978
+ 'padding', 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft',
979
+ 'fontSize', 'fontWeight', 'fontFamily', 'lineHeight', 'textAlign',
980
+ 'color', 'backgroundColor', 'border', 'borderRadius', 'boxShadow',
981
+ 'flexDirection', 'justifyContent', 'alignItems', 'gridTemplateColumns'
982
+ ]
983
+
984
+ for prop in key_properties:
985
+ mockup_value = mockup_styles.get(prop, '')
986
+ impl_value = impl_styles.get(prop, '')
987
+
988
+ if mockup_value != impl_value:
989
+ # Calculate significance of difference
990
+ significance = self._calculate_css_difference_significance(prop, mockup_value, impl_value)
991
+
992
+ property_differences[prop] = {
993
+ "mockup_value": mockup_value,
994
+ "implementation_value": impl_value,
995
+ "significance": significance,
996
+ "css_property": prop
997
+ }
998
+
999
+ if property_differences:
1000
+ # Compare bounding boxes for layout impact
1001
+ mockup_box = mockup_el.get('boundingBox', {})
1002
+ impl_box = impl_el.get('boundingBox', {})
1003
+
1004
+ layout_impact = {
1005
+ "position_difference": {
1006
+ "x": abs(mockup_box.get('x', 0) - impl_box.get('x', 0)),
1007
+ "y": abs(mockup_box.get('y', 0) - impl_box.get('y', 0))
1008
+ },
1009
+ "size_difference": {
1010
+ "width": abs(mockup_box.get('width', 0) - impl_box.get('width', 0)),
1011
+ "height": abs(mockup_box.get('height', 0) - impl_box.get('height', 0))
1012
+ }
1013
+ }
1014
+
1015
+ css_differences.append({
1016
+ "selector": selector,
1017
+ "element_path": mockup_el.get('elementPath'),
1018
+ "property_differences": property_differences,
1019
+ "layout_impact": layout_impact,
1020
+ "difference_count": len(property_differences),
1021
+ "severity": "high" if len(property_differences) > 5 else "medium" if len(property_differences) > 2 else "low"
1022
+ })
1023
+
1024
+ return {
1025
+ "elements_compared": len(common_selectors),
1026
+ "elements_with_differences": len(css_differences),
1027
+ "css_differences": css_differences,
1028
+ "overall_css_similarity": max(0, 100 - (len(css_differences) / max(len(common_selectors), 1) * 100))
1029
+ }
1030
+
1031
+ except Exception as e:
1032
+ self.logger.error(f"CSS property comparison failed: {e}")
1033
+ return {"error": str(e)}
1034
+
1035
+ def _calculate_css_difference_significance(self, property: str, mockup_value: str, impl_value: str) -> str:
1036
+ """Calculate the significance of a CSS property difference"""
1037
+
1038
+ # High significance properties (major visual impact)
1039
+ high_impact_props = ['display', 'position', 'width', 'height', 'backgroundColor', 'color', 'fontSize']
1040
+
1041
+ # Medium significance properties (moderate visual impact)
1042
+ medium_impact_props = ['margin', 'padding', 'border', 'borderRadius', 'fontWeight', 'textAlign']
1043
+
1044
+ if property in high_impact_props:
1045
+ return "high"
1046
+ elif property in medium_impact_props:
1047
+ return "medium"
1048
+ else:
1049
+ return "low"
1050
+
1051
+ def _generate_css_change_recommendations(self, dom_comparison: Dict, css_comparison: Dict) -> List[Dict]:
1052
+ """Generate specific CSS change recommendations based on analysis"""
1053
+ try:
1054
+ recommendations = []
1055
+
1056
+ # Recommendations for missing elements
1057
+ for diff in dom_comparison.get('structural_differences', []):
1058
+ if diff['type'] == 'missing_element':
1059
+ mockup_element = diff['mockup_element']
1060
+ recommendations.append({
1061
+ "type": "add_element",
1062
+ "priority": diff['severity'],
1063
+ "selector": diff['selector'],
1064
+ "description": f"Add missing {mockup_element['tagName']} element",
1065
+ "suggested_html": f"<{mockup_element['tagName']} class=\"{mockup_element.get('className', '')}\">{mockup_element.get('textContent', '')}</{mockup_element['tagName']}>",
1066
+ "estimated_effort": "medium"
1067
+ })
1068
+
1069
+ # Recommendations for CSS property differences
1070
+ for element_diff in css_comparison.get('css_differences', []):
1071
+ selector = element_diff['selector']
1072
+ property_diffs = element_diff['property_differences']
1073
+
1074
+ # Generate CSS rule for this element
1075
+ css_rules = []
1076
+ for prop, diff in property_diffs.items():
1077
+ css_property = self._convert_to_css_property(prop)
1078
+ css_rules.append(f" {css_property}: {diff['mockup_value']};")
1079
+
1080
+ if css_rules:
1081
+ css_rule = f"{selector} {{\n" + "\n".join(css_rules) + "\n}"
1082
+
1083
+ recommendations.append({
1084
+ "type": "modify_css",
1085
+ "priority": element_diff['severity'],
1086
+ "selector": selector,
1087
+ "description": f"Update CSS properties for {selector}",
1088
+ "suggested_css": css_rule,
1089
+ "properties_to_change": list(property_diffs.keys()),
1090
+ "layout_impact": element_diff['layout_impact'],
1091
+ "estimated_effort": "low" if len(property_diffs) <= 2 else "medium"
1092
+ })
1093
+
1094
+ # Sort by priority and impact
1095
+ priority_order = {"high": 3, "medium": 2, "low": 1}
1096
+ recommendations.sort(key=lambda x: priority_order.get(x['priority'], 0), reverse=True)
1097
+
1098
+ return recommendations
1099
+
1100
+ except Exception as e:
1101
+ self.logger.error(f"CSS recommendation generation failed: {e}")
1102
+ return []
1103
+
1104
+ def _convert_to_css_property(self, computed_property: str) -> str:
1105
+ """Convert computed style property name to CSS property name"""
1106
+ # Convert camelCase to kebab-case
1107
+ css_property = ""
1108
+ for char in computed_property:
1109
+ if char.isupper():
1110
+ css_property += "-" + char.lower()
1111
+ else:
1112
+ css_property += char
1113
+ return css_property
1114
+
1115
+ async def _compare_current_state_to_mockup(
1116
+ self,
1117
+ mockup_url: str,
1118
+ implementation_browser: BrowserController,
1119
+ comparison_name: str,
1120
+ config: Optional[Dict]
1121
+ ) -> Dict[str, Any]:
1122
+ """Compare current implementation state to mockup"""
1123
+ # Initialize mockup browser for comparison
1124
+ mockup_browser = BrowserController(mockup_url, {"headless": True})
1125
+ await mockup_browser.initialize()
1126
+
1127
+ try:
1128
+ # Navigate mockup browser
1129
+ await mockup_browser.navigate("/")
1130
+
1131
+ # Capture screenshots
1132
+ mockup_screenshot = await self._capture_comparison_screenshot(
1133
+ mockup_browser, f"{comparison_name}_mockup"
1134
+ )
1135
+ implementation_screenshot = await self._capture_comparison_screenshot(
1136
+ implementation_browser, f"{comparison_name}_implementation"
1137
+ )
1138
+
1139
+ # Create visual diff
1140
+ visual_diff = await self._create_visual_diff(
1141
+ mockup_screenshot, implementation_screenshot, comparison_name, config or {}
1142
+ )
1143
+
1144
+ return {
1145
+ "mockup_screenshot": mockup_screenshot,
1146
+ "implementation_screenshot": implementation_screenshot,
1147
+ "visual_diff": visual_diff,
1148
+ "comparison_name": comparison_name
1149
+ }
1150
+
1151
+ finally:
1152
+ await mockup_browser.cleanup()
1153
+
1154
+ def _calculate_improvement_metrics(self, baseline_comparison: Dict, improved_comparison: Dict) -> Dict[str, Any]:
1155
+ """Calculate improvement metrics between baseline and improved implementation"""
1156
+ try:
1157
+ baseline_similarity = 0
1158
+ improved_similarity = 0
1159
+
1160
+ # Extract similarity scores from comparison results
1161
+ if "results" in baseline_comparison and baseline_comparison["results"]:
1162
+ baseline_result = baseline_comparison["results"][0] # Use first viewport
1163
+ baseline_similarity = baseline_result.get("visual_diff", {}).get("similarity_score", 0)
1164
+
1165
+ if "visual_diff" in improved_comparison:
1166
+ improved_similarity = improved_comparison["visual_diff"].get("similarity_score", 0)
1167
+
1168
+ improvement = improved_similarity - baseline_similarity
1169
+
1170
+ return {
1171
+ "baseline_similarity": baseline_similarity,
1172
+ "improved_similarity": improved_similarity,
1173
+ "improvement": improvement,
1174
+ "improvement_percentage": round(improvement, 2),
1175
+ "is_improvement": improvement > 0
1176
+ }
1177
+
1178
+ except Exception as e:
1179
+ self.logger.error(f"Improvement metrics calculation failed: {e}")
1180
+ return {"error": str(e)}
1181
+
1182
+ def _create_comparison_summary(self, comparison_results: List[Dict]) -> Dict[str, Any]:
1183
+ """Create summary of comparison results"""
1184
+ if not comparison_results:
1185
+ return {"error": "No comparison results"}
1186
+
1187
+ # Calculate average similarity across viewports
1188
+ similarities = []
1189
+ for result in comparison_results:
1190
+ similarity = result.get("visual_diff", {}).get("similarity_score", 0)
1191
+ similarities.append(similarity)
1192
+
1193
+ avg_similarity = sum(similarities) / len(similarities) if similarities else 0
1194
+
1195
+ return {
1196
+ "average_similarity": round(avg_similarity, 2),
1197
+ "viewports_tested": len(comparison_results),
1198
+ "best_viewport_match": max(comparison_results, key=lambda x: x.get("visual_diff", {}).get("similarity_score", 0)),
1199
+ "needs_improvement": avg_similarity < 80 # Threshold for "good enough"
1200
+ }
1201
+
1202
+ def _generate_improvement_recommendations(self, comparison_results: List[Dict]) -> List[Dict]:
1203
+ """Generate recommendations for improving implementation to match mockup"""
1204
+ recommendations = []
1205
+
1206
+ for result in comparison_results:
1207
+ viewport = result.get("viewport", {})
1208
+ layout_analysis = result.get("layout_analysis", {})
1209
+ visual_diff = result.get("visual_diff", {})
1210
+
1211
+ # Analyze layout differences for recommendations
1212
+ differences = layout_analysis.get("differences", [])
1213
+
1214
+ for diff in differences:
1215
+ if diff["type"] == "missing_in_implementation":
1216
+ recommendations.append({
1217
+ "type": "add_element",
1218
+ "priority": "high",
1219
+ "description": f"Add missing element: {diff['selector']}",
1220
+ "viewport": viewport.get("name", "unknown")
1221
+ })
1222
+ elif diff["type"] == "property_differences":
1223
+ for prop, prop_diff in diff.get("differences", {}).items():
1224
+ recommendations.append({
1225
+ "type": "adjust_property",
1226
+ "priority": "medium",
1227
+ "description": f"Adjust {prop} for {diff['selector']}",
1228
+ "current_value": prop_diff.get("implementation"),
1229
+ "target_value": prop_diff.get("mockup"),
1230
+ "viewport": viewport.get("name", "unknown")
1231
+ })
1232
+
1233
+ # Add visual similarity recommendations
1234
+ similarity = visual_diff.get("similarity_score", 0)
1235
+ if similarity < 70:
1236
+ recommendations.append({
1237
+ "type": "major_visual_changes",
1238
+ "priority": "high",
1239
+ "description": f"Significant visual differences detected (similarity: {similarity}%)",
1240
+ "viewport": viewport.get("name", "unknown")
1241
+ })
1242
+
1243
+ return recommendations
1244
+
1245
+ def _create_iteration_summary(self, baseline_comparison: Dict, iteration_results: List[Dict]) -> Dict[str, Any]:
1246
+ """Create summary of iteration session"""
1247
+ if not iteration_results:
1248
+ return {"error": "No iteration results"}
1249
+
1250
+ # Track improvement over iterations
1251
+ improvements = []
1252
+ for result in iteration_results:
1253
+ improvement = result.get("improvement_metrics", {}).get("improvement", 0)
1254
+ improvements.append(improvement)
1255
+
1256
+ total_improvement = sum(improvements)
1257
+ best_improvement = max(improvements) if improvements else 0
1258
+
1259
+ return {
1260
+ "total_iterations": len(iteration_results),
1261
+ "total_improvement": round(total_improvement, 2),
1262
+ "best_single_improvement": round(best_improvement, 2),
1263
+ "successful_iterations": len([r for r in iteration_results if r.get("improvement_metrics", {}).get("is_improvement", False)]),
1264
+ "average_improvement_per_iteration": round(total_improvement / len(iteration_results), 2) if iteration_results else 0
1265
+ }
1266
+
1267
+ def _find_best_iteration(self, iteration_results: List[Dict]) -> Optional[Dict]:
1268
+ """Find the iteration with the best improvement"""
1269
+ if not iteration_results:
1270
+ return None
1271
+
1272
+ best_iteration = max(
1273
+ iteration_results,
1274
+ key=lambda x: x.get("improvement_metrics", {}).get("improved_similarity", 0)
1275
+ )
1276
+
1277
+ return {
1278
+ "iteration_number": best_iteration.get("iteration_number"),
1279
+ "css_change": best_iteration.get("css_change"),
1280
+ "similarity_achieved": best_iteration.get("improvement_metrics", {}).get("improved_similarity", 0),
1281
+ "improvement": best_iteration.get("improvement_metrics", {}).get("improvement", 0)
1282
+ }
1283
+
1284
+ def _generate_final_recommendations(self, iteration_results: List[Dict]) -> List[Dict]:
1285
+ """Generate final recommendations based on all iterations"""
1286
+ recommendations = []
1287
+
1288
+ # Find the most successful CSS changes
1289
+ successful_changes = [
1290
+ result for result in iteration_results
1291
+ if result.get("improvement_metrics", {}).get("is_improvement", False)
1292
+ ]
1293
+
1294
+ if successful_changes:
1295
+ recommendations.append({
1296
+ "type": "apply_successful_changes",
1297
+ "priority": "high",
1298
+ "description": f"Apply the {len(successful_changes)} successful CSS changes to your codebase",
1299
+ "changes": [result.get("css_change") for result in successful_changes]
1300
+ })
1301
+
1302
+ # Identify areas that still need work
1303
+ final_similarities = [
1304
+ result.get("improvement_metrics", {}).get("improved_similarity", 0)
1305
+ for result in iteration_results
1306
+ ]
1307
+
1308
+ if final_similarities and max(final_similarities) < 85:
1309
+ recommendations.append({
1310
+ "type": "additional_work_needed",
1311
+ "priority": "medium",
1312
+ "description": "Additional CSS improvements needed to achieve closer mockup match",
1313
+ "current_best_similarity": max(final_similarities) if final_similarities else 0
1314
+ })
1315
+
1316
+ return recommendations