design-agent
Version:
Universal AI Design Review Agent - CLI tool for scanning code for design drift
358 lines (293 loc) • 9.52 kB
JavaScript
export async function generateSuggestions(findings) {
const suggestions = [];
// Group findings by file for better organization
const findingsByFile = groupFindingsByFile(findings);
for (const [file, fileFindings] of Object.entries(findingsByFile)) {
const fileSuggestions = generateFileSuggestions(file, fileFindings);
if (fileSuggestions.length > 0) {
suggestions.push(...fileSuggestions);
}
}
return formatSuggestions(suggestions);
}
function groupFindingsByFile(findings) {
return findings.reduce((acc, finding) => {
const file = finding.file || 'unknown';
if (!acc[file]) {
acc[file] = [];
}
acc[file].push(finding);
return acc;
}, {});
}
function generateFileSuggestions(file, findings) {
const suggestions = [];
// Group findings by type for better suggestions
const findingsByType = groupFindingsByType(findings);
for (const [type, typeFindings] of Object.entries(findingsByType)) {
const typeSuggestions = generateTypeSuggestions(file, type, typeFindings);
suggestions.push(...typeSuggestions);
}
return suggestions;
}
function groupFindingsByType(findings) {
return findings.reduce((acc, finding) => {
const type = finding.kind || 'unknown';
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(finding);
return acc;
}, {});
}
function generateTypeSuggestions(file, type, findings) {
const suggestions = [];
switch (type) {
case 'tokenDriftColor':
suggestions.push(...generateColorTokenSuggestions(file, findings));
break;
case 'spacingRawPx':
suggestions.push(...generateSpacingTokenSuggestions(file, findings));
break;
case 'inlineStyle':
suggestions.push(...generateInlineStyleSuggestions(file, findings));
break;
case 'utilities':
suggestions.push(...generateUtilitySuggestions(file, findings));
break;
case 'typography':
suggestions.push(...generateTypographySuggestions(file, findings));
break;
case 'accessibility':
suggestions.push(...generateAccessibilitySuggestions(file, findings));
break;
default:
suggestions.push(...generateGenericSuggestions(file, findings));
}
return suggestions;
}
function generateColorTokenSuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
// Group by similar colors
const colorGroups = groupSimilarColors(findings);
for (const [color, colorFindings] of Object.entries(colorGroups)) {
const suggestedToken = suggestColorToken(color);
if (suggestedToken) {
suggestions.push({
file,
type: 'color-token',
description: `Replace hardcoded color ${color} with design token`,
changes: colorFindings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: suggestedToken,
reason: 'Use design token for consistency'
}))
});
}
}
return suggestions;
}
function generateSpacingTokenSuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
// Group by similar spacing values
const spacingGroups = groupSimilarSpacing(findings);
for (const [spacing, spacingFindings] of Object.entries(spacingGroups)) {
const suggestedToken = suggestSpacingToken(spacing);
if (suggestedToken) {
suggestions.push({
file,
type: 'spacing-token',
description: `Replace hardcoded spacing ${spacing} with design token`,
changes: spacingFindings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: suggestedToken,
reason: 'Use design token for consistent spacing'
}))
});
}
}
return suggestions;
}
function generateInlineStyleSuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
suggestions.push({
file,
type: 'inline-style',
description: 'Move inline styles to CSS classes or design tokens',
changes: findings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: 'Use CSS class or design token',
reason: 'Inline styles reduce maintainability and consistency'
}))
});
return suggestions;
}
function generateUtilitySuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
// Group by utility type
const utilityGroups = groupUtilityFindings(findings);
for (const [utilityType, utilityFindings] of Object.entries(utilityGroups)) {
suggestions.push({
file,
type: 'utility-optimization',
description: `Optimize ${utilityType} utility usage`,
changes: utilityFindings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: finding.autofix || 'Optimized utility classes',
reason: 'Remove redundant or conflicting utility classes'
}))
});
}
return suggestions;
}
function generateTypographySuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
suggestions.push({
file,
type: 'typography-token',
description: 'Use consistent typography tokens',
changes: findings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: 'Use typography design token',
reason: 'Ensure consistent typography across the application'
}))
});
return suggestions;
}
function generateAccessibilitySuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
for (const finding of findings) {
suggestions.push({
file,
type: 'accessibility',
description: finding.msg,
changes: [{
line: finding.line,
original: finding.value,
replacement: 'Add proper accessibility attributes',
reason: 'Improve accessibility compliance'
}]
});
}
return suggestions;
}
function generateGenericSuggestions(file, findings) {
const suggestions = [];
if (findings.length === 0) return suggestions;
suggestions.push({
file,
type: 'general',
description: 'Address design review findings',
changes: findings.map(finding => ({
line: finding.line,
original: finding.value,
replacement: finding.autofix || 'Apply suggested fix',
reason: finding.msg
}))
});
return suggestions;
}
function groupSimilarColors(findings) {
const groups = {};
for (const finding of findings) {
const color = finding.value;
if (!groups[color]) {
groups[color] = [];
}
groups[color].push(finding);
}
return groups;
}
function groupSimilarSpacing(findings) {
const groups = {};
for (const finding of findings) {
const spacing = finding.value;
if (!groups[spacing]) {
groups[spacing] = [];
}
groups[spacing].push(finding);
}
return groups;
}
function groupUtilityFindings(findings) {
const groups = {};
for (const finding of findings) {
const utilityType = extractUtilityType(finding.value);
if (!groups[utilityType]) {
groups[utilityType] = [];
}
groups[utilityType].push(finding);
}
return groups;
}
function extractUtilityType(value) {
if (value.includes('p-') || value.includes('m-')) return 'spacing';
if (value.includes('bg-') || value.includes('text-')) return 'color';
if (value.includes('w-') || value.includes('h-')) return 'sizing';
if (value.includes('flex') || value.includes('grid')) return 'layout';
return 'utility';
}
function suggestColorToken(color) {
// This is a simplified implementation
// In a real implementation, you'd use proper color distance calculations
const colorMap = {
'#000000': 'color-black',
'#ffffff': 'color-white',
'#ff0000': 'color-red-500',
'#00ff00': 'color-green-500',
'#0000ff': 'color-blue-500',
'#ffff00': 'color-yellow-500',
'#ff00ff': 'color-purple-500',
'#00ffff': 'color-cyan-500'
};
return colorMap[color.toLowerCase()] || 'color-primary';
}
function suggestSpacingToken(spacing) {
// This is a simplified implementation
const spacingMap = {
'4px': 'spacing-1',
'8px': 'spacing-2',
'12px': 'spacing-3',
'16px': 'spacing-4',
'20px': 'spacing-5',
'24px': 'spacing-6',
'32px': 'spacing-8',
'40px': 'spacing-10',
'48px': 'spacing-12',
'64px': 'spacing-16'
};
return spacingMap[spacing] || 'spacing-4';
}
function formatSuggestions(suggestions) {
if (suggestions.length === 0) {
return 'No suggestions available.';
}
let markdown = '# Suggested Changes\n\n';
for (const suggestion of suggestions) {
markdown += `## ${suggestion.file}\n\n`;
markdown += `**Type:** ${suggestion.type}\n\n`;
markdown += `**Description:** ${suggestion.description}\n\n`;
if (suggestion.changes && suggestion.changes.length > 0) {
markdown += `### Changes:\n\n`;
for (const change of suggestion.changes) {
markdown += `**Line ${change.line}:**\n`;
markdown += `- **Original:** \`${change.original}\`\n`;
markdown += `- **Replacement:** \`${change.replacement}\`\n`;
markdown += `- **Reason:** ${change.reason}\n\n`;
}
}
markdown += '---\n\n';
}
return markdown;
}