UNPKG

v0-ui-reviewer

Version:

Next-gen UI/UX reviewer with multi-model AI support (OpenAI, Claude, v0), style extraction, and live preview sandbox

408 lines (380 loc) β€’ 15.9 kB
import { promises as fs } from 'fs'; import path from 'path'; import { EnhancedScreenshotCapture } from './screenshot.js'; import { HDImageDisplay } from './hd-image-display.js'; import { EnhancedCapture } from './enhanced-capture.js'; import { StyleExtractor } from './style-extractor.js'; import { MultiModelAIService } from './ai-service.js'; /** * V0 UI/UX Expert Reviewer CLI * * A powerful command-line tool that combines Puppeteer screenshot capture * with the v0 API to provide expert UI/UX analysis with terminal image display. */ export class V0UIReviewerCLI { apiKey; baseURL = 'https://api.v0.dev/v1/chat/completions'; timeout; screenshotCapture; imageDisplay; enhancedCapture; aiService; constructor(apiKey, options = {}) { this.apiKey = apiKey || process.env.V0_API_KEY || ''; this.timeout = options.timeout || 30000; this.screenshotCapture = new EnhancedScreenshotCapture(options.verbose || false); this.imageDisplay = new HDImageDisplay(); this.enhancedCapture = new EnhancedCapture(options.verbose || false); this.aiService = new MultiModelAIService({ v0ApiKey: this.apiKey, verbose: options.verbose }); if (!this.apiKey && !this.aiService.getAvailableModels().length) { throw new Error('No AI API keys configured. Set V0_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY'); } } /** * Expert UI/UX Review Prompt - Enhanced for CLI use */ getExpertPrompt(context) { const contextPrefix = context ? `Context: ${context}\n\n` : ''; return `${contextPrefix}You are "Visionary 7", a hybrid **UI/UX engineer, product designer, and senior front-end developer**. ### Your Tasks 1. **Deconstruct the screenshot**: - Identify all visible components (navigation, hero, cards, CTAs, forms, tables, charts, footers, etc.) - Infer the page's goal(s) and target user(s) - Detect visual hierarchy, layout grid, typography, color palette, iconography, spacing, alignment 2. **Run a heuristic audit** (Nielsen + WCAG 2.2 + modern design systems): - For each violated heuristic, cite the rule (*e.g.* "Consistency & Standards", "Contrast (Min 4.5:1)", "Fitts's Law") - Flag usability, accessibility, and conversion blockers 3. **Prioritize issues** by **Impact / Effort**: - Impact 🟒 low, 🟑 medium, πŸ”΄ high - Effort πŸ’§ small (<Β½ dev-day), 🌧️ medium (<2 dev-days), β›ˆοΈ large (>2 dev-days) 4. **Deliver actionable recommendations**: - ✨ **Quick wins** (fast, high-impact tweaks) - πŸ› οΈ **Deeper redesign** ideas (layout, IA, flow) - β™Ώ **Accessibility fixes** (aria, contrast, keyboard, screen reader) - πŸ’» **Code snippets** (Tailwind/React or plain CSS/HTML) for at least **two** key improvements - πŸ“ Suggested spacing/sizing tokens and typography scale - 🎨 Color palette improvements with HEX + accessibility contrast ratios 5. **Propose A/B test hypotheses** for top 3 ideas ### Output Format (use exact headings): **1. Component Breakdown** - … **2. Heuristic & WCAG Audit** | # | Element | Issue | Guideline Violated | Impact | Effort | |---|---------|-------|--------------------|--------|--------| | 1 | … | … | … | πŸ”΄ | πŸ’§ | **3. Recommendations** #### Quick Wins 1. … #### Deeper Redesign - … #### Accessibility - … **4. Code Samples** \`\`\`jsx /* Example React + Tailwind snippet */ … \`\`\` **5. A/B Test Ideas** * … ### Style Rules * Be concise but specificβ€”no generic advice * Use bullet lists and tables, not dense paragraphs * Base comments strictly on what you seeβ€”no assumptions * When uncertain, flag as "Assumption" not fact`; } /** * Capture screenshot with enhanced anti-bot detection and retry logic */ async captureScreenshot(url, options = {}) { return this.screenshotCapture.captureWithRetry(url, { ...options, timeout: options.timeout || this.timeout }); } /** * Display image in terminal with enhanced quality */ async displayImageInTerminal(imagePath, options = {}) { await this.imageDisplay.displayImage(imagePath, { ...options }); } /** * Convert image to base64 for API */ async imageToBase64(imagePath) { const imageBuffer = await fs.readFile(imagePath); return imageBuffer.toString('base64'); } /** * Call v0 API with expert UI/UX review prompt */ async callAIAPI(imageBase64, options) { // Prepare prompt variables const promptVariables = { url: options.url, image_path: options.screenshotPath, context: options.context, device: options.mobile ? 'mobile' : 'desktop', model: options.model || 'v0', deep_dive: options.deepDive || options.verbose, extract_styles: options.extractStyles, grid_size: options.gridSize || 10, batch_id: options.batchId }; // If custom prompt is provided, use it directly if (options.customPrompt) { const messages = [ { role: 'user', content: options.customPrompt, imageUrl: `data:image/png;base64,${imageBase64}` } ]; return this.aiService.chat(messages, { model: options.model, temperature: 0.7, maxTokens: options.deepDive ? 8000 : 4096 }); } // Use model-specific prompts const messages = [ { role: 'user', content: 'Please analyze this UI screenshot and provide a comprehensive review.', imageUrl: `data:image/png;base64,${imageBase64}` } ]; return this.aiService.chat(messages, { model: options.model, promptVariables, temperature: 0.7, maxTokens: options.deepDive ? 8000 : 4096 }); } /** * Parse the structured response from v0 API */ parseResponse(content) { const sections = { componentBreakdown: '', heuristicAudit: '', recommendations: '', codeSamples: '', abTestIdeas: '' }; // Split content by the expected headings const parts = content.split(/\*\*\d+\.\s+/); for (const part of parts) { if (part.includes('Component Breakdown')) { sections.componentBreakdown = part.replace('Component Breakdown**', '').trim(); } else if (part.includes('Heuristic & WCAG Audit')) { sections.heuristicAudit = part.replace('Heuristic & WCAG Audit**', '').trim(); } else if (part.includes('Recommendations')) { sections.recommendations = part.replace('Recommendations**', '').trim(); } else if (part.includes('Code Samples')) { sections.codeSamples = part.replace('Code Samples**', '').trim(); } else if (part.includes('A/B Test Ideas')) { sections.abTestIdeas = part.replace('A/B Test Ideas**', '').trim(); } } return sections; } /** * Perform complete UI/UX review with CLI enhancements */ async reviewURL(url, options = {}) { const { context, customPrompt, mobile = false, fullPage = true, showImage = true, verbose = false, onProgress, extractStyles = false, styleOutputPath, styleFormat = 'json' } = options; try { if (verbose) console.log(`πŸ“Έ Capturing ${mobile ? 'mobile' : 'desktop'} screenshot...`); let screenshotPath; let designTokens; if (extractStyles) { // Use enhanced capture for style extraction const captureResult = await this.enhancedCapture.captureWithStyles({ url, mobile, fullPage, outputPath: options.outputPath?.replace(/\.(md|txt)$/, '.png'), extractStyles: true, verbose, onProgress }); screenshotPath = captureResult.screenshotPath; // Process extracted styles if available if (captureResult.extractedStyles && captureResult.designTokens) { const styleExtractor = new StyleExtractor(null); // We don't need page here // Generate output based on format switch (styleFormat) { case 'css': designTokens = styleExtractor.exportAsCSSVariables(captureResult.designTokens); break; case 'tailwind': designTokens = styleExtractor.exportAsTailwindConfig(captureResult.designTokens); break; default: designTokens = styleExtractor.exportAsJSON(captureResult.extractedStyles, captureResult.designTokens); } if (verbose) console.log(`🎨 Extracted ${captureResult.extractedStyles.length} style samples`); } } else { // Use regular screenshot capture screenshotPath = await this.captureScreenshot(url, { mobile, fullPage, outputPath: options.outputPath?.replace(/\.(md|txt)$/, '.png'), onProgress }); } // Step 2: Display image in terminal if requested if (showImage) { onProgress?.('Image Processing', 10, 'Displaying image...'); await this.displayImageInTerminal(screenshotPath, { width: mobile ? 50 : 80, height: mobile ? 30 : 40, verbose }); onProgress?.('Image Processing', 100, 'Image displayed'); } if (verbose) console.log('πŸ€– Analyzing UI/UX with v0 AI...'); // Step 3: Convert to base64 onProgress?.('Image Processing', 50, 'Processing image...'); const imageBase64 = await this.imageToBase64(screenshotPath); onProgress?.('Image Processing', 100, 'Image processed'); // Step 4: Prepare prompt const prompt = customPrompt || this.getExpertPrompt(context); // Step 5: Call AI API onProgress?.('API Analysis', 10, 'Sending to AI...'); const analysis = await this.callAIAPI(imageBase64, { ...options, context: options.context, customPrompt: prompt }); onProgress?.('API Analysis', 90, 'Analysis complete'); // Step 6: Parse response onProgress?.('Report Generation', 50, 'Parsing results...'); const parsedResult = this.parseResponse(analysis); onProgress?.('Report Generation', 100, 'Report ready'); return { ...parsedResult, screenshot: screenshotPath, analysisTimestamp: new Date().toISOString(), url, designTokens }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('Rate limit')) { throw new Error('🚫 Rate limit exceeded. v0 API allows 200 requests/day. Try again tomorrow.'); } else if (errorMessage.includes('Invalid API key')) { throw new Error('πŸ”‘ Invalid API key. Get your key from https://v0.dev and set V0_API_KEY environment variable.'); } else { throw error; } } } /** * Review an existing screenshot */ async reviewScreenshot(screenshotPath, options = {}) { const { context, customPrompt, showImage = true, verbose = false, onProgress } = options; try { // Step 1: Validate and display image onProgress?.('Image Validation', 50, 'Checking image...'); onProgress?.('Image Validation', 100, 'Image valid'); if (showImage) { onProgress?.('Image Processing', 10, 'Displaying image...'); await this.displayImageInTerminal(screenshotPath, { verbose }); onProgress?.('Image Processing', 50, 'Image displayed'); } if (verbose) console.log('πŸ€– Analyzing screenshot with v0 AI...'); // Step 2: Convert to base64 onProgress?.('Image Processing', 80, 'Processing image...'); const imageBase64 = await this.imageToBase64(screenshotPath); onProgress?.('Image Processing', 100, 'Image processed'); // Step 3: Prepare prompt const prompt = customPrompt || this.getExpertPrompt(context); // Step 4: Call AI API onProgress?.('API Analysis', 10, 'Sending to AI...'); const analysis = await this.callAIAPI(imageBase64, { ...options, context: options.context, customPrompt: prompt, screenshotPath }); onProgress?.('API Analysis', 90, 'Analysis complete'); // Step 5: Parse response onProgress?.('Report Generation', 50, 'Parsing results...'); const parsedResult = this.parseResponse(analysis); onProgress?.('Report Generation', 100, 'Report ready'); return { ...parsedResult, screenshot: screenshotPath, analysisTimestamp: new Date().toISOString() }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('Rate limit')) { throw new Error('🚫 Rate limit exceeded. v0 API allows 200 requests/day. Try again tomorrow.'); } else if (errorMessage.includes('Invalid API key')) { throw new Error('πŸ”‘ Invalid API key. Get your key from https://v0.dev and set V0_API_KEY environment variable.'); } else { throw error; } } } /** * Format analysis as markdown */ formatAsMarkdown(analysis) { const timestamp = new Date(analysis.analysisTimestamp).toLocaleString(); return `# 🎨 V0 UI/UX Expert Review **Generated:** ${timestamp} ${analysis.url ? `**URL:** ${analysis.url}` : ''} ${analysis.screenshot ? `**Screenshot:** ${analysis.screenshot}` : ''} --- ## 1. Component Breakdown ${analysis.componentBreakdown} ## 2. Heuristic & WCAG Audit ${analysis.heuristicAudit} ## 3. Recommendations ${analysis.recommendations} ## 4. Code Samples ${analysis.codeSamples} ## 5. A/B Test Ideas ${analysis.abTestIdeas} --- *Generated by V0 UI/UX Expert Reviewer CLI* *Get your own at: https://github.com/your-username/v0-ui-reviewer-cli* `; } /** * Save analysis to file */ async saveAnalysis(analysis, outputPath) { const markdown = this.formatAsMarkdown(analysis); // Ensure output directory exists const outputDir = path.dirname(path.resolve(outputPath)); await fs.mkdir(outputDir, { recursive: true }); await fs.writeFile(outputPath, markdown, 'utf-8'); } } export default V0UIReviewerCLI;