initial
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
import { FileManifest, EditIntent, EditType } from '@/types/file-manifest';
|
||||
import { analyzeEditIntent } from '@/lib/edit-intent-analyzer';
|
||||
import { getEditExamplesPrompt, getComponentPatternPrompt } from '@/lib/edit-examples';
|
||||
|
||||
export interface FileContext {
|
||||
primaryFiles: string[]; // Files to edit
|
||||
contextFiles: string[]; // Files to include for reference
|
||||
systemPrompt: string; // Enhanced prompt with file info
|
||||
editIntent: EditIntent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select files and build context based on user prompt
|
||||
*/
|
||||
export function selectFilesForEdit(
|
||||
userPrompt: string,
|
||||
manifest: FileManifest
|
||||
): FileContext {
|
||||
// Analyze the edit intent
|
||||
const editIntent = analyzeEditIntent(userPrompt, manifest);
|
||||
|
||||
// Get the files based on intent - only edit target files, but provide all others as context
|
||||
const primaryFiles = editIntent.targetFiles;
|
||||
const allFiles = Object.keys(manifest.files);
|
||||
let contextFiles = allFiles.filter(file => !primaryFiles.includes(file));
|
||||
|
||||
// ALWAYS include key files in context if they exist and aren't already primary files
|
||||
const keyFiles: string[] = [];
|
||||
|
||||
// App.jsx is most important - shows component structure
|
||||
const appFile = allFiles.find(f => f.endsWith('App.jsx') || f.endsWith('App.tsx'));
|
||||
if (appFile && !primaryFiles.includes(appFile)) {
|
||||
keyFiles.push(appFile);
|
||||
}
|
||||
|
||||
// Include design system files for style context
|
||||
const tailwindConfig = allFiles.find(f => f.endsWith('tailwind.config.js') || f.endsWith('tailwind.config.ts'));
|
||||
if (tailwindConfig && !primaryFiles.includes(tailwindConfig)) {
|
||||
keyFiles.push(tailwindConfig);
|
||||
}
|
||||
|
||||
const indexCss = allFiles.find(f => f.endsWith('index.css') || f.endsWith('globals.css'));
|
||||
if (indexCss && !primaryFiles.includes(indexCss)) {
|
||||
keyFiles.push(indexCss);
|
||||
}
|
||||
|
||||
// Include package.json to understand dependencies
|
||||
const packageJson = allFiles.find(f => f.endsWith('package.json'));
|
||||
if (packageJson && !primaryFiles.includes(packageJson)) {
|
||||
keyFiles.push(packageJson);
|
||||
}
|
||||
|
||||
// Put key files at the beginning of context for visibility
|
||||
contextFiles = [...keyFiles, ...contextFiles.filter(f => !keyFiles.includes(f))];
|
||||
|
||||
// Build enhanced system prompt
|
||||
const systemPrompt = buildSystemPrompt(
|
||||
userPrompt,
|
||||
editIntent,
|
||||
primaryFiles,
|
||||
contextFiles,
|
||||
manifest
|
||||
);
|
||||
|
||||
return {
|
||||
primaryFiles,
|
||||
contextFiles,
|
||||
systemPrompt,
|
||||
editIntent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an enhanced system prompt with file structure context
|
||||
*/
|
||||
function buildSystemPrompt(
|
||||
userPrompt: string,
|
||||
editIntent: EditIntent,
|
||||
primaryFiles: string[],
|
||||
contextFiles: string[],
|
||||
manifest: FileManifest
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Add edit examples first for better understanding
|
||||
if (editIntent.type !== EditType.FULL_REBUILD) {
|
||||
sections.push(getEditExamplesPrompt());
|
||||
}
|
||||
|
||||
// Add edit intent section
|
||||
sections.push(`## Edit Intent
|
||||
Type: ${editIntent.type}
|
||||
Description: ${editIntent.description}
|
||||
Confidence: ${(editIntent.confidence * 100).toFixed(0)}%
|
||||
|
||||
User Request: "${userPrompt}"`);
|
||||
|
||||
// Add file structure overview
|
||||
sections.push(buildFileStructureSection(manifest));
|
||||
|
||||
// Add component patterns
|
||||
const fileList = Object.keys(manifest.files).map(f => f.replace('/home/user/app/', '')).join('\n');
|
||||
sections.push(getComponentPatternPrompt(fileList));
|
||||
|
||||
// Add primary files section
|
||||
if (primaryFiles.length > 0) {
|
||||
sections.push(`## Files to Edit
|
||||
${primaryFiles.map(f => {
|
||||
const fileInfo = manifest.files[f];
|
||||
return `- ${f}${fileInfo?.componentInfo ? ` (${fileInfo.componentInfo.name} component)` : ''}`;
|
||||
}).join('\n')}`);
|
||||
}
|
||||
|
||||
// Add context files section
|
||||
if (contextFiles.length > 0) {
|
||||
sections.push(`## Context Files (for reference only)
|
||||
${contextFiles.map(f => {
|
||||
const fileInfo = manifest.files[f];
|
||||
return `- ${f}${fileInfo?.componentInfo ? ` (${fileInfo.componentInfo.name} component)` : ''}`;
|
||||
}).join('\n')}`);
|
||||
}
|
||||
|
||||
// Add specific instructions based on edit type
|
||||
sections.push(buildEditInstructions(editIntent.type));
|
||||
|
||||
// Add component relationships if relevant
|
||||
if (editIntent.type === EditType.UPDATE_COMPONENT ||
|
||||
editIntent.type === EditType.ADD_FEATURE) {
|
||||
sections.push(buildComponentRelationships(primaryFiles, manifest));
|
||||
}
|
||||
|
||||
return sections.join('\n\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Build file structure overview section
|
||||
*/
|
||||
function buildFileStructureSection(manifest: FileManifest): string {
|
||||
const allFiles = Object.entries(manifest.files)
|
||||
.map(([path]) => path.replace('/home/user/app/', ''))
|
||||
.filter(path => !path.includes('node_modules'))
|
||||
.sort();
|
||||
|
||||
const componentFiles = Object.entries(manifest.files)
|
||||
.filter(([, info]) => info.type === 'component' || info.type === 'page')
|
||||
.map(([path, info]) => ({
|
||||
path: path.replace('/home/user/app/', ''),
|
||||
name: info.componentInfo?.name || path.split('/').pop(),
|
||||
type: info.type,
|
||||
}));
|
||||
|
||||
return `## 🚨 EXISTING PROJECT FILES - DO NOT CREATE NEW FILES WITH SIMILAR NAMES 🚨
|
||||
|
||||
### ALL PROJECT FILES (${allFiles.length} files)
|
||||
\`\`\`
|
||||
${allFiles.join('\n')}
|
||||
\`\`\`
|
||||
|
||||
### Component Files (USE THESE EXACT NAMES)
|
||||
${componentFiles.map(f =>
|
||||
`- ${f.name} → ${f.path} (${f.type})`
|
||||
).join('\n')}
|
||||
|
||||
### CRITICAL: Component Relationships
|
||||
**ALWAYS CHECK App.jsx FIRST** to understand what components exist and how they're imported!
|
||||
|
||||
Common component overlaps to watch for:
|
||||
- "nav" or "navigation" → Often INSIDE Header.jsx, not a separate file
|
||||
- "menu" → Usually part of Header/Nav, not separate
|
||||
- "logo" → Typically in Header, not standalone
|
||||
|
||||
When user says "nav" or "navigation":
|
||||
1. First check if Header.jsx exists
|
||||
2. Look inside Header.jsx for navigation elements
|
||||
3. Only create Nav.jsx if navigation doesn't exist anywhere
|
||||
|
||||
Entry Point: ${manifest.entryPoint}
|
||||
|
||||
### Routes
|
||||
${manifest.routes.map(r =>
|
||||
`- ${r.path} → ${r.component.split('/').pop()}`
|
||||
).join('\n') || 'No routes detected'}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build edit-type specific instructions
|
||||
*/
|
||||
function buildEditInstructions(editType: EditType): string {
|
||||
const instructions: Record<EditType, string> = {
|
||||
[EditType.UPDATE_COMPONENT]: `## SURGICAL EDIT INSTRUCTIONS
|
||||
- You MUST preserve 99% of the original code
|
||||
- ONLY edit the specific component(s) mentioned
|
||||
- Make ONLY the minimal change requested
|
||||
- DO NOT rewrite or refactor unless explicitly asked
|
||||
- DO NOT remove any existing code unless explicitly asked
|
||||
- DO NOT change formatting or structure
|
||||
- Preserve all imports and exports
|
||||
- Maintain the existing code style
|
||||
- Return the COMPLETE file with the surgical change applied
|
||||
- Think of yourself as a surgeon making a precise incision, not an artist repainting`,
|
||||
|
||||
[EditType.ADD_FEATURE]: `## Instructions
|
||||
- Create new components in appropriate directories
|
||||
- IMPORTANT: Update parent components to import and use the new component
|
||||
- Update routing if adding new pages
|
||||
- Follow existing patterns and conventions
|
||||
- Add necessary styles to match existing design
|
||||
- Example workflow:
|
||||
1. Create NewComponent.jsx
|
||||
2. Import it in the parent: import NewComponent from './NewComponent'
|
||||
3. Use it in the parent's render: <NewComponent />`,
|
||||
|
||||
[EditType.FIX_ISSUE]: `## Instructions
|
||||
- Identify and fix the specific issue
|
||||
- Test the fix doesn't break other functionality
|
||||
- Preserve existing behavior except for the bug
|
||||
- Add error handling if needed`,
|
||||
|
||||
[EditType.UPDATE_STYLE]: `## SURGICAL STYLE EDIT INSTRUCTIONS
|
||||
- Change ONLY the specific style/class mentioned
|
||||
- If user says "change background to blue", change ONLY the background class
|
||||
- DO NOT touch any other styles, classes, or attributes
|
||||
- DO NOT refactor or "improve" the styling
|
||||
- DO NOT change the component structure
|
||||
- Preserve ALL other classes and styles exactly as they are
|
||||
- Return the COMPLETE file with only the specific style change`,
|
||||
|
||||
[EditType.REFACTOR]: `## Instructions
|
||||
- Improve code quality without changing functionality
|
||||
- Follow project conventions
|
||||
- Maintain all existing features
|
||||
- Improve readability and maintainability`,
|
||||
|
||||
[EditType.FULL_REBUILD]: `## Instructions
|
||||
- You may rebuild the entire application
|
||||
- Keep the same core functionality
|
||||
- Improve upon the existing design
|
||||
- Use modern best practices`,
|
||||
|
||||
[EditType.ADD_DEPENDENCY]: `## Instructions
|
||||
- Update package.json with new dependency
|
||||
- Add necessary import statements
|
||||
- Configure the dependency if needed
|
||||
- Update any build configuration`,
|
||||
};
|
||||
|
||||
return instructions[editType] || instructions[EditType.UPDATE_COMPONENT];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build component relationship information
|
||||
*/
|
||||
function buildComponentRelationships(
|
||||
files: string[],
|
||||
manifest: FileManifest
|
||||
): string {
|
||||
const relationships: string[] = ['## Component Relationships'];
|
||||
|
||||
for (const file of files) {
|
||||
const fileInfo = manifest.files[file];
|
||||
if (!fileInfo?.componentInfo) continue;
|
||||
|
||||
const componentName = fileInfo.componentInfo.name;
|
||||
const treeNode = manifest.componentTree[componentName];
|
||||
|
||||
if (treeNode) {
|
||||
relationships.push(`\n### ${componentName}`);
|
||||
|
||||
if (treeNode.imports.length > 0) {
|
||||
relationships.push(`Imports: ${treeNode.imports.join(', ')}`);
|
||||
}
|
||||
|
||||
if (treeNode.importedBy.length > 0) {
|
||||
relationships.push(`Used by: ${treeNode.importedBy.join(', ')}`);
|
||||
}
|
||||
|
||||
if (fileInfo.componentInfo.childComponents?.length) {
|
||||
relationships.push(`Renders: ${fileInfo.componentInfo.childComponents.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return relationships.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file content for selected files
|
||||
*/
|
||||
export async function getFileContents(
|
||||
files: string[],
|
||||
manifest: FileManifest
|
||||
): Promise<Record<string, string>> {
|
||||
const contents: Record<string, string> = {};
|
||||
|
||||
for (const file of files) {
|
||||
const fileInfo = manifest.files[file];
|
||||
if (fileInfo) {
|
||||
contents[file] = fileInfo.content;
|
||||
}
|
||||
}
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format files for AI context
|
||||
*/
|
||||
export function formatFilesForAI(
|
||||
primaryFiles: Record<string, string>,
|
||||
contextFiles: Record<string, string>
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// Add primary files
|
||||
sections.push('## Files to Edit (ONLY OUTPUT THESE FILES)\n');
|
||||
sections.push('🚨 You MUST ONLY generate the files listed below. Do NOT generate any other files! 🚨\n');
|
||||
sections.push('⚠️ CRITICAL: Return the COMPLETE file - NEVER truncate with "..." or skip any lines! ⚠️\n');
|
||||
sections.push('The file MUST include ALL imports, ALL functions, ALL JSX, and ALL closing tags.\n\n');
|
||||
for (const [path, content] of Object.entries(primaryFiles)) {
|
||||
sections.push(`### ${path}
|
||||
**IMPORTANT: This is the COMPLETE file. Your output must include EVERY line shown below, modified only where necessary.**
|
||||
\`\`\`${getFileExtension(path)}
|
||||
${content}
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
|
||||
// Add context files if any - but truncate large files
|
||||
if (Object.keys(contextFiles).length > 0) {
|
||||
sections.push('\n## Context Files (Reference Only - Do Not Edit)\n');
|
||||
for (const [path, content] of Object.entries(contextFiles)) {
|
||||
// Truncate very large context files to save tokens
|
||||
let truncatedContent = content;
|
||||
if (content.length > 2000) {
|
||||
truncatedContent = content.substring(0, 2000) + '\n// ... [truncated for context length]';
|
||||
}
|
||||
|
||||
sections.push(`### ${path}
|
||||
\`\`\`${getFileExtension(path)}
|
||||
${truncatedContent}
|
||||
\`\`\`
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file extension for syntax highlighting
|
||||
*/
|
||||
function getFileExtension(path: string): string {
|
||||
const ext = path.split('.').pop() || '';
|
||||
const mapping: Record<string, string> = {
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'css': 'css',
|
||||
'json': 'json',
|
||||
};
|
||||
return mapping[ext] || ext;
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Example-based prompts for teaching AI proper edit behavior
|
||||
*/
|
||||
|
||||
export const EDIT_EXAMPLES = `
|
||||
## Edit Strategy Examples
|
||||
|
||||
### Example 1: Update Header Color
|
||||
USER: "Make the header background black"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Identify Header component location
|
||||
2. Edit ONLY Header.jsx
|
||||
3. Change background color class/style
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Regenerating entire App.jsx
|
||||
- Creating new Header.jsx from scratch
|
||||
- Modifying unrelated files
|
||||
|
||||
EXPECTED OUTPUT:
|
||||
<file path="src/components/Header.jsx">
|
||||
// Only the Header component with updated background
|
||||
// Preserving all existing functionality
|
||||
</file>
|
||||
|
||||
### Example 2: Add New Page
|
||||
USER: "Add a videos page"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Create Videos.jsx component
|
||||
2. Update routing in App.jsx or Router component
|
||||
3. Add navigation link if needed
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Regenerating entire application
|
||||
- Recreating all existing pages
|
||||
|
||||
EXPECTED OUTPUT:
|
||||
<file path="src/components/Videos.jsx">
|
||||
// New Videos component
|
||||
</file>
|
||||
|
||||
<file path="src/App.jsx">
|
||||
// ONLY the routing update, preserving everything else
|
||||
</file>
|
||||
|
||||
### Example 3: Fix Styling Issue
|
||||
USER: "Fix the button styling on mobile"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Identify which component has the button
|
||||
2. Update only that component's Tailwind classes
|
||||
3. Add responsive modifiers (sm:, md:, etc)
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Regenerating all components
|
||||
- Creating new CSS files
|
||||
- Modifying global styles unnecessarily
|
||||
|
||||
### Example 4: Add Feature to Component
|
||||
USER: "Add a search bar to the header"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Modify Header.jsx to add search functionality
|
||||
2. Preserve all existing header content
|
||||
3. Integrate search seamlessly
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Creating Header.jsx from scratch
|
||||
- Losing existing navigation/branding
|
||||
|
||||
### Example 5: Add New Component
|
||||
USER: "Add a newsletter signup to the footer"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Create Newsletter.jsx component
|
||||
2. UPDATE Footer.jsx to import Newsletter
|
||||
3. Add <Newsletter /> in the appropriate place in Footer
|
||||
|
||||
EXPECTED OUTPUT:
|
||||
<file path="src/components/Newsletter.jsx">
|
||||
// New Newsletter component
|
||||
</file>
|
||||
|
||||
<file path="src/components/Footer.jsx">
|
||||
// Updated Footer with Newsletter import and usage
|
||||
import Newsletter from './Newsletter';
|
||||
// ... existing code ...
|
||||
// Add <Newsletter /> in the render
|
||||
</file>
|
||||
|
||||
### Example 6: Add External Library
|
||||
USER: "Add animations with framer-motion to the hero"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Import framer-motion in Hero.jsx
|
||||
2. Use motion components
|
||||
3. System will auto-install framer-motion
|
||||
|
||||
EXPECTED OUTPUT:
|
||||
<file path="src/components/Hero.jsx">
|
||||
import { motion } from 'framer-motion';
|
||||
// ... rest of Hero with motion animations
|
||||
</file>
|
||||
|
||||
### Example 7: Remove Element
|
||||
USER: "Remove start deploying button"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Search for "start deploying" in all component files
|
||||
2. Find it in Hero.jsx
|
||||
3. Edit ONLY Hero.jsx to remove that button
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Creating a new file
|
||||
- Editing multiple files
|
||||
- Redesigning the entire Hero
|
||||
|
||||
EXPECTED OUTPUT:
|
||||
<file path="src/components/Hero.jsx">
|
||||
// Hero component with "start deploying" button removed
|
||||
// All other content preserved
|
||||
</file>
|
||||
|
||||
### Example 8: Delete Section
|
||||
USER: "Delete the testimonials section"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Find which file contains testimonials
|
||||
2. Remove only that section from the file
|
||||
3. Keep all other content intact
|
||||
|
||||
INCORRECT APPROACH:
|
||||
- Deleting the entire file
|
||||
- Recreating the page without testimonials
|
||||
|
||||
### Example 9: Change a Single Style (CRITICAL EXAMPLE)
|
||||
USER: "update the hero to bg blue"
|
||||
|
||||
CORRECT APPROACH:
|
||||
1. Identify the Hero component file: 'src/components/Hero.jsx'.
|
||||
2. Locate the outermost 'div' or container element.
|
||||
3. Find the existing background color class (e.g., 'bg-gray-900').
|
||||
4. Replace ONLY that class with 'bg-blue-500'.
|
||||
5. Return the entire file, completely unchanged except for that single class modification.
|
||||
|
||||
**Original File Content (BEFORE):**
|
||||
<file path="src/components/Hero.jsx">
|
||||
import React from 'react';
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<div className="w-full bg-gray-900 text-white py-20 px-4">
|
||||
<h1 className="text-5xl font-bold">Welcome to the App</h1>
|
||||
<p className="mt-4 text-lg">This is the original hero section.</p>
|
||||
<button className="mt-6 px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
**Expected Output (AFTER):**
|
||||
<file path="src/components/Hero.jsx">
|
||||
import React from 'react';
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<div className="w-full bg-blue-500 text-white py-20 px-4">
|
||||
<h1 className="text-5xl font-bold">Welcome to the App</h1>
|
||||
<p className="mt-4 text-lg">This is the original hero section.</p>
|
||||
<button className="mt-6 px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||
Get Started
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</file>
|
||||
|
||||
NOTICE: Everything remains EXACTLY the same except 'bg-gray-900' → 'bg-blue-500'.
|
||||
- The button still has bg-blue-600 (unchanged)
|
||||
- All text, structure, imports are identical
|
||||
- No reformatting, no "improvements", no cleanup
|
||||
|
||||
## Key Principles
|
||||
|
||||
1. **Minimal Changes**: Only modify what's necessary
|
||||
2. **Preserve Functionality**: Keep all existing features
|
||||
3. **Respect Structure**: Follow existing patterns
|
||||
4. **Target Precision**: Edit specific files, not everything
|
||||
5. **Context Awareness**: Use imports/exports to understand relationships
|
||||
|
||||
## File Identification Patterns
|
||||
|
||||
- "header" → src/components/Header.jsx
|
||||
- "navigation" → src/components/Nav.jsx or Header.jsx
|
||||
- "footer" → src/components/Footer.jsx
|
||||
- "home page" → src/App.jsx or src/pages/Home.jsx
|
||||
- "styling" → Component files (Tailwind) or index.css
|
||||
- "routing" → App.jsx or Router component
|
||||
- "layout" → Layout components or App.jsx
|
||||
|
||||
## Edit Intent Classification
|
||||
|
||||
UPDATE_COMPONENT: Modify existing component
|
||||
- Keywords: update, change, modify, edit, fix
|
||||
- Action: Edit single file
|
||||
|
||||
ADD_FEATURE: Add new functionality
|
||||
- Keywords: add, create, implement, build
|
||||
- Action: Create new files + minimal edits
|
||||
|
||||
FIX_ISSUE: Resolve problems
|
||||
- Keywords: fix, resolve, debug, repair
|
||||
- Action: Targeted fixes
|
||||
|
||||
UPDATE_STYLE: Change appearance
|
||||
- Keywords: style, color, theme, design
|
||||
- Action: Update Tailwind classes
|
||||
|
||||
REFACTOR: Improve code quality
|
||||
- Keywords: refactor, clean, optimize
|
||||
- Action: Restructure without changing behavior
|
||||
`;
|
||||
|
||||
export function getEditExamplesPrompt(): string {
|
||||
return EDIT_EXAMPLES;
|
||||
}
|
||||
|
||||
export function getComponentPatternPrompt(fileStructure: string): string {
|
||||
return `
|
||||
## Current Project Structure
|
||||
|
||||
${fileStructure}
|
||||
|
||||
## Component Naming Patterns
|
||||
Based on your file structure, here are the patterns to follow:
|
||||
|
||||
1. Component files are in: src/components/
|
||||
2. Page components might be in: src/pages/ or src/components/
|
||||
3. Utility functions are in: src/utils/ or src/lib/
|
||||
4. Styles use Tailwind classes inline
|
||||
5. Main app entry is: src/App.jsx
|
||||
|
||||
When the user mentions a component by name, look for:
|
||||
- Exact matches first (Header → Header.jsx)
|
||||
- Partial matches (nav → Navigation.jsx, NavBar.jsx)
|
||||
- Semantic matches (top bar → Header.jsx)
|
||||
`;
|
||||
}
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
import { FileInfo, ImportInfo, ComponentInfo } from '@/types/file-manifest';
|
||||
|
||||
/**
|
||||
* Parse a JavaScript/JSX file to extract imports, exports, and component info
|
||||
*/
|
||||
export function parseJavaScriptFile(content: string, filePath: string): Partial<FileInfo> {
|
||||
const imports = extractImports(content);
|
||||
const exports = extractExports(content);
|
||||
const componentInfo = extractComponentInfo(content, filePath);
|
||||
const fileType = determineFileType(filePath, content);
|
||||
|
||||
return {
|
||||
imports,
|
||||
exports,
|
||||
componentInfo,
|
||||
type: fileType,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract import statements from file content
|
||||
*/
|
||||
function extractImports(content: string): ImportInfo[] {
|
||||
const imports: ImportInfo[] = [];
|
||||
|
||||
// Match import statements
|
||||
const importRegex = /import\s+(?:(.+?)\s+from\s+)?['"](.+?)['"]/g;
|
||||
const matches = content.matchAll(importRegex);
|
||||
|
||||
for (const match of matches) {
|
||||
const [, importClause, source] = match;
|
||||
const importInfo: ImportInfo = {
|
||||
source,
|
||||
imports: [],
|
||||
isLocal: source.startsWith('./') || source.startsWith('../') || source.startsWith('@/'),
|
||||
};
|
||||
|
||||
if (importClause) {
|
||||
// Handle default import
|
||||
const defaultMatch = importClause.match(/^(\w+)(?:,|$)/);
|
||||
if (defaultMatch) {
|
||||
importInfo.defaultImport = defaultMatch[1];
|
||||
}
|
||||
|
||||
// Handle named imports
|
||||
const namedMatch = importClause.match(/\{([^}]+)\}/);
|
||||
if (namedMatch) {
|
||||
importInfo.imports = namedMatch[1]
|
||||
.split(',')
|
||||
.map(imp => imp.trim())
|
||||
.map(imp => imp.split(/\s+as\s+/)[0].trim());
|
||||
}
|
||||
}
|
||||
|
||||
imports.push(importInfo);
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract export statements from file content
|
||||
*/
|
||||
function extractExports(content: string): string[] {
|
||||
const exports: string[] = [];
|
||||
|
||||
// Match default export
|
||||
if (/export\s+default\s+/m.test(content)) {
|
||||
// Try to find the name of the default export
|
||||
const defaultExportMatch = content.match(/export\s+default\s+(?:function\s+)?(\w+)/);
|
||||
if (defaultExportMatch) {
|
||||
exports.push(`default:${defaultExportMatch[1]}`);
|
||||
} else {
|
||||
exports.push('default');
|
||||
}
|
||||
}
|
||||
|
||||
// Match named exports
|
||||
const namedExportRegex = /export\s+(?:const|let|var|function|class)\s+(\w+)/g;
|
||||
const namedMatches = content.matchAll(namedExportRegex);
|
||||
|
||||
for (const match of namedMatches) {
|
||||
exports.push(match[1]);
|
||||
}
|
||||
|
||||
// Match export { ... } statements
|
||||
const exportBlockRegex = /export\s+\{([^}]+)\}/g;
|
||||
const blockMatches = content.matchAll(exportBlockRegex);
|
||||
|
||||
for (const match of blockMatches) {
|
||||
const names = match[1]
|
||||
.split(',')
|
||||
.map(exp => exp.trim())
|
||||
.map(exp => exp.split(/\s+as\s+/)[0].trim());
|
||||
exports.push(...names);
|
||||
}
|
||||
|
||||
return exports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract React component information
|
||||
*/
|
||||
function extractComponentInfo(content: string, filePath: string): ComponentInfo | undefined {
|
||||
// Check if this is likely a React component
|
||||
const hasJSX = /<[A-Z]\w*|<[a-z]+\s+[^>]*\/?>/.test(content);
|
||||
if (!hasJSX && !content.includes('React')) return undefined;
|
||||
|
||||
// Try to find component name
|
||||
let componentName = '';
|
||||
|
||||
// Check for function component
|
||||
const funcComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?function\s+([A-Z]\w*)\s*\(/);
|
||||
if (funcComponentMatch) {
|
||||
componentName = funcComponentMatch[1];
|
||||
} else {
|
||||
// Check for arrow function component
|
||||
const arrowComponentMatch = content.match(/(?:export\s+)?(?:default\s+)?(?:const|let)\s+([A-Z]\w*)\s*=\s*(?:\([^)]*\)|[^=])*=>/);
|
||||
if (arrowComponentMatch) {
|
||||
componentName = arrowComponentMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// If no component name found, try to get from filename
|
||||
if (!componentName) {
|
||||
const fileName = filePath.split('/').pop()?.replace(/\.(jsx?|tsx?)$/, '');
|
||||
if (fileName && /^[A-Z]/.test(fileName)) {
|
||||
componentName = fileName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!componentName) return undefined;
|
||||
|
||||
// Extract hooks used
|
||||
const hooks: string[] = [];
|
||||
const hookRegex = /use[A-Z]\w*/g;
|
||||
const hookMatches = content.matchAll(hookRegex);
|
||||
for (const match of hookMatches) {
|
||||
if (!hooks.includes(match[0])) {
|
||||
hooks.push(match[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if component has state
|
||||
const hasState = hooks.includes('useState') || hooks.includes('useReducer');
|
||||
|
||||
// Extract child components (rough approximation)
|
||||
const childComponents: string[] = [];
|
||||
const componentRegex = /<([A-Z]\w*)[^>]*(?:\/?>|>)/g;
|
||||
const componentMatches = content.matchAll(componentRegex);
|
||||
|
||||
for (const match of componentMatches) {
|
||||
const comp = match[1];
|
||||
if (!childComponents.includes(comp) && comp !== componentName) {
|
||||
childComponents.push(comp);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: componentName,
|
||||
hooks,
|
||||
hasState,
|
||||
childComponents,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine file type based on path and content
|
||||
*/
|
||||
function determineFileType(
|
||||
filePath: string,
|
||||
content: string
|
||||
): FileInfo['type'] {
|
||||
const fileName = filePath.split('/').pop()?.toLowerCase() || '';
|
||||
const dirPath = filePath.toLowerCase();
|
||||
|
||||
// Style files
|
||||
if (fileName.endsWith('.css')) return 'style';
|
||||
|
||||
// Config files
|
||||
if (fileName.includes('config') ||
|
||||
fileName === 'vite.config.js' ||
|
||||
fileName === 'tailwind.config.js' ||
|
||||
fileName === 'postcss.config.js') {
|
||||
return 'config';
|
||||
}
|
||||
|
||||
// Hook files
|
||||
if (dirPath.includes('/hooks/') || fileName.startsWith('use')) {
|
||||
return 'hook';
|
||||
}
|
||||
|
||||
// Context files
|
||||
if (dirPath.includes('/context/') || fileName.includes('context')) {
|
||||
return 'context';
|
||||
}
|
||||
|
||||
// Layout components
|
||||
if (fileName.includes('layout') || content.includes('children')) {
|
||||
return 'layout';
|
||||
}
|
||||
|
||||
// Page components (in pages directory or have routing)
|
||||
if (dirPath.includes('/pages/') ||
|
||||
content.includes('useRouter') ||
|
||||
content.includes('useParams')) {
|
||||
return 'page';
|
||||
}
|
||||
|
||||
// Utility files
|
||||
if (dirPath.includes('/utils/') ||
|
||||
dirPath.includes('/lib/') ||
|
||||
!content.includes('export default')) {
|
||||
return 'utility';
|
||||
}
|
||||
|
||||
// Default to component
|
||||
return 'component';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build component dependency tree
|
||||
*/
|
||||
export function buildComponentTree(files: Record<string, FileInfo>) {
|
||||
const tree: Record<string, {
|
||||
file: string;
|
||||
imports: string[];
|
||||
importedBy: string[];
|
||||
type: 'page' | 'layout' | 'component';
|
||||
}> = {};
|
||||
|
||||
// First pass: collect all components
|
||||
for (const [path, fileInfo] of Object.entries(files)) {
|
||||
if (fileInfo.componentInfo) {
|
||||
const componentName = fileInfo.componentInfo.name;
|
||||
tree[componentName] = {
|
||||
file: path,
|
||||
imports: [],
|
||||
importedBy: [],
|
||||
type: fileInfo.type === 'page' ? 'page' :
|
||||
fileInfo.type === 'layout' ? 'layout' : 'component',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: build relationships
|
||||
for (const [path, fileInfo] of Object.entries(files)) {
|
||||
if (fileInfo.componentInfo && fileInfo.imports) {
|
||||
const componentName = fileInfo.componentInfo.name;
|
||||
|
||||
// Find imported components
|
||||
for (const imp of fileInfo.imports) {
|
||||
if (imp.isLocal && imp.defaultImport) {
|
||||
// Check if this import is a component we know about
|
||||
if (tree[imp.defaultImport]) {
|
||||
tree[componentName].imports.push(imp.defaultImport);
|
||||
tree[imp.defaultImport].importedBy.push(componentName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* Agentic file search executor
|
||||
* Executes search plans to find exact code locations before editing
|
||||
*/
|
||||
|
||||
export interface SearchResult {
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
lineContent: string;
|
||||
matchedTerm?: string;
|
||||
matchedPattern?: string;
|
||||
contextBefore: string[];
|
||||
contextAfter: string[];
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export interface SearchPlan {
|
||||
editType: string;
|
||||
reasoning: string;
|
||||
searchTerms: string[];
|
||||
regexPatterns?: string[];
|
||||
fileTypesToSearch?: string[];
|
||||
expectedMatches?: number;
|
||||
fallbackSearch?: {
|
||||
terms: string[];
|
||||
patterns?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SearchExecutionResult {
|
||||
success: boolean;
|
||||
results: SearchResult[];
|
||||
filesSearched: number;
|
||||
executionTime: number;
|
||||
usedFallback: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a search plan against the codebase
|
||||
*/
|
||||
export function executeSearchPlan(
|
||||
searchPlan: SearchPlan,
|
||||
files: Record<string, string>
|
||||
): SearchExecutionResult {
|
||||
const startTime = Date.now();
|
||||
const results: SearchResult[] = [];
|
||||
let filesSearched = 0;
|
||||
let usedFallback = false;
|
||||
|
||||
const {
|
||||
searchTerms = [],
|
||||
regexPatterns = [],
|
||||
fileTypesToSearch = ['.jsx', '.tsx', '.js', '.ts'],
|
||||
fallbackSearch
|
||||
} = searchPlan;
|
||||
|
||||
// Helper function to perform search
|
||||
const performSearch = (terms: string[], patterns?: string[]): SearchResult[] => {
|
||||
const searchResults: SearchResult[] = [];
|
||||
|
||||
for (const [filePath, content] of Object.entries(files)) {
|
||||
// Skip files that don't match the desired extensions
|
||||
const shouldSearch = fileTypesToSearch.some(ext => filePath.endsWith(ext));
|
||||
if (!shouldSearch) continue;
|
||||
|
||||
filesSearched++;
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
let matched = false;
|
||||
let matchedTerm: string | undefined;
|
||||
let matchedPattern: string | undefined;
|
||||
|
||||
// Check simple search terms (case-insensitive)
|
||||
for (const term of terms) {
|
||||
if (line.toLowerCase().includes(term.toLowerCase())) {
|
||||
matched = true;
|
||||
matchedTerm = term;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check regex patterns if no term match
|
||||
if (!matched && patterns) {
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
const regex = new RegExp(pattern, 'i');
|
||||
if (regex.test(line)) {
|
||||
matched = true;
|
||||
matchedPattern = pattern;
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[file-search] Invalid regex pattern: ${pattern}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matched) {
|
||||
// Get context lines (3 before, 3 after)
|
||||
const contextBefore = lines.slice(Math.max(0, i - 3), i);
|
||||
const contextAfter = lines.slice(i + 1, Math.min(lines.length, i + 4));
|
||||
|
||||
// Determine confidence based on match type and context
|
||||
let confidence: 'high' | 'medium' | 'low' = 'medium';
|
||||
|
||||
// High confidence if it's an exact match or in a component definition
|
||||
if (matchedTerm && line.includes(matchedTerm)) {
|
||||
confidence = 'high';
|
||||
} else if (line.includes('function') || line.includes('export') || line.includes('return')) {
|
||||
confidence = 'high';
|
||||
} else if (matchedPattern) {
|
||||
confidence = 'medium';
|
||||
}
|
||||
|
||||
searchResults.push({
|
||||
filePath,
|
||||
lineNumber: i + 1,
|
||||
lineContent: line.trim(),
|
||||
matchedTerm,
|
||||
matchedPattern,
|
||||
contextBefore,
|
||||
contextAfter,
|
||||
confidence
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchResults;
|
||||
};
|
||||
|
||||
// Execute primary search
|
||||
results.push(...performSearch(searchTerms, regexPatterns));
|
||||
|
||||
// If no results and we have a fallback, try it
|
||||
if (results.length === 0 && fallbackSearch) {
|
||||
console.log('[file-search] No results from primary search, trying fallback...');
|
||||
usedFallback = true;
|
||||
results.push(...performSearch(
|
||||
fallbackSearch.terms,
|
||||
fallbackSearch.patterns
|
||||
));
|
||||
}
|
||||
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// Sort results by confidence
|
||||
results.sort((a, b) => {
|
||||
const confidenceOrder = { high: 3, medium: 2, low: 1 };
|
||||
return confidenceOrder[b.confidence] - confidenceOrder[a.confidence];
|
||||
});
|
||||
|
||||
return {
|
||||
success: results.length > 0,
|
||||
results,
|
||||
filesSearched,
|
||||
executionTime,
|
||||
usedFallback,
|
||||
error: results.length === 0 ? 'No matches found for search terms' : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format search results for AI consumption
|
||||
*/
|
||||
export function formatSearchResultsForAI(results: SearchResult[]): string {
|
||||
if (results.length === 0) {
|
||||
return 'No search results found.';
|
||||
}
|
||||
|
||||
const sections: string[] = [];
|
||||
|
||||
sections.push('🔍 SEARCH RESULTS - EXACT LOCATIONS FOUND:\n');
|
||||
|
||||
// Group by file for better readability
|
||||
const resultsByFile = new Map<string, SearchResult[]>();
|
||||
for (const result of results) {
|
||||
if (!resultsByFile.has(result.filePath)) {
|
||||
resultsByFile.set(result.filePath, []);
|
||||
}
|
||||
resultsByFile.get(result.filePath)!.push(result);
|
||||
}
|
||||
|
||||
for (const [filePath, fileResults] of resultsByFile) {
|
||||
sections.push(`\n📄 FILE: ${filePath}`);
|
||||
|
||||
for (const result of fileResults) {
|
||||
sections.push(`\n 📍 Line ${result.lineNumber} (${result.confidence} confidence)`);
|
||||
|
||||
if (result.matchedTerm) {
|
||||
sections.push(` Matched: "${result.matchedTerm}"`);
|
||||
} else if (result.matchedPattern) {
|
||||
sections.push(` Pattern: ${result.matchedPattern}`);
|
||||
}
|
||||
|
||||
sections.push(` Code: ${result.lineContent}`);
|
||||
|
||||
if (result.contextBefore.length > 0 || result.contextAfter.length > 0) {
|
||||
sections.push(` Context:`);
|
||||
for (const line of result.contextBefore) {
|
||||
sections.push(` ${line}`);
|
||||
}
|
||||
sections.push(` → ${result.lineContent}`);
|
||||
for (const line of result.contextAfter) {
|
||||
sections.push(` ${line}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sections.push('\n\n🎯 RECOMMENDED ACTION:');
|
||||
|
||||
// Recommend the highest confidence result
|
||||
const bestResult = results[0];
|
||||
sections.push(`Edit ${bestResult.filePath} at line ${bestResult.lineNumber}`);
|
||||
|
||||
return sections.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the best file to edit based on search results
|
||||
*/
|
||||
export function selectTargetFile(
|
||||
results: SearchResult[],
|
||||
editType: string
|
||||
): { filePath: string; lineNumber: number; reason: string } | null {
|
||||
if (results.length === 0) return null;
|
||||
|
||||
// For style updates, prefer components over CSS files
|
||||
if (editType === 'UPDATE_STYLE') {
|
||||
const componentResult = results.find(r =>
|
||||
r.filePath.endsWith('.jsx') || r.filePath.endsWith('.tsx')
|
||||
);
|
||||
if (componentResult) {
|
||||
return {
|
||||
filePath: componentResult.filePath,
|
||||
lineNumber: componentResult.lineNumber,
|
||||
reason: 'Found component with style to update'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// For remove operations, find the component that renders the element
|
||||
if (editType === 'REMOVE_ELEMENT') {
|
||||
const renderResult = results.find(r =>
|
||||
r.lineContent.includes('return') ||
|
||||
r.lineContent.includes('<')
|
||||
);
|
||||
if (renderResult) {
|
||||
return {
|
||||
filePath: renderResult.filePath,
|
||||
lineNumber: renderResult.lineNumber,
|
||||
reason: 'Found element to remove in render output'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Default: use highest confidence result
|
||||
const best = results[0];
|
||||
return {
|
||||
filePath: best.filePath,
|
||||
lineNumber: best.lineNumber,
|
||||
reason: `Highest confidence match (${best.confidence})`
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Centralized icon exports to avoid Turbopack chunk loading issues
|
||||
// This file pre-loads all icons to prevent dynamic import errors
|
||||
|
||||
export {
|
||||
FiFile,
|
||||
FiChevronRight,
|
||||
FiChevronDown,
|
||||
FiGithub
|
||||
} from 'react-icons/fi';
|
||||
|
||||
export {
|
||||
BsFolderFill,
|
||||
BsFolder2Open
|
||||
} from 'react-icons/bs';
|
||||
|
||||
export {
|
||||
SiJavascript,
|
||||
SiReact,
|
||||
SiCss3,
|
||||
SiJson
|
||||
} from 'react-icons/si';
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
Reference in New Issue
Block a user