UNPKG

design-agent

Version:

Universal AI Design Review Agent - CLI tool for scanning code for design drift

358 lines (293 loc) 9.52 kB
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; }