BOLT / lib /edit-intent-analyzer.ts
legends810's picture
Upload folder using huggingface_hub
d83e271 verified
import { FileManifest, EditType, EditIntent, IntentPattern } from '@/types/file-manifest';
/**
* Analyze user prompts to determine edit intent and select relevant files
*/
export function analyzeEditIntent(
prompt: string,
manifest: FileManifest
): EditIntent {
const lowerPrompt = prompt.toLowerCase();
// Define intent patterns
const patterns: IntentPattern[] = [
{
patterns: [
/update\s+(the\s+)?(\w+)\s+(component|section|page)/i,
/change\s+(the\s+)?(\w+)/i,
/modify\s+(the\s+)?(\w+)/i,
/edit\s+(the\s+)?(\w+)/i,
/fix\s+(the\s+)?(\w+)\s+(styling|style|css|layout)/i,
/remove\s+.*\s+(button|link|text|element|section)/i,
/delete\s+.*\s+(button|link|text|element|section)/i,
/hide\s+.*\s+(button|link|text|element|section)/i,
],
type: EditType.UPDATE_COMPONENT,
fileResolver: (p, m) => findComponentByContent(p, m),
},
{
patterns: [
/add\s+(a\s+)?new\s+(\w+)\s+(page|section|feature|component)/i,
/create\s+(a\s+)?(\w+)\s+(page|section|feature|component)/i,
/implement\s+(a\s+)?(\w+)\s+(page|section|feature)/i,
/build\s+(a\s+)?(\w+)\s+(page|section|feature)/i,
/add\s+(\w+)\s+to\s+(?:the\s+)?(\w+)/i,
/add\s+(?:a\s+)?(\w+)\s+(?:component|section)/i,
/include\s+(?:a\s+)?(\w+)/i,
],
type: EditType.ADD_FEATURE,
fileResolver: (p, m) => findFeatureInsertionPoints(p, m),
},
{
patterns: [
/fix\s+(the\s+)?(\w+|\w+\s+\w+)(?!\s+styling|\s+style)/i,
/resolve\s+(the\s+)?error/i,
/debug\s+(the\s+)?(\w+)/i,
/repair\s+(the\s+)?(\w+)/i,
],
type: EditType.FIX_ISSUE,
fileResolver: (p, m) => findProblemFiles(p, m),
},
{
patterns: [
/change\s+(the\s+)?(color|theme|style|styling|css)/i,
/update\s+(the\s+)?(color|theme|style|styling|css)/i,
/make\s+it\s+(dark|light|blue|red|green)/i,
/style\s+(the\s+)?(\w+)/i,
],
type: EditType.UPDATE_STYLE,
fileResolver: (p, m) => findStyleFiles(p, m),
},
{
patterns: [
/refactor\s+(the\s+)?(\w+)/i,
/clean\s+up\s+(the\s+)?code/i,
/reorganize\s+(the\s+)?(\w+)/i,
/optimize\s+(the\s+)?(\w+)/i,
],
type: EditType.REFACTOR,
fileResolver: (p, m) => findRefactorTargets(p, m),
},
{
patterns: [
/start\s+over/i,
/recreate\s+everything/i,
/rebuild\s+(the\s+)?app/i,
/new\s+app/i,
/from\s+scratch/i,
],
type: EditType.FULL_REBUILD,
fileResolver: (p, m) => [m.entryPoint],
},
{
patterns: [
/install\s+(\w+)/i,
/add\s+(\w+)\s+(package|library|dependency)/i,
/use\s+(\w+)\s+(library|framework)/i,
],
type: EditType.ADD_DEPENDENCY,
fileResolver: (p, m) => findPackageFiles(m),
},
];
// Find matching pattern
for (const pattern of patterns) {
for (const regex of pattern.patterns) {
if (regex.test(lowerPrompt)) {
const targetFiles = pattern.fileResolver(prompt, manifest);
const suggestedContext = getSuggestedContext(targetFiles, manifest);
return {
type: pattern.type,
targetFiles,
confidence: calculateConfidence(prompt, pattern, targetFiles),
description: generateDescription(pattern.type, prompt, targetFiles),
suggestedContext,
};
}
}
}
// Default to component update if no pattern matches
return {
type: EditType.UPDATE_COMPONENT,
targetFiles: [manifest.entryPoint],
confidence: 0.3,
description: 'General update to application',
suggestedContext: [],
};
}
/**
* Find component files mentioned in the prompt
*/
function findComponentFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
// Extract component names from prompt
const componentWords = extractComponentNames(prompt);
console.log('[findComponentFiles] Extracted words:', componentWords);
// First pass: Look for exact component file matches
for (const [path, fileInfo] of Object.entries(manifest.files)) {
// Check if file name or component name matches
const fileName = path.split('/').pop()?.toLowerCase() || '';
const componentName = fileInfo.componentInfo?.name.toLowerCase();
for (const word of componentWords) {
if (fileName.includes(word) || componentName?.includes(word)) {
console.log(`[findComponentFiles] Match found: word="${word}" in file="${path}"`);
files.push(path);
break; // Stop after first match to avoid duplicates
}
}
}
// If no specific component found, check for common UI elements
if (files.length === 0) {
const uiElements = ['header', 'footer', 'nav', 'sidebar', 'button', 'card', 'modal', 'hero', 'banner', 'about', 'services', 'features', 'testimonials', 'gallery', 'contact', 'team', 'pricing'];
for (const element of uiElements) {
if (lowerPrompt.includes(element)) {
// Look for exact component file matches first
for (const [path, fileInfo] of Object.entries(manifest.files)) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
// Only match if the filename contains the element name
if (fileName.includes(element + '.') || fileName === element) {
files.push(path);
console.log(`[findComponentFiles] UI element match: element="${element}" in file="${path}"`);
return files; // Return immediately with just this file
}
}
// If no exact file match, look for the element in file names (but be more selective)
for (const [path, fileInfo] of Object.entries(manifest.files)) {
const fileName = path.split('/').pop()?.toLowerCase() || '';
if (fileName.includes(element)) {
files.push(path);
console.log(`[findComponentFiles] UI element partial match: element="${element}" in file="${path}"`);
return files; // Return immediately with just this file
}
}
}
}
}
// Limit results to most specific matches
if (files.length > 1) {
console.log(`[findComponentFiles] Multiple files found (${files.length}), limiting to first match`);
return [files[0]]; // Only return the first match
}
return files.length > 0 ? files : [manifest.entryPoint];
}
/**
* Find where to add new features
*/
function findFeatureInsertionPoints(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
// For new pages, we need routing files and layout
if (lowerPrompt.includes('page')) {
// Find router configuration
for (const [path, fileInfo] of Object.entries(manifest.files)) {
if (fileInfo.content.includes('Route') ||
fileInfo.content.includes('createBrowserRouter') ||
path.includes('router') ||
path.includes('routes')) {
files.push(path);
}
}
// Also include App.jsx for navigation updates
if (manifest.entryPoint) {
files.push(manifest.entryPoint);
}
}
// For new components, find the most appropriate parent
if (lowerPrompt.includes('component') || lowerPrompt.includes('section') ||
lowerPrompt.includes('add') || lowerPrompt.includes('create')) {
// Extract where to add it (e.g., "to the footer", "in header")
const locationMatch = prompt.match(/(?:in|to|on|inside)\s+(?:the\s+)?(\w+)/i);
if (locationMatch) {
const location = locationMatch[1];
const parentFiles = findComponentFiles(location, manifest);
files.push(...parentFiles);
console.log(`[findFeatureInsertionPoints] Adding to ${location}, parent files:`, parentFiles);
} else {
// Look for component mentions in the prompt
const componentWords = extractComponentNames(prompt);
for (const word of componentWords) {
const relatedFiles = findComponentFiles(word, manifest);
if (relatedFiles.length > 0 && relatedFiles[0] !== manifest.entryPoint) {
files.push(...relatedFiles);
}
}
// Default to App.jsx if no specific location found
if (files.length === 0) {
files.push(manifest.entryPoint);
}
}
}
// Remove duplicates
return [...new Set(files)];
}
/**
* Find files that might have problems
*/
function findProblemFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
// Look for error keywords
if (prompt.match(/error|bug|issue|problem|broken|not working/i)) {
// Check recently modified files first
const sortedFiles = Object.entries(manifest.files)
.sort(([, a], [, b]) => b.lastModified - a.lastModified)
.slice(0, 5);
files.push(...sortedFiles.map(([path]) => path));
}
// Also check for specific component mentions
const componentFiles = findComponentFiles(prompt, manifest);
files.push(...componentFiles);
return [...new Set(files)];
}
/**
* Find style-related files
*/
function findStyleFiles(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
// Add all CSS files
files.push(...manifest.styleFiles);
// Check for Tailwind config
const tailwindConfig = Object.keys(manifest.files).find(
path => path.includes('tailwind.config')
);
if (tailwindConfig) files.push(tailwindConfig);
// If specific component styling mentioned, include that component
const componentFiles = findComponentFiles(prompt, manifest);
files.push(...componentFiles);
return files;
}
/**
* Find files to refactor
*/
function findRefactorTargets(prompt: string, manifest: FileManifest): string[] {
// Similar to findComponentFiles but broader
return findComponentFiles(prompt, manifest);
}
/**
* Find package configuration files
*/
function findPackageFiles(manifest: FileManifest): string[] {
const files: string[] = [];
for (const path of Object.keys(manifest.files)) {
if (path.endsWith('package.json') ||
path.endsWith('vite.config.js') ||
path.endsWith('tsconfig.json')) {
files.push(path);
}
}
return files;
}
/**
* Find component by searching for content mentioned in the prompt
*/
function findComponentByContent(prompt: string, manifest: FileManifest): string[] {
const files: string[] = [];
const lowerPrompt = prompt.toLowerCase();
console.log('[findComponentByContent] Searching for content in prompt:', prompt);
// Extract quoted strings or specific button/link text
const quotedStrings = prompt.match(/["']([^"']+)["']/g) || [];
const searchTerms: string[] = quotedStrings.map(s => s.replace(/["']/g, ''));
// Also look for specific terms after 'remove', 'delete', 'hide'
const actionMatch = prompt.match(/(?:remove|delete|hide)\s+(?:the\s+)?(.+?)(?:\s+button|\s+link|\s+text|\s+element|\s+section|$)/i);
if (actionMatch) {
searchTerms.push(actionMatch[1].trim());
}
console.log('[findComponentByContent] Search terms:', searchTerms);
// If we have search terms, look for them in file contents
if (searchTerms.length > 0) {
for (const [path, fileInfo] of Object.entries(manifest.files)) {
// Only search in component files
if (!path.includes('.jsx') && !path.includes('.tsx')) continue;
const content = fileInfo.content.toLowerCase();
for (const term of searchTerms) {
if (content.includes(term.toLowerCase())) {
console.log(`[findComponentByContent] Found "${term}" in ${path}`);
files.push(path);
break; // Only add file once
}
}
}
}
// If no files found by content, fall back to component name search
if (files.length === 0) {
console.log('[findComponentByContent] No files found by content, falling back to component name search');
return findComponentFiles(prompt, manifest);
}
// Return only the first match to avoid editing multiple files
return [files[0]];
}
/**
* Extract component names from prompt
*/
function extractComponentNames(prompt: string): string[] {
const words: string[] = [];
// Remove common words but keep component-related words
const cleanPrompt = prompt
.replace(/\b(the|a|an|in|on|to|from|update|change|modify|edit|fix|make)\b/gi, '')
.toLowerCase();
// Extract potential component names (words that might be components)
const matches = cleanPrompt.match(/\b\w+\b/g) || [];
for (const match of matches) {
if (match.length > 2) { // Skip very short words
words.push(match);
}
}
return words;
}
/**
* Get additional files for context - returns ALL files for comprehensive context
*/
function getSuggestedContext(
targetFiles: string[],
manifest: FileManifest
): string[] {
// Return all files except the ones being edited
const allFiles = Object.keys(manifest.files);
return allFiles.filter(file => !targetFiles.includes(file));
}
/**
* Resolve import path to actual file path
*/
function resolveImportPath(
fromFile: string,
importPath: string,
manifest: FileManifest
): string | null {
// Handle relative imports
if (importPath.startsWith('./') || importPath.startsWith('../')) {
const fromDir = fromFile.substring(0, fromFile.lastIndexOf('/'));
const resolved = resolveRelativePath(fromDir, importPath);
// Try with different extensions
const extensions = ['.jsx', '.js', '.tsx', '.ts', ''];
for (const ext of extensions) {
const fullPath = resolved + ext;
if (manifest.files[fullPath]) {
return fullPath;
}
// Try index file
const indexPath = resolved + '/index' + ext;
if (manifest.files[indexPath]) {
return indexPath;
}
}
}
// Handle @/ alias (common in Vite projects)
if (importPath.startsWith('@/')) {
const srcPath = importPath.replace('@/', '/home/user/app/src/');
return resolveImportPath(fromFile, srcPath, manifest);
}
return null;
}
/**
* Resolve relative path
*/
function resolveRelativePath(fromDir: string, relativePath: string): string {
const parts = fromDir.split('/');
const relParts = relativePath.split('/');
for (const part of relParts) {
if (part === '..') {
parts.pop();
} else if (part !== '.') {
parts.push(part);
}
}
return parts.join('/');
}
/**
* Calculate confidence score
*/
function calculateConfidence(
prompt: string,
pattern: IntentPattern,
targetFiles: string[]
): number {
let confidence = 0.5; // Base confidence
// Higher confidence if we found specific files
if (targetFiles.length > 0 && targetFiles[0] !== '') {
confidence += 0.2;
}
// Higher confidence for more specific prompts
if (prompt.split(' ').length > 5) {
confidence += 0.1;
}
// Higher confidence for exact pattern matches
for (const regex of pattern.patterns) {
if (regex.test(prompt)) {
confidence += 0.2;
break;
}
}
return Math.min(confidence, 1.0);
}
/**
* Generate human-readable description
*/
function generateDescription(
type: EditType,
prompt: string,
targetFiles: string[]
): string {
const fileNames = targetFiles.map(f => f.split('/').pop()).join(', ');
switch (type) {
case EditType.UPDATE_COMPONENT:
return `Updating component(s): ${fileNames}`;
case EditType.ADD_FEATURE:
return `Adding new feature to: ${fileNames}`;
case EditType.FIX_ISSUE:
return `Fixing issue in: ${fileNames}`;
case EditType.UPDATE_STYLE:
return `Updating styles in: ${fileNames}`;
case EditType.REFACTOR:
return `Refactoring: ${fileNames}`;
case EditType.FULL_REBUILD:
return 'Rebuilding entire application';
case EditType.ADD_DEPENDENCY:
return 'Adding new dependency';
default:
return `Editing: ${fileNames}`;
}
}