This commit is contained in:
Developers Digest
2025-08-08 09:04:33 -04:00
parent 0e883102ed
commit 1629e12079
73 changed files with 24502 additions and 0 deletions
+363
View File
@@ -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;
}
+252
View File
@@ -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)
`;
}
+510
View File
@@ -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}`;
}
}
+265
View File
@@ -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;
}
+268
View File
@@ -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})`
};
}
+21
View File
@@ -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';
+6
View File
@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}