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
JavaScript
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;