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
JavaScript
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();
}