jpglens
Version:
š Universal AI-Powered UI Testing - See your interfaces through the lens of intelligence
1,394 lines (1,367 loc) ⢠117 kB
JavaScript
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