v0-ui-reviewer
Version:
Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox
193 lines (192 loc) • 8.28 kB
JavaScript
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import { promises as fs } from 'fs';
import path from 'path';
import { StyleExtractor } from './style-extractor.js';
import { logger } from './logger.js';
// Configure puppeteer-extra with stealth plugin
puppeteer.use(StealthPlugin());
export class EnhancedCapture {
verbose;
constructor(verbose = false) {
this.verbose = verbose;
}
log(message, level = 'info') {
if (this.verbose || level === 'error' || level === 'warn') {
logger[level](message);
}
}
async captureWithStyles(options) {
const { url, fullPage = true, viewportWidth = 1920, viewportHeight = 1080, outputPath, mobile = false, timeout = 30000, extractStyles = false, stylePoints, styleGridSize = 10, onProgress } = options;
const browserArgs = [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--disable-gpu',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--disable-blink-features=AutomationControlled'
];
this.log('Launching browser with stealth mode...', 'debug');
onProgress?.('Browser Launch', 10, 'Starting browser...');
const browser = await puppeteer.launch({
headless: true,
args: browserArgs,
executablePath: puppeteer.executablePath(),
timeout: 60000
});
try {
const page = await browser.newPage();
// Enhanced stealth configuration
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
await page.setExtraHTTPHeaders({
'Accept-Language': 'en-US,en;q=0.9'
});
// Override navigator properties
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => false,
});
// Add chrome object
if (!window.chrome) {
window.chrome = {
runtime: {}
};
}
// Override permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (parameters.name === 'notifications' ?
Promise.resolve({ state: 'denied' }) :
originalQuery(parameters));
});
// Set viewport
await page.setViewport({
width: mobile ? 375 : viewportWidth,
height: mobile ? 667 : viewportHeight,
deviceScaleFactor: mobile ? 2 : 1,
isMobile: mobile,
hasTouch: mobile
});
this.log(`Navigating to ${url}...`, 'debug');
onProgress?.('Navigation', 20, 'Loading page...');
// Navigate with comprehensive wait conditions
await page.goto(url, {
waitUntil: 'networkidle0',
timeout
});
// Wait for dynamic content
await new Promise(resolve => setTimeout(resolve, 2000));
onProgress?.('Navigation', 40, 'Page loaded');
// Scroll to trigger lazy loading
await page.evaluate(() => {
return new Promise((resolve) => {
let totalHeight = 0;
const distance = 100;
const timer = setInterval(() => {
const scrollHeight = document.body.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;
if (totalHeight >= scrollHeight) {
clearInterval(timer);
window.scrollTo(0, 0); // Scroll back to top
setTimeout(resolve, 1000); // Wait for any final renders
}
}, 100);
});
});
onProgress?.('Screenshot', 50, 'Capturing screenshot...');
// Generate filename
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const domain = new URL(url).hostname.replace(/\./g, '-');
const filename = outputPath || `v0-review-${domain}-${timestamp}.png`;
// Ensure output directory exists
const outputDir = path.dirname(path.resolve(filename));
await fs.mkdir(outputDir, { recursive: true });
const screenshotPath = path.resolve(filename);
this.log('Capturing screenshot...', 'debug');
// Capture screenshot with enhanced options
await page.screenshot({
path: screenshotPath,
fullPage,
type: 'png',
captureBeyondViewport: true,
optimizeForSpeed: false
});
this.log(`Screenshot saved to ${screenshotPath}`, 'debug');
onProgress?.('Screenshot', 70, 'Screenshot captured');
// Extract styles if requested
let extractedStyles;
let designTokens;
if (extractStyles) {
onProgress?.('Style Extraction', 80, 'Extracting styles...');
this.log('Extracting styles from page...', 'debug');
const styleExtractor = new StyleExtractor(page);
if (stylePoints && stylePoints.length > 0) {
// Extract from specific points
extractedStyles = await styleExtractor.extractStylesFromPoints(stylePoints);
}
else {
// Extract from grid
extractedStyles = await styleExtractor.extractStylesFromGrid(styleGridSize);
}
// Create design tokens
designTokens = styleExtractor.createDesignTokens(extractedStyles);
this.log(`Extracted ${extractedStyles.length} style samples`, 'debug');
onProgress?.('Style Extraction', 100, 'Styles extracted');
}
return {
screenshotPath,
extractedStyles,
designTokens
};
}
finally {
await browser.close();
}
}
/**
* Extract styles from an existing screenshot URL
*/
async extractStylesFromURL(url, options = {}) {
const browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
try {
const page = await browser.newPage();
// Set viewport
await page.setViewport({
width: options.mobile ? 375 : 1920,
height: options.mobile ? 667 : 1080,
deviceScaleFactor: options.mobile ? 2 : 1,
isMobile: options.mobile || false,
hasTouch: options.mobile || false
});
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: options.timeout || 30000
});
// Wait for dynamic content
await new Promise(resolve => setTimeout(resolve, 2000));
const styleExtractor = new StyleExtractor(page);
let extractedStyles;
if (options.points && options.points.length > 0) {
extractedStyles = await styleExtractor.extractStylesFromPoints(options.points);
}
else {
extractedStyles = await styleExtractor.extractStylesFromGrid(options.gridSize || 10);
}
const designTokens = styleExtractor.createDesignTokens(extractedStyles);
return {
extractedStyles,
designTokens
};
}
finally {
await browser.close();
}
}
}