UNPKG

jpglens

Version:

šŸ” Universal AI-Powered UI Testing - See your interfaces through the lens of intelligence

1,394 lines (1,367 loc) • 111 kB
import fs, { writeFileSync, readFileSync } from 'fs'; import path, { join } from 'path'; import 'url'; /** * šŸ” jpglens - Master Prompt System * Universal AI-Powered UI Testing * * @author Taha Bahrami (Kaito) * @license MIT */ /** * Master prompt template that provides comprehensive context to AI models * This is the core intelligence that makes jpglens understand user experience */ function createMasterPrompt(context, analysisTypes) { const { userContext, businessContext, technicalContext, stage, userIntent, criticalElements } = context; return `You are a world-class UX expert, accessibility specialist, and design systems consultant analyzing a user interface through the lens of REAL USER EXPERIENCE. šŸŽÆ **ANALYSIS CONTEXT** User Stage: ${stage} User Intent: ${userIntent} Critical Elements: ${criticalElements?.join(', ') || 'All visible elements'} šŸ‘¤ **USER CONTEXT** ${formatUserContext(userContext)} šŸ¢ **BUSINESS CONTEXT** ${formatBusinessContext(businessContext)} āš™ļø **TECHNICAL CONTEXT** ${formatTechnicalContext(technicalContext)} šŸ” **ANALYSIS REQUIREMENTS** You must analyze this interface for: ${analysisTypes.join(', ')} ${generateAnalysisInstructions(analysisTypes)} šŸ“Š **RESPONSE FORMAT** Provide your analysis in this EXACT format: **šŸŽÆ OVERALL UX SCORE: X/10** **āœ… STRENGTHS:** - [What works exceptionally well for this specific user context] **🚨 CRITICAL ISSUES:** (Blocks user success) - [Issues that prevent task completion or cause significant user frustration] **āš ļø MAJOR ISSUES:** (Impacts user experience) - [Problems that make the interface difficult or unpleasant to use] **šŸ’” MINOR ISSUES:** (Polish opportunities) - [Small improvements that would enhance the experience] **šŸ› ļø SPECIFIC RECOMMENDATIONS:** For each issue, provide: 1. **Problem**: Clear description 2. **Impact**: How it affects the user in this context 3. **Solution**: Specific, actionable fix 4. **Priority**: Critical/Major/Minor **šŸŽØ CONTEXTUAL INSIGHTS:** - How well does this interface serve the user's specific intent: "${userIntent}"? - What would make this experience more successful for this user context? - Are there any missed opportunities for this business context? **šŸ“± DEVICE-SPECIFIC NOTES:** - Any issues specific to the ${userContext.deviceContext} experience? - Touch targets, readability, interaction patterns? Remember: This user is ${getUserPersonaDescription(userContext.persona)} in the context of ${stage}. Every recommendation should consider their specific needs, constraints, and goals. Be specific, actionable, and focused on REAL USER SUCCESS.`; } /** * Format user context for the prompt */ function formatUserContext(userContext) { const persona = typeof userContext.persona === 'string' ? `Persona: ${userContext.persona}` : formatPersonaDetails(userContext.persona); return `${persona} Device Context: ${userContext.deviceContext} Expertise Level: ${userContext.expertise || 'Not specified'} Time Constraint: ${userContext.timeConstraint || 'Normal'} Trust Level: ${userContext.trustLevel || 'Medium'} Business Goals: ${userContext.businessGoals?.join(', ') || 'Not specified'}`; } /** * Format business context for the prompt */ function formatBusinessContext(businessContext) { if (!businessContext) return 'Not specified'; return `Industry: ${businessContext.industry} Conversion Goal: ${businessContext.conversionGoal} Competitive Advantage: ${businessContext.competitiveAdvantage || 'Not specified'} Brand Personality: ${businessContext.brandPersonality || 'Not specified'} Target Audience: ${businessContext.targetAudience || 'Not specified'}`; } /** * Format technical context for the prompt */ function formatTechnicalContext(technicalContext) { if (!technicalContext) return 'Not specified'; return `Framework: ${technicalContext.framework || 'Not specified'} Design System: ${technicalContext.designSystem || 'Not specified'} Device Support: ${technicalContext.deviceSupport} Performance Target: ${technicalContext.performanceTarget || 'Not specified'} Accessibility Target: ${technicalContext.accessibilityTarget || 'WCAG AA'}`; } /** * Format persona details for the prompt */ function formatPersonaDetails(persona) { return `Persona: ${persona.name} Expertise: ${persona.expertise} Primary Device: ${persona.device} Urgency: ${persona.urgency} Goals: ${persona.goals.join(', ')} Pain Points: ${persona.painPoints?.join(', ') || 'Not specified'} Context: ${persona.context || 'Not specified'}`; } /** * Get user persona description for contextual understanding */ function getUserPersonaDescription(persona) { if (!persona) return 'a general user'; if (typeof persona === 'string') return persona; return `${persona.name} (${persona.expertise} level, ${persona.device} user, ${persona.urgency} urgency)`; } /** * Generate specific analysis instructions based on requested types */ function generateAnalysisInstructions(analysisTypes) { const instructions = []; if (analysisTypes.includes('usability')) { instructions.push(` **🧭 USABILITY ANALYSIS:** - Is the interface intuitive for this user's expertise level? - Can the user complete their intended task efficiently? - Are there any confusing or misleading elements? - Does the information architecture make sense? - Are interactive elements clearly identifiable?`); } if (analysisTypes.includes('accessibility')) { instructions.push(` **♿ ACCESSIBILITY ANALYSIS:** - WCAG 2.1 compliance (AA minimum, AAA preferred) - Color contrast ratios (4.5:1 for normal text, 3:1 for large text) - Keyboard navigation support - Screen reader compatibility (semantic HTML, ARIA labels) - Focus management and visual focus indicators - Alternative text for images - Form labels and error handling`); } if (analysisTypes.includes('visual-design')) { instructions.push(` **šŸŽØ VISUAL DESIGN ANALYSIS:** - Typography hierarchy and readability - Color usage and brand consistency - Visual balance and composition - Spacing and layout effectiveness - Responsive design quality - Visual feedback for interactions - Overall aesthetic appeal and professionalism`); } if (analysisTypes.includes('performance')) { instructions.push(` **⚔ PERFORMANCE ANALYSIS:** - Perceived performance and loading states - Image optimization and lazy loading - Critical rendering path - User perception of speed - Progressive enhancement - Mobile performance considerations`); } if (analysisTypes.includes('mobile-optimization')) { instructions.push(` **šŸ“± MOBILE OPTIMIZATION ANALYSIS:** - Touch target sizes (minimum 44px) - Thumb-friendly navigation - Mobile-first responsive design - Portrait/landscape orientation support - Mobile-specific interaction patterns - One-handed usage considerations`); } if (analysisTypes.includes('conversion-optimization')) { instructions.push(` **šŸ’° CONVERSION OPTIMIZATION ANALYSIS:** - Clear value proposition presentation - Friction points in the conversion funnel - Trust signals and credibility indicators - Call-to-action effectiveness - Form optimization - User motivation and persuasion elements`); } if (analysisTypes.includes('brand-consistency')) { instructions.push(` **šŸŽÆ BRAND CONSISTENCY ANALYSIS:** - Design system adherence - Brand voice and tone in copy - Visual identity consistency - Component usage patterns - Brand personality reflection - Cross-platform consistency`); } if (analysisTypes.includes('error-handling')) { instructions.push(` **🚨 ERROR HANDLING ANALYSIS:** - Error prevention strategies - Clear error messaging - Recovery path availability - User guidance during errors - Validation feedback timing - Graceful degradation`); } return instructions.join('\n'); } /** * Create specialized prompts for specific scenarios */ const SpecializedPrompts = { /** * E-commerce focused analysis */ ecommerce: (context) => ` ${createMasterPrompt(context, ['usability', 'conversion-optimization', 'mobile-optimization'])} **šŸ›’ E-COMMERCE SPECIFIC FOCUS:** - Product discoverability and presentation - Shopping cart and checkout flow optimization - Trust signals and security indicators - Mobile shopping experience - Price presentation and value communication - Return policy and customer service accessibility`, /** * SaaS application analysis */ saas: (context) => ` ${createMasterPrompt(context, ['usability', 'accessibility', 'performance'])} **šŸ’¼ SAAS SPECIFIC FOCUS:** - Dashboard clarity and information hierarchy - Feature discoverability and onboarding - Data visualization effectiveness - Workflow efficiency - User role and permission clarity - Integration and API documentation accessibility`, /** * Design system component analysis */ designSystem: (context) => ` ${createMasterPrompt(context, ['visual-design', 'accessibility', 'brand-consistency'])} **šŸŽØ DESIGN SYSTEM SPECIFIC FOCUS:** - Component consistency and reusability - Documentation clarity and completeness - Implementation flexibility - Accessibility built-in by default - Responsive behavior patterns - Cross-browser compatibility`, /** * Mobile app analysis */ mobileApp: (context) => ` ${createMasterPrompt(context, ['mobile-optimization', 'usability', 'performance'])} **šŸ“± MOBILE APP SPECIFIC FOCUS:** - Native platform conventions adherence - Gesture support and touch interactions - Offline functionality and sync - App store guidelines compliance - Battery and performance optimization - Push notification integration`, }; /** * šŸ” jpglens - OpenRouter AI Provider * Universal AI-Powered UI Testing * * @author Taha Bahrami (Kaito) * @license MIT */ /** * OpenRouter AI provider for jpglens * Supports multiple AI models through OpenRouter's unified API */ class OpenRouterProvider { config; name = 'OpenRouter'; baseUrl; apiKey; model; constructor(config) { this.config = config; this.baseUrl = config.ai.baseUrl || 'https://openrouter.ai/api/v1'; this.apiKey = config.ai.apiKey; this.model = config.ai.model; if (!this.apiKey) { throw new Error('OpenRouter API key is required. Set JPGLENS_API_KEY environment variable.'); } } /** * Check if OpenRouter is available */ async isAvailable() { try { const response = await fetch(`${this.baseUrl}/models`, { headers: { Authorization: `Bearer ${this.apiKey}`, 'HTTP-Referer': 'https://jpglens.dev', 'X-Title': 'jpglens - Universal AI UI Testing', }, }); return response.ok; } catch (error) { console.warn('OpenRouter availability check failed:', error); return false; } } /** * Get model information */ getModelInfo() { return { name: this.model, capabilities: ['vision', 'text-analysis', 'code-generation', 'accessibility-analysis'], }; } /** * Analyze screenshot with OpenRouter */ async analyze(screenshot, context, prompt) { const startTime = Date.now(); try { // Validate inputs if (!screenshot.buffer || screenshot.buffer.length === 0) { throw new Error('Invalid screenshot data'); } // Convert screenshot to base64 const base64Image = screenshot.buffer.toString('base64'); // Prepare request body const requestBody = { model: this.model, messages: [ { role: 'user', content: [ { type: 'text', text: prompt, }, { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}`, detail: this.config.analysis.depth === 'comprehensive' ? 'high' : 'auto', }, }, ], }, ], max_tokens: this.config.ai.maxTokens || 4000, temperature: this.config.ai.temperature || 0.1, stream: false, }; // Make API request const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', 'HTTP-Referer': 'https://jpglens.dev', 'X-Title': 'jpglens - Universal AI UI Testing', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`OpenRouter API error: ${response.status} ${response.statusText} - ${errorText}`); } const result = await response.json(); if (!result.choices || result.choices.length === 0) { throw new Error('No analysis result returned from OpenRouter'); } const analysisText = result.choices[0].message.content; const tokensUsed = result.usage?.total_tokens || 0; // Parse the analysis text into structured result const structuredResult = this.parseAnalysisResult(analysisText, context, tokensUsed, startTime); return structuredResult; } catch (error) { console.error('OpenRouter analysis failed:', error); throw new Error(`OpenRouter analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Parse AI analysis text into structured result */ parseAnalysisResult(analysisText, context, tokensUsed, startTime) { const analysisTime = Date.now() - startTime; // Extract overall score const scoreMatch = analysisText.match(/(?:OVERALL UX SCORE|QUALITY SCORE):\s*(\d+)\/10/i); const overallScore = scoreMatch ? parseInt(scoreMatch[1]) : 5; // Extract sections using regex patterns const strengths = this.extractSection(analysisText, 'STRENGTHS'); const criticalIssues = this.extractIssues(analysisText, 'CRITICAL ISSUES', 'critical'); const majorIssues = this.extractIssues(analysisText, 'MAJOR ISSUES', 'major'); const minorIssues = this.extractIssues(analysisText, 'MINOR ISSUES', 'minor'); const recommendations = this.extractRecommendations(analysisText); // Extract specific scores if available const scores = { usability: this.extractSpecificScore(analysisText, 'usability') || overallScore, accessibility: this.extractSpecificScore(analysisText, 'accessibility') || overallScore, visualDesign: this.extractSpecificScore(analysisText, 'visual') || overallScore, performance: this.extractSpecificScore(analysisText, 'performance') || overallScore, }; return { id: `jpglens-openrouter-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, timestamp: new Date().toISOString(), page: context.pageInfo?.url || 'unknown', context, overallScore, scores, strengths, criticalIssues, majorIssues, minorIssues, recommendations, model: this.model, tokensUsed, analysisTime, provider: 'OpenRouter', rawAnalysis: analysisText, // Keep raw text for debugging }; } /** * Extract a section from the analysis text */ extractSection(text, sectionName) { const regex = new RegExp(`\\*\\*${sectionName}[:\\s]*\\*\\*([\\s\\S]*?)(?=\\*\\*[A-Z]|$)`, 'i'); const match = text.match(regex); if (!match) return []; return match[1] .split(/[-•]\s+/) .filter(item => item.trim().length > 0) .map(item => item.trim().replace(/\n/g, ' ')); } /** * Extract issues from a section */ extractIssues(text, sectionName, severity) { const items = this.extractSection(text, sectionName); return items.map(item => ({ severity, category: this.categorizeIssue(item), title: this.extractTitle(item), description: item, impact: this.getImpactBySeverity(severity), selector: this.extractSelector(item), fix: this.extractFix(item), })); } /** * Extract recommendations from text */ extractRecommendations(text) { const items = this.extractSection(text, 'RECOMMENDATIONS'); return items.map(item => ({ type: this.categorizeRecommendation(item), title: this.extractTitle(item), description: item, implementation: this.extractCodeBlock(item) || item, impact: this.assessImpact(item), effort: this.assessEffort(item), })); } /** * Categorize issue by analyzing content */ categorizeIssue(issueText) { const text = issueText.toLowerCase(); if (text.includes('contrast') || text.includes('accessibility') || text.includes('wcag') || text.includes('screen reader')) { return 'accessibility'; } if (text.includes('mobile') || text.includes('touch') || text.includes('responsive') || text.includes('44px')) { return 'mobile-optimization'; } if (text.includes('performance') || text.includes('loading') || text.includes('speed') || text.includes('slow')) { return 'performance'; } if (text.includes('visual') || text.includes('design') || text.includes('color') || text.includes('typography')) { return 'visual-design'; } if (text.includes('conversion') || text.includes('cta') || text.includes('purchase') || text.includes('signup')) { return 'conversion-optimization'; } return 'usability'; } /** * Extract title from text (first sentence or 50 chars) */ extractTitle(text) { const firstSentence = text.split('.')[0]; return firstSentence.length > 50 ? firstSentence.substring(0, 47) + '...' : firstSentence; } /** * Get impact description by severity */ getImpactBySeverity(severity) { const impacts = { critical: 'Prevents users from completing their tasks or causes significant frustration', major: 'Makes the interface difficult or unpleasant to use, reducing user satisfaction', minor: 'Small improvement that would enhance the overall user experience', }; return impacts[severity]; } /** * Extract CSS selector from issue text */ extractSelector(text) { const selectorPatterns = [ /\.[\w-]+/g, // CSS classes /#[\w-]+/g, // CSS IDs /\[[\w-]+=[\w-]+\]/g, // Attribute selectors /button|input|form|div|span|a/gi, // HTML elements ]; for (const pattern of selectorPatterns) { const matches = text.match(pattern); if (matches) { return matches[0]; } } return undefined; } /** * Extract fix suggestion from text */ extractFix(text) { const fixPatterns = [/(?:fix|solution|recommend)[:\s]+([^.]+)/i, /should[:\s]+([^.]+)/i, /change[:\s]+([^.]+)/i]; for (const pattern of fixPatterns) { const match = text.match(pattern); if (match) { return match[1].trim(); } } return undefined; } /** * Categorize recommendation type */ categorizeRecommendation(text) { const lower = text.toLowerCase(); if (lower.includes('css') || lower.includes('html') || lower.includes('javascript') || lower.includes('```')) { return 'code'; } if (lower.includes('content') || lower.includes('copy') || lower.includes('text') || lower.includes('wording')) { return 'content'; } if (lower.includes('process') || lower.includes('workflow') || lower.includes('team') || lower.includes('testing')) { return 'process'; } return 'design'; } /** * Extract code block from text */ extractCodeBlock(text) { const codeMatch = text.match(/```[\s\S]*?```/); return codeMatch ? codeMatch[0] : undefined; } /** * Assess recommendation impact */ assessImpact(text) { const lower = text.toLowerCase(); if (lower.includes('critical') || lower.includes('conversion') || lower.includes('accessibility') || lower.includes('revenue')) { return 'high'; } if (lower.includes('major') || lower.includes('usability') || lower.includes('satisfaction')) { return 'medium'; } return 'low'; } /** * Assess implementation effort */ assessEffort(text) { const lower = text.toLowerCase(); if (lower.includes('simple') || lower.includes('quick') || lower.includes('css change') || lower.includes('one line')) { return 'low'; } if (lower.includes('redesign') || lower.includes('refactor') || lower.includes('complex') || lower.includes('major change')) { return 'high'; } return 'medium'; } /** * Extract specific score from text */ extractSpecificScore(text, category) { const regex = new RegExp(`${category}[:\\s]*([\\d]+)(?:\/10)?`, 'i'); const match = text.match(regex); return match ? parseInt(match[1]) : undefined; } } /** * šŸ” jpglens - OpenAI Provider * Universal AI-Powered UI Testing * * @author Taha Bahrami (Kaito) * @license MIT */ /** * OpenAI provider for jpglens * Direct integration with OpenAI's GPT-4 Vision and other models */ class OpenAIProvider { config; name = 'OpenAI'; baseUrl; apiKey; model; constructor(config) { this.config = config; this.apiKey = config.ai.apiKey; this.model = config.ai.model?.includes('/') ? config.ai.model.split('/')[1] : config.ai.model; this.baseUrl = config.ai.baseUrl || 'https://api.openai.com/v1'; if (!this.apiKey) { throw new Error('OpenAI API key is required'); } } async isAvailable() { try { const response = await fetch(`${this.baseUrl}/models`, { headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, }); return response.ok; } catch { return false; } } getModelInfo() { return { name: this.model, capabilities: ['vision', 'text-analysis', 'code-generation'], }; } async analyze(screenshot, context, prompt) { const startTime = Date.now(); try { const base64Image = screenshot.buffer.toString('base64'); const requestBody = { model: this.model, messages: [ { role: 'user', content: [ { type: 'text', text: prompt, }, { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Image}`, detail: this.config.analysis.depth === 'comprehensive' ? 'high' : 'auto', }, }, ], }, ], max_tokens: this.config.ai.maxTokens || 4000, temperature: this.config.ai.temperature || 0.1, }; const response = await fetch(`${this.baseUrl}/chat/completions`, { method: 'POST', headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`OpenAI API error: ${response.status} - ${errorText}`); } const result = await response.json(); const analysisText = result.choices[0].message.content; const tokensUsed = result.usage?.total_tokens || 0; return this.parseAnalysisResult(analysisText, context, tokensUsed, startTime); } catch (error) { throw new Error(`OpenAI analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } parseAnalysisResult(analysisText, context, tokensUsed, startTime) { // Similar parsing logic as OpenRouter provider // This is a simplified version - in production you'd want shared parsing utilities const scoreMatch = analysisText.match(/(?:OVERALL UX SCORE|QUALITY SCORE):\s*(\d+)\/10/i); const overallScore = scoreMatch ? parseInt(scoreMatch[1]) : 5; return { id: `jpglens-openai-${Date.now()}`, timestamp: new Date().toISOString(), page: context.pageInfo?.url || 'unknown', context, overallScore, scores: { usability: overallScore, accessibility: overallScore, visualDesign: overallScore, performance: overallScore, }, strengths: [], criticalIssues: [], majorIssues: [], minorIssues: [], recommendations: [], model: this.model, tokensUsed, analysisTime: Date.now() - startTime, provider: 'OpenAI', rawAnalysis: analysisText, }; } } /** * šŸ” jpglens - Anthropic Provider * Universal AI-Powered UI Testing * * @author Taha Bahrami (Kaito) * @license MIT */ /** * Anthropic Claude provider for jpglens * Direct integration with Claude's vision capabilities */ class AnthropicProvider { config; name = 'Anthropic'; baseUrl = 'https://api.anthropic.com/v1'; apiKey; model; constructor(config) { this.config = config; this.apiKey = config.ai.apiKey; this.model = config.ai.model?.includes('/') ? config.ai.model.split('/')[1] : config.ai.model; if (!this.apiKey) { throw new Error('Anthropic API key is required'); } } async isAvailable() { try { const response = await fetch(`${this.baseUrl}/messages`, { method: 'POST', headers: { 'x-api-key': this.apiKey, 'content-type': 'application/json', 'anthropic-version': '2023-06-01', }, body: JSON.stringify({ model: this.model, max_tokens: 10, messages: [{ role: 'user', content: 'test' }], }), }); return response.status !== 401; // Not unauthorized } catch { return false; } } getModelInfo() { return { name: this.model, capabilities: ['vision', 'text-analysis', 'detailed-reasoning'], }; } async analyze(screenshot, context, prompt) { const startTime = Date.now(); try { const base64Image = screenshot.buffer.toString('base64'); const requestBody = { model: this.model, max_tokens: this.config.ai.maxTokens || 4000, messages: [ { role: 'user', content: [ { type: 'text', text: prompt, }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: base64Image, }, }, ], }, ], }; const response = await fetch(`${this.baseUrl}/messages`, { method: 'POST', headers: { 'x-api-key': this.apiKey, 'content-type': 'application/json', 'anthropic-version': '2023-06-01', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Anthropic API error: ${response.status} - ${errorText}`); } const result = await response.json(); const analysisText = result.content[0].text; const tokensUsed = result.usage?.input_tokens + result.usage?.output_tokens || 0; return this.parseAnalysisResult(analysisText, context, tokensUsed, startTime); } catch (error) { throw new Error(`Anthropic analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } parseAnalysisResult(analysisText, context, tokensUsed, startTime) { const scoreMatch = analysisText.match(/(?:OVERALL UX SCORE|QUALITY SCORE):\s*(\d+)\/10/i); const overallScore = scoreMatch ? parseInt(scoreMatch[1]) : 5; return { id: `jpglens-anthropic-${Date.now()}`, timestamp: new Date().toISOString(), page: context.pageInfo?.url || 'unknown', context, overallScore, scores: { usability: overallScore, accessibility: overallScore, visualDesign: overallScore, performance: overallScore, }, strengths: [], criticalIssues: [], majorIssues: [], minorIssues: [], recommendations: [], model: this.model, tokensUsed, analysisTime: Date.now() - startTime, provider: 'Anthropic', rawAnalysis: analysisText, }; } } /** * šŸ” jpglens - Report Generator * Configurable AI Analysis Report Generation System * * @author Taha Bahrami (Kaito) * @license MIT */ /** * Default report templates for different output formats */ const DEFAULT_REPORT_TEMPLATES = { detailed: { name: 'Detailed Analysis Report', format: 'markdown', sections: [ 'executive_summary', 'overall_score', 'visual_hierarchy', 'accessibility', 'usability', 'critical_issues', 'recommendations', 'technical_details', ], prompts: { executive_summary: 'Provide a comprehensive executive summary of the UI analysis', overall_score: 'Rate the overall UI quality from 1-10 with detailed reasoning', visual_hierarchy: 'Analyze the visual hierarchy and information architecture', accessibility: 'Evaluate accessibility compliance and provide specific recommendations', usability: 'Assess usability patterns and user experience quality', critical_issues: 'Identify critical issues that must be addressed immediately', recommendations: 'Provide actionable recommendations for improvement', technical_details: 'Include technical implementation details and metrics', }, }, summary: { name: 'Quick Summary Report', format: 'json', sections: ['overall_score', 'top_issues', 'quick_wins'], prompts: { overall_score: 'Provide an overall quality score from 1-10', top_issues: 'List the top 3 most critical issues', quick_wins: 'Suggest 3 quick improvements that can be implemented immediately', }, }, executive: { name: 'Executive Dashboard Report', format: 'json', sections: ['executive_summary', 'key_metrics', 'business_impact', 'next_actions'], prompts: { executive_summary: 'Provide a business-focused summary suitable for executives', key_metrics: 'Present key performance indicators and quality metrics', business_impact: 'Explain the business impact of identified issues and improvements', next_actions: 'Recommend prioritized actions with timeline and resource estimates', }, }, }; /** * Default report configuration */ const DEFAULT_REPORT_CONFIG = { enabled: true, outputDir: './jpglens-reports', template: 'detailed', format: 'markdown', includeScreenshots: true, includeRawAnalysis: false, timestampFormat: 'ISO', fileNaming: '{timestamp}-{component}-{page}', customPrompts: {}, apiCompatibility: 'auto', // auto-detect based on provider }; /** * Report Generator Class */ class ReportGenerator { config; templates; constructor(config = {}) { this.config = { ...DEFAULT_REPORT_CONFIG, ...config }; this.templates = { ...DEFAULT_REPORT_TEMPLATES }; // Ensure output directory exists this.ensureOutputDirectory(); } /** * Generate a report from analysis results */ async generateReport(analysisResult, customConfig) { if (!this.config.enabled) { return ''; } const reportConfig = { ...this.config, ...customConfig }; const template = this.getTemplate(reportConfig.template); // Generate report content based on format const reportContent = await this.generateReportContent(analysisResult, template, reportConfig); // Save report to file const filePath = await this.saveReport(reportContent, analysisResult, reportConfig); return filePath; } /** * Generate report content based on template and format */ async generateReportContent(result, template, config) { switch (template.format) { case 'markdown': return this.generateMarkdownReport(result, template, config); case 'json': return this.generateJsonReport(result, template, config); case 'html': return this.generateHtmlReport(result, template, config); default: throw new Error(`Unsupported report format: ${template.format}`); } } /** * Generate markdown report */ generateMarkdownReport(result, template, config) { let markdown = `# ${template.name}\n\n`; markdown += `**Generated:** ${this.formatTimestamp(result.timestamp, config.timestampFormat)}\n`; markdown += `**Component:** ${result.component || 'N/A'}\n`; markdown += `**Page:** ${result.page}\n`; markdown += `**Model:** ${result.model}\n`; markdown += `**Analysis Time:** ${result.analysisTime}ms\n\n`; // Add sections based on template for (const section of template.sections) { markdown += this.generateMarkdownSection(section, result, template, config); } // Add technical details if requested if (config.includeRawAnalysis && result.rawAnalysis) { markdown += `## Raw Analysis\n\n\`\`\`\n${result.rawAnalysis}\n\`\`\`\n\n`; } return markdown; } /** * Generate JSON report */ generateJsonReport(result, template, config) { const jsonReport = { metadata: { generated: this.formatTimestamp(result.timestamp, config.timestampFormat), template: template.name, component: result.component, page: result.page, model: result.model, analysisTime: result.analysisTime, tokensUsed: result.tokensUsed, }, analysis: {}, }; // Add sections based on template for (const section of template.sections) { jsonReport.analysis[section] = this.extractSectionData(section, result); } if (config.includeRawAnalysis) { jsonReport.rawAnalysis = result.rawAnalysis; } return JSON.stringify(jsonReport, null, 2); } /** * Generate HTML report */ generateHtmlReport(result, template, config) { let html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>${template.name}</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; } .header { border-bottom: 2px solid #eee; padding-bottom: 1rem; margin-bottom: 2rem; } .section { margin: 2rem 0; } .score { font-size: 2rem; font-weight: bold; color: #007acc; } .issue { background: #fee; padding: 1rem; border-left: 4px solid #e74c3c; margin: 1rem 0; } .recommendation { background: #efe; padding: 1rem; border-left: 4px solid #27ae60; margin: 1rem 0; } </style> </head> <body> <div class="header"> <h1>${template.name}</h1> <p><strong>Generated:</strong> ${this.formatTimestamp(result.timestamp, config.timestampFormat)}</p> <p><strong>Component:</strong> ${result.component || 'N/A'}</p> <p><strong>Page:</strong> ${result.page}</p> </div>`; // Add sections for (const section of template.sections) { html += this.generateHtmlSection(section, result, template, config); } html += `</body></html>`; return html; } /** * Generate markdown section */ generateMarkdownSection(section, result, template, config) { let content = `## ${this.formatSectionTitle(section)}\n\n`; switch (section) { case 'executive_summary': content += `${this.extractExecutiveSummary(result)}\n\n`; break; case 'overall_score': content += `**Score:** ${result.overallScore}/10\n\n`; break; case 'visual_hierarchy': content += `**Visual Design Score:** ${result.scores.visualDesign || 'N/A'}/10\n\n`; break; case 'accessibility': content += `**Accessibility Score:** ${result.scores.accessibility || 'N/A'}/10\n\n`; break; case 'usability': content += `**Usability Score:** ${result.scores.usability || 'N/A'}/10\n\n`; break; case 'critical_issues': content += this.formatIssues(result.criticalIssues, 'Critical'); break; case 'recommendations': content += this.formatRecommendations(result.recommendations); break; case 'technical_details': content += this.formatTechnicalDetails(result); break; default: content += `Data for ${section} not available.\n\n`; } return content; } /** * Generate HTML section */ generateHtmlSection(section, result, template, config) { let content = `<div class="section"><h2>${this.formatSectionTitle(section)}</h2>`; switch (section) { case 'overall_score': content += `<div class="score">${result.overallScore}/10</div>`; break; case 'critical_issues': result.criticalIssues.forEach(issue => { content += `<div class="issue"><strong>${issue.title}</strong><br>${issue.description}</div>`; }); break; case 'recommendations': result.recommendations.forEach(rec => { content += `<div class="recommendation"><strong>${rec.title}</strong><br>${rec.description}</div>`; }); break; default: content += `<p>Data for ${section} not available.</p>`; } content += `</div>`; return content; } /** * Extract section data for JSON format */ extractSectionData(section, result) { switch (section) { case 'overall_score': return result.overallScore; case 'top_issues': return result.criticalIssues.slice(0, 3).map(issue => ({ title: issue.title, severity: issue.severity, impact: issue.impact, })); case 'quick_wins': return result.recommendations.slice(0, 3).map(rec => ({ title: rec.title, effort: rec.effort, impact: rec.impact, })); default: return null; } } /** * Save report to file */ async saveReport(content, result, config) { const fileName = this.generateFileName(result, config); const filePath = path.join(config.outputDir, fileName); await fs.promises.writeFile(filePath, content, 'utf-8'); return filePath; } /** * Generate file name based on configuration */ generateFileName(result, config) { const template = config.fileNaming; const timestamp = this.formatTimestamp(result.timestamp, 'filename'); const extension = this.getFileExtension(config.format); return (template .replace('{timestamp}', timestamp) .replace('{component}', result.component || 'unknown') .replace('{page}', result.page || 'unknown') .replace('{id}', result.id) + extension); } /** * Get file extension for format */ getFileExtension(format) { switch (format) { case 'markdown': return '.md'; case 'json': return '.json'; case 'html': return '.html'; default: return '.txt'; } } /** * Format timestamp based on configuration */ formatTimestamp(timestamp, format) { const date = new Date(timestamp); switch (format) { case 'ISO': return date.toISOString(); case 'filename': return date.toISOString().replace(/[:.]/g, '-').slice(0, 19); case 'readable': return date.toLocaleString(); default: return timestamp; } } /** * Format section title */ formatSectionTitle(section) { return section .split('_') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Extract executive summary from result */ extractExecutiveSummary(result) { // Try to extract from raw analysis or generate from available data if (result.rawAnalysis && result.rawAnalysis.includes('EXECUTIVE SUMMARY')) { const match = result.rawAnalysis.match(/EXECUTIVE SUMMARY[:\n]+(.*?)(?=\n\n|\n[A-Z]|$)/s); if (match) return match[1].trim(); } // Generate summary from available data return (`UI analysis completed with an overall score of ${result.overallScore}/10. ` + `${result.criticalIssues.length} critical issues identified. ` + `${result.recommendations.length} recommendations provided for improvement.`); } /** * Format issues for display */ formatIssues(issues, severity) { if (!issues.length) return `No ${severity.toLowerCase()} issues found.\n\n`; let content = ''; issues.forEach((issue, index) => { content += `### ${index + 1}. ${issue.title}\n`; content += `**Severity:** ${issue.severity}\n`; content += `**Description:** ${issue.description}\n`; if (issue.fix) content += `**Fix:** ${issue.fix}\n`; content += '\n'; }); return content; } /** * Format recommendations for display */ formatRecommendations(recommendations) { if (!recommendations.length) return `No recommendations available.\n\n`; let content = ''; recommendations.forEach((rec, index) => { content += `### ${index + 1}. ${rec.title}\n`; content += `**Impact:** ${rec.impact}\n`; content += `**Effort:** ${rec.effort}\n`; content += `**Description:** ${rec.description}\n\n`; }); return content; } /** * Format technical details */ formatTechnicalDetails(result) { return (`**Analysis ID:** ${result.id}\n` + `**Model Used:** ${result.model}\n` + `**Tokens Used:** ${result.tokensUsed}\n` + `**Analysis Time:** ${result.analysisTime}ms\n` + `**Provider:** ${result.provider || 'Unknown'}\n\n`); } /** * Get template by name */ getTemplate(templateName) { const template = this.templates[templateName]; if (!template) { throw new Error(`Template '${templateName}' not found`); } return template; } /** * Ensure output directory exists */ ensureOutputDirectory() { try { if (!fs.existsSync(this.config.outputDir)) { fs.mkdirSync(this.config.outputDir, { recursive: true }); } } catch (error) { console.warn(`Failed to create report output directory: ${error}`); } } /** * Add custom template */ addTemplate(name, template) { this.templates[name] = template; } /** * Update configuration */ updateConfig(config) { this.config = { ...this.config, ...config }; this.ensureOutputDirectory(); } /** * Get current configuration */ getConfig() { return { ...this.config }; } } /** * šŸ” jpglens - API Compatibility Layer * Handles differences between OpenAI and Anthropic API formats * * @author Taha Bahrami (Kaito) * @license MIT */ /** * API Compatibility Handler */ class APICompatibilityHandler { config; constructor(config) { this.config = config; } /** * Detect API format based on provider and model */ detectAPIFormat() { if (this.config.messageFormat && this.config.messageFormat !== 'auto') { return this.config.messageFormat; } // Auto-detect based on provider and model if (this.config.provider === 'anthropic') { return 'anthropic'; } if (this.config.provider === 'openai') { return 'openai'; } // For OpenRouter, detect based on model name if (this.config.provider === 'openrouter') { if (this.config.model?.includes('anthropic/') || this.config.model?.includes('claude')) { return 'anthropic'; } if (this.config.model?.includes('openai/') || this.config.model?.includes('gpt')) { return 'openai'; } } // Default to OpenAI format for compatibility return 'openai'; } /** * Convert prompt and image to appropriate API format */ formatRequest(prompt, imageBase64, systemPrompt) { const apiFormat = this.detectAPIFormat(); if (apiFormat === 'anthropic') { return this.formatAnthropicRequest(prompt, imageBase64, systemPrompt); } else { return this.formatOpenAIRequest(prompt, imageBase64, systemPrompt); } } /** * Format request for OpenAI API */ formatOpenAIRequest(prompt, imageBase64, systemPrompt) { const messages = []; // Add system message if provided if (systemPrompt) { messages.push({ role: 'system', content: systemPrompt, }); } // Format user message with text and optional image const userContent = [{ type: 'text', text: prompt }]; if (imageBase64) { userContent.push({ type: 'image_url', image_url: { url: `data:image/png;bas