UNPKG

v0-ui-reviewer

Version:

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

300 lines (293 loc) • 11.7 kB
import chalk from 'chalk'; import inquirer from 'inquirer'; import ora from 'ora'; import boxen from 'boxen'; import { MultiModelAIService } from './ai-service.js'; import { V0UIReviewerCLI } from './index.js'; import { configManager } from './config.js'; import { logger } from './logger.js'; import path from 'path'; import { promises as fs } from 'fs'; export class DesignChatSession { aiService; v0Reviewer; messages = []; screenshotPath; screenshotBase64; currentModel; verbose; constructor(options) { this.screenshotPath = options.screenshotPath; this.verbose = options.verbose || false; // Initialize AI service this.aiService = new MultiModelAIService({ verbose: this.verbose }); // Initialize V0 reviewer for UI/UX expertise const v0ApiKey = configManager.getApiKey(); if (!v0ApiKey) { throw new Error('V0 API key is required for design chat'); } this.v0Reviewer = new V0UIReviewerCLI(v0ApiKey, { verbose: this.verbose }); // Set default model const availableModels = this.aiService.getAvailableModels(); this.currentModel = options.model || configManager.get('defaultAIModel') || availableModels[0] || 'v0'; // Initialize conversation with context if (options.initialAnalysis) { this.messages.push({ role: 'system', content: `You are an AI design assistant helping to improve UI/UX. Here's the initial analysis of the design:\n\n${options.initialAnalysis}` }); } else { this.messages.push({ role: 'system', content: 'You are an AI design assistant helping to improve UI/UX. You have access to a screenshot of the current design and can provide specific, actionable feedback.' }); } } async start() { console.clear(); // Display header console.log(boxen(`šŸŽØ ${chalk.bold('Interactive Design Chat')}\n\n` + `šŸ“· Screenshot: ${chalk.cyan(path.basename(this.screenshotPath))}\n` + `šŸ¤– AI Model: ${chalk.green(this.currentModel)}\n` + `šŸ’¬ Type your questions or design requests\n` + `šŸ“ Commands: /model, /analyze, /tokens, /save, /exit`, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'cyan' })); // Load screenshot as base64 await this.loadScreenshot(); // Start chat loop await this.chatLoop(); } async loadScreenshot() { const imageBuffer = await fs.readFile(this.screenshotPath); this.screenshotBase64 = `data:image/png;base64,${imageBuffer.toString('base64')}`; } async chatLoop() { while (true) { const { input } = await inquirer.prompt({ type: 'input', name: 'input', message: chalk.green('You:') }); if (!input.trim()) continue; // Handle commands if (input.startsWith('/')) { const handled = await this.handleCommand(input); if (handled === 'exit') break; continue; } // Regular chat await this.processChat(input); } } async handleCommand(command) { const [cmd, ...args] = command.split(' '); switch (cmd) { case '/exit': case '/quit': console.log(chalk.yellow('\nšŸ‘‹ Ending design chat session...\n')); return 'exit'; case '/model': await this.switchModel(); break; case '/analyze': await this.performDetailedAnalysis(); break; case '/tokens': await this.extractDesignTokens(); break; case '/save': await this.saveConversation(args[0]); break; case '/help': this.showHelp(); break; default: console.log(chalk.red(`Unknown command: ${cmd}`)); this.showHelp(); } } showHelp() { console.log(boxen(chalk.bold('Available Commands:\n\n') + '/model - Switch AI model\n' + '/analyze - Perform detailed UI/UX analysis\n' + '/tokens - Extract design tokens (colors, fonts, spacing)\n' + '/save [file] - Save conversation to file\n' + '/exit - End chat session\n' + '/help - Show this help', { padding: 1, borderStyle: 'single', borderColor: 'gray' })); } async switchModel() { const availableModels = this.aiService.getAvailableModels(); if (availableModels.length === 0) { console.log(chalk.red('No AI models configured. Please set API keys.')); return; } const { model } = await inquirer.prompt([{ type: 'list', name: 'model', message: 'Select AI model:', choices: availableModels.map(m => ({ name: `${m} ${m === this.currentModel ? '(current)' : ''}`, value: m })) }]); this.currentModel = model; console.log(chalk.green(`āœ… Switched to ${model}`)); } async performDetailedAnalysis() { const spinner = ora('Performing detailed UI/UX analysis...').start(); try { // Use V0 for expert UI/UX analysis const messages = [ { role: 'user', content: 'Please provide a detailed UI/UX analysis of this design, including component breakdown, accessibility issues, and specific improvement recommendations.', imageUrl: this.screenshotBase64 } ]; const analysis = await this.aiService.chat(messages, { model: 'v0', temperature: 0.7 }); spinner.succeed('Analysis complete'); console.log(boxen(chalk.bold('šŸ” Detailed UI/UX Analysis\n\n') + analysis, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue' })); // Add to conversation history this.messages.push({ role: 'user', content: 'Perform detailed UI/UX analysis' }, { role: 'assistant', content: analysis }); } catch (error) { spinner.fail('Analysis failed'); console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error')); } } async extractDesignTokens() { const spinner = ora('Extracting design tokens...').start(); try { const prompt = `Analyze this UI screenshot and extract the following design tokens: 1. **Color Palette**: List all colors used with their HEX values and usage (primary, secondary, background, text, etc.) 2. **Typography**: Font families, sizes, weights, and line heights for different text elements 3. **Spacing**: Common spacing values (margins, paddings) in pixels 4. **Border Radius**: Corner radius values used 5. **Shadows**: Box shadow definitions 6. **Grid/Layout**: Column widths, gaps, breakpoints if visible Format the output as structured data that could be used in a design system.`; const messages = [ { role: 'user', content: prompt, imageUrl: this.screenshotBase64 } ]; const tokens = await this.aiService.chat(messages, { model: this.currentModel, temperature: 0.3 // Lower temperature for more consistent extraction }); spinner.succeed('Design tokens extracted'); console.log(boxen(chalk.bold('šŸŽØ Design Tokens\n\n') + tokens, { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'magenta' })); // Add to conversation history this.messages.push({ role: 'user', content: 'Extract design tokens' }, { role: 'assistant', content: tokens }); } catch (error) { spinner.fail('Token extraction failed'); console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error')); } } async processChat(input) { const spinner = ora('Thinking...').start(); try { // Add user message this.messages.push({ role: 'user', content: input }); // Include screenshot in the conversation for models that support it const modelCapabilities = this.aiService.getModelCapabilities(this.currentModel); const messagesWithImage = [...this.messages]; if (modelCapabilities.supportsImages && this.screenshotBase64) { // Add image to the last user message messagesWithImage[messagesWithImage.length - 1].imageUrl = this.screenshotBase64; } // Get AI response const response = await this.aiService.chat(messagesWithImage, { model: this.currentModel, temperature: 0.7 }); spinner.stop(); // Display response console.log(chalk.blue('\nAI:'), response, '\n'); // Add to history this.messages.push({ role: 'assistant', content: response }); } catch (error) { spinner.fail('Failed to get response'); console.error(chalk.red(error instanceof Error ? error.message : 'Unknown error')); } } async saveConversation(filename) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const outputPath = filename || `design-chat-${timestamp}.md`; try { const content = this.formatConversationAsMarkdown(); await fs.writeFile(outputPath, content, 'utf-8'); console.log(chalk.green(`āœ… Conversation saved to: ${outputPath}`)); } catch (error) { console.error(chalk.red(`Failed to save conversation: ${error instanceof Error ? error.message : 'Unknown error'}`)); } } formatConversationAsMarkdown() { const timestamp = new Date().toLocaleString(); let markdown = `# Design Chat Session **Date:** ${timestamp} **Screenshot:** ${this.screenshotPath} **AI Model:** ${this.currentModel} --- ## Conversation `; for (const msg of this.messages) { if (msg.role === 'system') continue; const prefix = msg.role === 'user' ? '### šŸ‘¤ User' : '### šŸ¤– AI'; markdown += `${prefix}\n\n${msg.content}\n\n`; } markdown += `\n---\n\n*Generated by V0 UI/UX Reviewer - Design Chat*`; return markdown; } } export async function startDesignChat(screenshotPath, options = {}) { try { const session = new DesignChatSession({ screenshotPath, ...options }); await session.start(); } catch (error) { logger.error('Design chat error:', error instanceof Error ? error.message : String(error)); throw error; } }