363 lines
12 KiB
TypeScript
363 lines
12 KiB
TypeScript
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;
|
|
} |