figma-restoration-mcp-vue-tools
Version:
Professional Figma Component Restoration Kit - MCP tools with snapDOM-powered high-quality screenshots, intelligent shadow detection, and advanced diff analysis for Vue component restoration. Features enhanced figma_compare with color-coded region analysi
821 lines (701 loc) • 26.8 kB
JavaScript
/**
* Metrics Calculator
* Computes comprehensive statistics and quality metrics
*/
import { BENCHMARK_CONFIG, ACCURACY_CATEGORIES } from './config.js';
import { logWithTimestamp } from './utils.js';
/**
* Interface for benchmark metrics
* @typedef {Object} BenchmarkMetrics
* @property {Object} summary - Summary statistics
* @property {Object} distribution - Accuracy distribution
* @property {RestorationData[]} components - Component data
* @property {Object} trends - Trend analysis
* @property {string} timestamp - Generation timestamp
*/
export class MetricsCalculator {
constructor() {
this.calculatedMetrics = null;
}
/**
* Calculate comprehensive metrics from restoration data
* @param {RestorationData[]} restorationData - Array of restoration data
* @returns {BenchmarkMetrics} Calculated metrics
*/
calculateMetrics(restorationData) {
logWithTimestamp(`Calculating metrics for ${restorationData.length} components`);
const metrics = {
summary: this.calculateSummaryStatistics(restorationData),
distribution: this.calculateAccuracyDistribution(restorationData),
components: this.sortComponentsByAccuracy(restorationData),
trends: this.calculateTrends(restorationData),
timestamp: new Date().toISOString(),
metadata: {
totalComponents: restorationData.length,
calculationTime: new Date().toISOString(),
version: '1.0.0'
}
};
this.calculatedMetrics = metrics;
logWithTimestamp(`Metrics calculation completed. Average accuracy: ${metrics.summary.averageAccuracy?.toFixed(1) || 'N/A'}%`);
return metrics;
}
/**
* Calculate summary statistics
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Summary statistics
*/
calculateSummaryStatistics(data) {
const totalComponents = data.length;
const completedComponents = data.filter(d => d.status === BENCHMARK_CONFIG.STATUS.COMPLETED).length;
// Get accuracy values for completed components
const accuracyValues = data
.filter(d => d.status === BENCHMARK_CONFIG.STATUS.COMPLETED && d.matchPercentage !== null)
.map(d => d.matchPercentage)
.sort((a, b) => a - b);
let averageAccuracy = null;
let medianAccuracy = null;
let bestAccuracy = null;
let worstAccuracy = null;
if (accuracyValues.length > 0) {
averageAccuracy = accuracyValues.reduce((sum, acc) => sum + acc, 0) / accuracyValues.length;
medianAccuracy = this.calculateMedian(accuracyValues);
bestAccuracy = Math.max(...accuracyValues);
worstAccuracy = Math.min(...accuracyValues);
}
const completionRate = totalComponents > 0 ? (completedComponents / totalComponents) * 100 : 0;
// Find best and worst performing components
const bestComponent = data.find(d => d.matchPercentage === bestAccuracy);
const worstComponent = data.find(d => d.matchPercentage === worstAccuracy);
// Calculate additional statistics
const pendingComponents = data.filter(d => d.status === BENCHMARK_CONFIG.STATUS.PENDING).length;
const failedComponents = data.filter(d => d.status === BENCHMARK_CONFIG.STATUS.FAILED).length;
const notTestedComponents = data.filter(d => d.status === BENCHMARK_CONFIG.STATUS.NOT_TESTED).length;
const totalAssets = data.reduce((sum, d) => sum + (d.assetCount || 0), 0);
const averageAssets = totalComponents > 0 ? totalAssets / totalComponents : 0;
return {
totalComponents,
completedComponents,
pendingComponents,
failedComponents,
notTestedComponents,
averageAccuracy,
medianAccuracy,
bestAccuracy,
worstAccuracy,
completionRate,
bestComponent: bestComponent ? {
name: bestComponent.componentName,
accuracy: bestComponent.matchPercentage
} : null,
worstComponent: worstComponent ? {
name: worstComponent.componentName,
accuracy: worstComponent.matchPercentage
} : null,
totalAssets,
averageAssets: Math.round(averageAssets * 10) / 10,
standardDeviation: this.calculateStandardDeviation(accuracyValues, averageAccuracy)
};
}
/**
* Calculate accuracy distribution
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Distribution statistics
*/
calculateAccuracyDistribution(data) {
const completedData = data.filter(d =>
d.status === BENCHMARK_CONFIG.STATUS.COMPLETED &&
d.matchPercentage !== null
);
const distribution = {
excellent: 0,
good: 0,
fair: 0,
poor: 0
};
const componentsByCategory = {
excellent: [],
good: [],
fair: [],
poor: []
};
for (const component of completedData) {
const category = this.categorizeAccuracy(component.matchPercentage);
distribution[category]++;
componentsByCategory[category].push({
name: component.componentName,
accuracy: component.matchPercentage
});
}
// Calculate percentages
const total = completedData.length;
const distributionPercentages = {};
for (const [category, count] of Object.entries(distribution)) {
distributionPercentages[category] = total > 0 ? (count / total) * 100 : 0;
}
return {
counts: distribution,
percentages: distributionPercentages,
componentsByCategory,
totalEvaluated: total
};
}
/**
* Categorize accuracy level
* @param {number} accuracy - Accuracy percentage
* @returns {string} Category name
*/
categorizeAccuracy(accuracy) {
if (accuracy >= BENCHMARK_CONFIG.ACCURACY_THRESHOLDS.EXCELLENT) {
return 'excellent';
} else if (accuracy >= BENCHMARK_CONFIG.ACCURACY_THRESHOLDS.GOOD) {
return 'good';
} else if (accuracy >= BENCHMARK_CONFIG.ACCURACY_THRESHOLDS.FAIR) {
return 'fair';
} else {
return 'poor';
}
}
/**
* Sort components by accuracy
* @param {RestorationData[]} data - Restoration data
* @returns {RestorationData[]} Sorted components
*/
sortComponentsByAccuracy(data) {
return [...data].sort((a, b) => {
// Completed components with accuracy first
if (a.matchPercentage !== null && b.matchPercentage !== null) {
return b.matchPercentage - a.matchPercentage;
}
// Components with accuracy come before those without
if (a.matchPercentage !== null && b.matchPercentage === null) {
return -1;
}
if (a.matchPercentage === null && b.matchPercentage !== null) {
return 1;
}
// Sort by status priority
const statusPriority = {
[BENCHMARK_CONFIG.STATUS.COMPLETED]: 1,
[BENCHMARK_CONFIG.STATUS.FAILED]: 2,
[BENCHMARK_CONFIG.STATUS.PENDING]: 3,
[BENCHMARK_CONFIG.STATUS.NOT_TESTED]: 4
};
const aPriority = statusPriority[a.status] || 5;
const bPriority = statusPriority[b.status] || 5;
if (aPriority !== bPriority) {
return aPriority - bPriority;
}
// Finally sort by name
return a.componentName.localeCompare(b.componentName);
});
}
/**
* Calculate trends and improvements
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Trend analysis
*/
calculateTrends(data) {
const trends = {
improvementRate: 0,
lastUpdated: null,
recentActivity: [],
statusTrends: {
improving: 0,
stable: 0,
declining: 0
}
};
// Find most recent activity
const componentsWithTimestamps = data
.filter(d => d.timestamp)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
if (componentsWithTimestamps.length > 0) {
trends.lastUpdated = componentsWithTimestamps[0].timestamp;
// Get recent activity (last 7 days)
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
trends.recentActivity = componentsWithTimestamps
.filter(d => new Date(d.timestamp) > sevenDaysAgo)
.map(d => ({
componentName: d.componentName,
timestamp: d.timestamp,
accuracy: d.matchPercentage,
status: d.status
}));
}
// Calculate improvement rate (placeholder - would need historical data)
const completedComponents = data.filter(d => d.status === BENCHMARK_CONFIG.STATUS.COMPLETED);
if (completedComponents.length > 0) {
const averageAccuracy = completedComponents.reduce((sum, d) => sum + (d.matchPercentage || 0), 0) / completedComponents.length;
// Simple heuristic: if average accuracy is above 95%, consider it improving
if (averageAccuracy >= 95) {
trends.improvementRate = 2.5; // Positive trend
} else if (averageAccuracy >= 90) {
trends.improvementRate = 1.0; // Slight improvement
} else {
trends.improvementRate = -0.5; // Needs improvement
}
}
return trends;
}
/**
* Calculate median value
* @param {number[]} values - Sorted array of values
* @returns {number} Median value
*/
calculateMedian(values) {
if (values.length === 0) return null;
const mid = Math.floor(values.length / 2);
if (values.length % 2 === 0) {
return (values[mid - 1] + values[mid]) / 2;
} else {
return values[mid];
}
}
/**
* Calculate standard deviation
* @param {number[]} values - Array of values
* @param {number} mean - Mean value
* @returns {number|null} Standard deviation
*/
calculateStandardDeviation(values, mean) {
if (!values.length || mean === null) return null;
const squaredDifferences = values.map(value => Math.pow(value - mean, 2));
const avgSquaredDiff = squaredDifferences.reduce((sum, diff) => sum + diff, 0) / values.length;
return Math.sqrt(avgSquaredDiff);
}
/**
* Get quality score based on metrics
* @param {BenchmarkMetrics} metrics - Calculated metrics
* @returns {Object} Quality score assessment
*/
getQualityScore(metrics = this.calculatedMetrics) {
if (!metrics) {
return {
score: 0,
grade: 'F',
description: 'No metrics available'
};
}
const { summary, distribution } = metrics;
// Calculate weighted score
let score = 0;
let maxScore = 0;
// Completion rate (30% weight)
if (summary.completionRate !== null) {
score += (summary.completionRate / 100) * 30;
}
maxScore += 30;
// Average accuracy (40% weight)
if (summary.averageAccuracy !== null) {
score += (summary.averageAccuracy / 100) * 40;
}
maxScore += 40;
// Distribution quality (30% weight)
const excellentRatio = distribution.percentages.excellent / 100;
const goodRatio = distribution.percentages.good / 100;
const distributionScore = (excellentRatio * 1.0) + (goodRatio * 0.8);
score += distributionScore * 30;
maxScore += 30;
const finalScore = maxScore > 0 ? (score / maxScore) * 100 : 0;
// Assign grade
let grade, description;
if (finalScore >= 90) {
grade = 'A';
description = 'Excellent restoration quality';
} else if (finalScore >= 80) {
grade = 'B';
description = 'Good restoration quality';
} else if (finalScore >= 70) {
grade = 'C';
description = 'Fair restoration quality';
} else if (finalScore >= 60) {
grade = 'D';
description = 'Poor restoration quality';
} else {
grade = 'F';
description = 'Needs significant improvement';
}
return {
score: Math.round(finalScore * 10) / 10,
grade,
description,
breakdown: {
completionRate: summary.completionRate,
averageAccuracy: summary.averageAccuracy,
distributionScore: distributionScore * 100
}
};
}
/**
* Generate recommendations based on metrics
* @param {BenchmarkMetrics} metrics - Calculated metrics
* @returns {string[]} Array of recommendations
*/
generateRecommendations(metrics = this.calculatedMetrics) {
if (!metrics) {
return ['Run benchmark analysis to get recommendations'];
}
const recommendations = [];
const { summary, distribution } = metrics;
// Completion rate recommendations
if (summary.completionRate < 50) {
recommendations.push('🎯 Focus on completing more component restorations - only ' +
Math.round(summary.completionRate) + '% are complete');
}
// Accuracy recommendations
if (summary.averageAccuracy !== null) {
if (summary.averageAccuracy < 90) {
recommendations.push('📈 Improve restoration accuracy - current average is ' +
summary.averageAccuracy.toFixed(1) + '%');
}
if (summary.standardDeviation > 10) {
recommendations.push('📊 Work on consistency - accuracy varies significantly across components');
}
}
// Distribution recommendations
if (distribution.counts.poor > 0) {
recommendations.push('🔧 Address ' + distribution.counts.poor +
' component(s) with poor accuracy (<90%)');
}
if (distribution.counts.excellent < distribution.totalEvaluated * 0.5) {
recommendations.push('⭐ Aim for more excellent results (≥98% accuracy) - currently ' +
distribution.counts.excellent + ' out of ' + distribution.totalEvaluated);
}
// Asset recommendations
if (summary.averageAssets > 10) {
recommendations.push('🖼️ Consider optimizing components with many assets (avg: ' +
summary.averageAssets + ' assets per component)');
}
// Status-specific recommendations
if (summary.failedComponents > 0) {
recommendations.push('❌ Investigate ' + summary.failedComponents +
' failed component(s) - they may have technical issues');
}
if (summary.pendingComponents > summary.completedComponents) {
recommendations.push('⏳ Many components are pending - consider batch processing to improve efficiency');
}
if (recommendations.length === 0) {
recommendations.push('🎉 Great work! Your restoration quality is excellent');
}
return recommendations;
}
/**
* Get calculated metrics
* @returns {BenchmarkMetrics|null} Calculated metrics or null
*/
getMetrics() {
return this.calculatedMetrics;
}
/**
* Clear calculated metrics
*/
clearMetrics() {
this.calculatedMetrics = null;
}
/**
* Create component ranking with detailed categorization
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Component ranking results
*/
createComponentRanking(data) {
const completedComponents = data.filter(d =>
d.status === BENCHMARK_CONFIG.STATUS.COMPLETED &&
d.matchPercentage !== null
);
const ranking = {
topPerformers: [],
needsImprovement: [],
categories: {
excellent: [],
good: [],
fair: [],
poor: []
},
statistics: {
totalRanked: completedComponents.length,
averageRank: 0,
medianAccuracy: 0
}
};
// Sort by accuracy descending
const sortedComponents = completedComponents
.sort((a, b) => b.matchPercentage - a.matchPercentage)
.map((component, index) => ({
...component,
rank: index + 1,
category: this.categorizeAccuracy(component.matchPercentage),
percentile: ((completedComponents.length - index) / completedComponents.length) * 100
}));
// Categorize components
for (const component of sortedComponents) {
ranking.categories[component.category].push(component);
}
// Identify top performers (top 25% or accuracy >= 95%)
const topThreshold = Math.max(3, Math.ceil(sortedComponents.length * 0.25));
ranking.topPerformers = sortedComponents
.slice(0, topThreshold)
.filter(c => c.matchPercentage >= 95);
// Identify components needing improvement (bottom 25% or accuracy < 90%)
const bottomThreshold = Math.max(3, Math.ceil(sortedComponents.length * 0.25));
ranking.needsImprovement = sortedComponents
.slice(-bottomThreshold)
.filter(c => c.matchPercentage < 90);
// Calculate statistics
if (sortedComponents.length > 0) {
const accuracies = sortedComponents.map(c => c.matchPercentage);
ranking.statistics.averageRank = sortedComponents.reduce((sum, c) => sum + c.rank, 0) / sortedComponents.length;
ranking.statistics.medianAccuracy = this.calculateMedian(accuracies);
}
return ranking;
}
/**
* Analyze accuracy trends over time
* @param {RestorationData[]} data - Restoration data with timestamps
* @returns {Object} Trend analysis results
*/
analyzeAccuracyTrends(data) {
const componentsWithTimestamps = data
.filter(d => d.timestamp && d.matchPercentage !== null)
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
if (componentsWithTimestamps.length < 2) {
return {
hasTrendData: false,
message: 'Insufficient data for trend analysis'
};
}
const trends = {
hasTrendData: true,
timespan: {
start: componentsWithTimestamps[0].timestamp,
end: componentsWithTimestamps[componentsWithTimestamps.length - 1].timestamp
},
overallTrend: 'stable',
trendStrength: 0,
periods: [],
improvements: [],
regressions: []
};
// Group by time periods (weekly)
const periods = this.groupByTimePeriods(componentsWithTimestamps, 'week');
trends.periods = periods;
// Calculate overall trend
const firstPeriodAvg = periods[0]?.averageAccuracy || 0;
const lastPeriodAvg = periods[periods.length - 1]?.averageAccuracy || 0;
const trendChange = lastPeriodAvg - firstPeriodAvg;
trends.trendStrength = Math.abs(trendChange);
if (trendChange > 2) {
trends.overallTrend = 'improving';
} else if (trendChange < -2) {
trends.overallTrend = 'declining';
} else {
trends.overallTrend = 'stable';
}
// Identify specific improvements and regressions
for (let i = 1; i < periods.length; i++) {
const current = periods[i];
const previous = periods[i - 1];
const change = current.averageAccuracy - previous.averageAccuracy;
if (change > 5) {
trends.improvements.push({
period: current.period,
change: change,
from: previous.averageAccuracy,
to: current.averageAccuracy
});
} else if (change < -5) {
trends.regressions.push({
period: current.period,
change: change,
from: previous.averageAccuracy,
to: current.averageAccuracy
});
}
}
return trends;
}
/**
* Group components by time periods
* @param {RestorationData[]} components - Components with timestamps
* @param {string} periodType - Period type ('day', 'week', 'month')
* @returns {Object[]} Grouped periods
*/
groupByTimePeriods(components, periodType = 'week') {
const periods = new Map();
for (const component of components) {
const date = new Date(component.timestamp);
let periodKey;
switch (periodType) {
case 'day':
periodKey = date.toISOString().split('T')[0];
break;
case 'week':
const weekStart = new Date(date);
weekStart.setDate(date.getDate() - date.getDay());
periodKey = weekStart.toISOString().split('T')[0];
break;
case 'month':
periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
break;
default:
periodKey = date.toISOString().split('T')[0];
}
if (!periods.has(periodKey)) {
periods.set(periodKey, {
period: periodKey,
components: [],
averageAccuracy: 0,
count: 0
});
}
periods.get(periodKey).components.push(component);
}
// Calculate averages for each period
const periodArray = Array.from(periods.values());
for (const period of periodArray) {
const accuracies = period.components.map(c => c.matchPercentage);
period.averageAccuracy = accuracies.reduce((sum, acc) => sum + acc, 0) / accuracies.length;
period.count = period.components.length;
}
return periodArray.sort((a, b) => a.period.localeCompare(b.period));
}
/**
* Generate detailed category analysis
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Category analysis
*/
generateCategoryAnalysis(data) {
const analysis = {
categories: {},
insights: [],
recommendations: []
};
const completedData = data.filter(d =>
d.status === BENCHMARK_CONFIG.STATUS.COMPLETED &&
d.matchPercentage !== null
);
// Analyze each category
for (const [categoryName, categoryInfo] of Object.entries(ACCURACY_CATEGORIES)) {
const categoryComponents = completedData.filter(d =>
this.categorizeAccuracy(d.matchPercentage) === categoryName
);
const categoryAnalysis = {
count: categoryComponents.length,
percentage: completedData.length > 0 ? (categoryComponents.length / completedData.length) * 100 : 0,
components: categoryComponents.map(c => ({
name: c.componentName,
accuracy: c.matchPercentage,
assetCount: c.assetCount
})),
averageAccuracy: categoryComponents.length > 0
? categoryComponents.reduce((sum, c) => sum + c.matchPercentage, 0) / categoryComponents.length
: 0,
averageAssets: categoryComponents.length > 0
? categoryComponents.reduce((sum, c) => sum + (c.assetCount || 0), 0) / categoryComponents.length
: 0,
label: categoryInfo.label,
color: categoryInfo.color,
threshold: categoryInfo.min
};
analysis.categories[categoryName] = categoryAnalysis;
// Generate insights for each category
if (categoryComponents.length > 0) {
if (categoryName === 'excellent' && categoryAnalysis.percentage > 50) {
analysis.insights.push(`🌟 ${categoryAnalysis.percentage.toFixed(1)}% of components achieve excellent quality (≥98%)`);
}
if (categoryName === 'poor' && categoryComponents.length > 0) {
analysis.insights.push(`⚠️ ${categoryComponents.length} component(s) need significant improvement (<90%)`);
}
if (categoryName === 'fair' && categoryAnalysis.percentage > 30) {
analysis.insights.push(`📈 ${categoryAnalysis.percentage.toFixed(1)}% of components are in fair range (90-95%) - potential for improvement`);
}
}
}
// Generate category-specific recommendations
const excellentCount = analysis.categories.excellent.count;
const poorCount = analysis.categories.poor.count;
const totalEvaluated = completedData.length;
if (excellentCount / totalEvaluated < 0.3) {
analysis.recommendations.push('🎯 Target: Increase excellent components to 30%+ of total');
}
if (poorCount > 0) {
analysis.recommendations.push(`🔧 Priority: Address ${poorCount} poor-performing component(s)`);
}
if (analysis.categories.fair.count > analysis.categories.good.count) {
analysis.recommendations.push('📊 Focus on moving fair components (90-95%) to good range (95-98%)');
}
return analysis;
}
/**
* Calculate component similarity and clustering
* @param {RestorationData[]} data - Restoration data
* @returns {Object} Similarity analysis
*/
calculateComponentSimilarity(data) {
const completedData = data.filter(d =>
d.status === BENCHMARK_CONFIG.STATUS.COMPLETED &&
d.matchPercentage !== null
);
const similarity = {
clusters: [],
outliers: [],
patterns: []
};
if (completedData.length < 3) {
return {
...similarity,
message: 'Insufficient data for similarity analysis'
};
}
// Simple clustering based on accuracy ranges
const clusters = {
highPerformers: completedData.filter(d => d.matchPercentage >= 95),
mediumPerformers: completedData.filter(d => d.matchPercentage >= 85 && d.matchPercentage < 95),
lowPerformers: completedData.filter(d => d.matchPercentage < 85)
};
// Identify outliers (components significantly different from their cluster)
for (const [clusterName, clusterData] of Object.entries(clusters)) {
if (clusterData.length > 0) {
const clusterAvg = clusterData.reduce((sum, d) => sum + d.matchPercentage, 0) / clusterData.length;
const clusterStdDev = this.calculateStandardDeviation(
clusterData.map(d => d.matchPercentage),
clusterAvg
);
similarity.clusters.push({
name: clusterName,
count: clusterData.length,
averageAccuracy: clusterAvg,
standardDeviation: clusterStdDev,
components: clusterData.map(d => d.componentName)
});
// Find outliers (more than 2 standard deviations from cluster mean)
if (clusterStdDev > 0) {
const outliers = clusterData.filter(d =>
Math.abs(d.matchPercentage - clusterAvg) > 2 * clusterStdDev
);
similarity.outliers.push(...outliers.map(d => ({
componentName: d.componentName,
accuracy: d.matchPercentage,
cluster: clusterName,
deviation: Math.abs(d.matchPercentage - clusterAvg)
})));
}
}
}
// Identify patterns
const assetCounts = completedData.map(d => d.assetCount || 0);
const avgAssets = assetCounts.reduce((sum, count) => sum + count, 0) / assetCounts.length;
const highAssetComponents = completedData.filter(d => (d.assetCount || 0) > avgAssets * 1.5);
const lowAssetComponents = completedData.filter(d => (d.assetCount || 0) < avgAssets * 0.5);
if (highAssetComponents.length > 0) {
const highAssetAvgAccuracy = highAssetComponents.reduce((sum, d) => sum + d.matchPercentage, 0) / highAssetComponents.length;
similarity.patterns.push({
type: 'asset_correlation',
description: `Components with many assets (${highAssetComponents.length}) have ${highAssetAvgAccuracy.toFixed(1)}% average accuracy`,
components: highAssetComponents.map(d => d.componentName)
});
}
return similarity;
}
}