UNPKG

v0-ui-reviewer

Version:

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

303 lines (302 loc) • 12.8 kB
import chalk from 'chalk'; import readline from 'readline'; import { V0UIReviewerCLI } from './index.js'; import { MultiModelAIService } from './ai-service.js'; import { configManager } from './config.js'; import path from 'path'; export class SimpleInteractiveSession { rl; state; isProcessing = false; constructor(reviewer, options = {}) { this.state = { mode: 'remote', model: configManager.get('defaultAIModel') || 'v0', reviewer, aiService: new MultiModelAIService({ verbose: options.verbose }), verbose: options.verbose || false, isReviewing: false, currentUrl: options.initialUrl ? (options.initialUrl.startsWith('http') ? options.initialUrl : `https://${options.initialUrl}`) : undefined }; // Create readline interface with custom prompt this.rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: this.getPrompt(), terminal: true }); // Handle line input this.rl.on('line', async (line) => { if (this.isProcessing) return; this.isProcessing = true; await this.handleInput(line.trim()); this.isProcessing = false; // Update and show prompt this.rl.setPrompt(this.getPrompt()); this.rl.prompt(); }); // Handle Ctrl+C gracefully this.rl.on('SIGINT', () => { console.log(chalk.yellow('\n\nExiting... (press Ctrl+C again to force quit)')); this.cleanup(); process.exit(0); }); } getPrompt() { const mode = this.state.mode === 'remote' ? '🌐' : 'šŸ“'; const location = this.state.mode === 'remote' ? (this.state.currentUrl || 'no url set') : path.basename(this.state.currentPath || process.cwd()); const model = chalk.gray(`[${this.state.model}]`); return `${mode} ${chalk.cyan(location)} ${model} › `; } async handleInput(input) { if (!input) return; // Handle commands if (input.startsWith('/')) { await this.handleCommand(input); } else { // Natural chat about the current context await this.chat(input); } } async handleCommand(input) { const [cmd, ...args] = input.split(' '); const arg = args.join(' ').trim(); switch (cmd) { case '/help': case '/?': this.showHelp(); break; case '/url': if (!arg) { console.log(chalk.yellow('Current URL:'), this.state.currentUrl || 'none'); console.log(chalk.gray('Usage: /url <website-url>')); } else { this.state.currentUrl = arg.startsWith('http') ? arg : `https://${arg}`; console.log(chalk.green('āœ“'), `URL set to: ${this.state.currentUrl}`); console.log(chalk.gray('Now run /review to analyze it')); } break; case '/review': case '/r': await this.review(); break; case '/model': case '/m': if (!arg) { await this.showModels(); } else { await this.setModel(arg); } break; case '/verbose': case '/v': this.state.verbose = !this.state.verbose; console.log(chalk.yellow('Verbose mode:'), this.state.verbose ? 'ON' : 'OFF'); break; case '/local': this.state.mode = 'local'; this.state.currentPath = process.cwd(); console.log(chalk.green('āœ“'), 'Switched to local mode'); console.log(chalk.gray('Current directory:'), this.state.currentPath); break; case '/remote': this.state.mode = 'remote'; console.log(chalk.green('āœ“'), 'Switched to remote mode'); if (this.state.currentUrl) { console.log(chalk.gray('Current URL:'), this.state.currentUrl); } break; case '/clear': case '/c': console.clear(); this.showHeader(); break; case '/exit': case '/quit': case '/q': this.cleanup(); process.exit(0); break; default: console.log(chalk.red('Unknown command:'), cmd); console.log(chalk.gray('Type /help for available commands')); } } async review() { if (this.state.mode === 'remote' && !this.state.currentUrl) { console.log(chalk.red('āŒ No URL set. Use /url <website> first')); return; } if (this.state.isReviewing) { console.log(chalk.yellow('ā³ Review already in progress...')); return; } this.state.isReviewing = true; try { console.log(chalk.blue('\nšŸ”„ Starting review...')); if (this.state.mode === 'remote') { const result = await this.state.reviewer.reviewURL(this.state.currentUrl, { model: this.state.model, verbose: this.state.verbose, showImage: true }); this.state.lastScreenshot = result.screenshot; // Show results console.log(chalk.green('\nāœ… Review complete!\n')); if (!this.state.verbose) { // Show summary only console.log(chalk.bold('Quick Summary:')); const lines = result.componentBreakdown.split('\n').slice(0, 5); console.log(lines.join('\n') + '...\n'); console.log(chalk.gray('Use /verbose and /review again for full analysis')); } else { // Show full analysis console.log(result.componentBreakdown); console.log('\n' + result.heuristicAudit); console.log('\n' + result.recommendations); } } else { console.log(chalk.yellow('Local mode analysis coming soon...')); } } catch (error) { console.log(chalk.red('āŒ Review failed:'), error instanceof Error ? error.message : error); if (error instanceof Error && error.message.includes('Rate limit')) { console.log(chalk.yellow('šŸ’” Try a different model with /model')); } } finally { this.state.isReviewing = false; } } async chat(message) { if (!this.state.lastScreenshot && this.state.mode === 'remote') { console.log(chalk.yellow('šŸ’” Run /review first to analyze the website')); return; } try { const context = this.state.mode === 'remote' ? `Discussing the UI/UX of ${this.state.currentUrl}` : `Discussing the project in ${this.state.currentPath}`; console.log(chalk.gray('Thinking...')); const response = await this.state.aiService.chat([ { role: 'system', content: context }, { role: 'user', content: message } ], { model: this.state.model, temperature: 0.7 }); // Clear thinking message process.stdout.write('\r\x1b[K'); console.log(response); } catch (error) { console.log(chalk.red('āŒ Chat error:'), error instanceof Error ? error.message : error); } } async showModels() { console.log(chalk.bold('\nšŸ“Š Available AI Models:\n')); const models = [ { id: 'v0', name: 'V0 UI/UX Expert', desc: 'Specialized for UI/UX analysis' }, { id: 'gpt-4', name: 'GPT-4', desc: 'OpenAI\'s most capable model' }, { id: 'o3-mini', name: 'O3 Mini', desc: 'Fast and efficient' }, { id: 'claude-sonnet-4', name: 'Claude Sonnet 4', desc: 'Anthropic\'s latest model' } ]; for (const model of models) { const available = await this.state.aiService.isModelAvailable(model.id); const current = this.state.model === model.id ? chalk.green(' (current)') : ''; const status = available ? chalk.green('āœ“') : chalk.red('āœ—'); console.log(` ${status} ${chalk.bold(model.id)}${current}`); console.log(` ${chalk.gray(model.desc)}\n`); } if (this.state.model) { console.log(chalk.gray('Change with: /model <model-id>')); } } async setModel(modelId) { const validModels = ['v0', 'gpt-4', 'o3-mini', 'claude-sonnet-4', 'claude-3-opus', 'claude-3-sonnet']; if (!validModels.includes(modelId)) { console.log(chalk.red('āŒ Invalid model:'), modelId); console.log(chalk.gray('Valid models:'), validModels.join(', ')); return; } const available = await this.state.aiService.isModelAvailable(modelId); if (!available) { console.log(chalk.red('āŒ Model not available:'), modelId); console.log(chalk.gray('Check your API keys with: v0-review --setup')); return; } this.state.model = modelId; console.log(chalk.green('āœ“'), `Model changed to: ${modelId}`); } showHelp() { console.log(chalk.bold('\nšŸŽØ V0-Review Commands\n')); const commands = [ { cmd: '/url <website>', desc: 'Set website to review', example: '/url tacolabs.ai' }, { cmd: '/review', desc: 'Analyze the current website', alias: '/r' }, { cmd: '/model [id]', desc: 'Show/change AI model', alias: '/m' }, { cmd: '/verbose', desc: 'Toggle detailed output', alias: '/v' }, { cmd: '/local', desc: 'Switch to local mode' }, { cmd: '/remote', desc: 'Switch to remote mode' }, { cmd: '/clear', desc: 'Clear screen', alias: '/c' }, { cmd: '/help', desc: 'Show this help', alias: '/?' }, { cmd: '/exit', desc: 'Exit the program', alias: '/q' } ]; commands.forEach(({ cmd, desc, alias, example }) => { const cmdText = chalk.cyan(cmd.padEnd(20)); const aliasText = alias ? chalk.gray(` (${alias})`) : ''; console.log(` ${cmdText} ${desc}${aliasText}`); if (example) { console.log(chalk.gray(` ${''.padEnd(20)} Example: ${example}\n`)); } }); console.log(chalk.gray('\nšŸ’” Tip: Type anything without / to chat about the current design\n')); } showHeader() { console.log(chalk.bold.magenta(` ╔════════════════════════════════════════╗ ā•‘ V0-Review Interactive ā•‘ ā•‘ UI/UX Analysis Assistant ā•‘ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā• `)); console.log(chalk.gray('Type /help for commands\n')); } cleanup() { this.rl.close(); } async start() { console.clear(); this.showHeader(); // Quick start guide console.log(chalk.yellow('Quick Start:')); console.log(' 1. Set a URL: ' + chalk.cyan('/url tacolabs.ai')); console.log(' 2. Review it: ' + chalk.cyan('/review')); console.log(' 3. Chat about it: Just type your questions!\n'); // Show if URL was pre-set if (this.state.currentUrl) { console.log(chalk.green('āœ“'), `URL set to: ${this.state.currentUrl}`); } // Update prompt after setting URL this.rl.setPrompt(this.getPrompt()); this.rl.prompt(); } } export async function runSimpleInteractiveMode(options = {}) { const apiKey = configManager.getApiKey(); if (!apiKey) { console.error(chalk.red('No API key found. Run: v0-review --setup')); process.exit(1); } const reviewer = new V0UIReviewerCLI(apiKey, { verbose: options.verbose }); const session = new SimpleInteractiveSession(reviewer, options); await session.start(); }