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.
- cursorflow/cli.py +212 -0
- cursorflow/core/browser_controller.py +659 -7
- cursorflow/core/cursor_integration.py +788 -0
- cursorflow/core/cursorflow.py +151 -0
- cursorflow/core/mockup_comparator.py +1316 -0
- cursorflow-1.3.0.dist-info/METADATA +226 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/RECORD +11 -9
- cursorflow-1.3.0.dist-info/licenses/LICENSE +21 -0
- cursorflow-1.2.0.dist-info/METADATA +0 -444
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/WHEEL +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/entry_points.txt +0 -0
- {cursorflow-1.2.0.dist-info → cursorflow-1.3.0.dist-info}/top_level.txt +0 -0
@@ -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
|