initial
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user