Skip to content

Commit bdceb72

Browse files
Juan Castañojuancastano
authored andcommitted
Add editing functionality
1 parent 8315d10 commit bdceb72

File tree

5 files changed

+196
-29
lines changed

5 files changed

+196
-29
lines changed

app/api/generate/route.ts

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
// app/api/generate/route.ts
22
import { NextRequest, NextResponse } from 'next/server';
3-
import { generateApp } from '@/lib/openai';
3+
import { generateApp, editApp } from '@/lib/openai';
44
import { createSandbox } from '@/lib/e2b';
55
import { componentSchema } from '@/lib/schemas';
66
import { Benchify } from 'benchify';
77
import { applyPatch } from 'diff';
8+
import { z } from 'zod';
9+
import { benchifyFileSchema } from '@/lib/schemas';
810

911
const benchify = new Benchify({
1012
apiKey: process.env.BENCHIFY_API_KEY,
@@ -32,12 +34,30 @@ export default App;`
3234
}
3335
];
3436

37+
// Extended schema to support editing
38+
const extendedComponentSchema = componentSchema.extend({
39+
existingFiles: benchifyFileSchema.optional(),
40+
editInstruction: z.string().optional(),
41+
});
42+
43+
// Helper function to merge updated files with existing files
44+
function mergeFiles(existingFiles: z.infer<typeof benchifyFileSchema>, updatedFiles: z.infer<typeof benchifyFileSchema>): z.infer<typeof benchifyFileSchema> {
45+
const existingMap = new Map(existingFiles.map(file => [file.path, file]));
46+
47+
// Apply updates
48+
updatedFiles.forEach(updatedFile => {
49+
existingMap.set(updatedFile.path, updatedFile);
50+
});
51+
52+
return Array.from(existingMap.values());
53+
}
54+
3555
export async function POST(request: NextRequest) {
3656
try {
3757
const body = await request.json();
3858

39-
// Validate the request using Zod schema
40-
const validationResult = componentSchema.safeParse(body);
59+
// Validate the request using extended schema
60+
const validationResult = extendedComponentSchema.safeParse(body);
4161

4262
if (!validationResult.success) {
4363
return NextResponse.json(
@@ -46,30 +66,53 @@ export async function POST(request: NextRequest) {
4666
);
4767
}
4868

49-
const { description } = validationResult.data;
69+
const { description, existingFiles, editInstruction } = validationResult.data;
70+
71+
console.log('API Request:', {
72+
isEdit: !!(existingFiles && editInstruction),
73+
filesCount: existingFiles?.length || 0,
74+
editInstruction: editInstruction || 'none',
75+
description: description || 'none'
76+
});
77+
78+
let filesToSandbox;
79+
80+
// Determine if this is an edit request or new generation
81+
if (existingFiles && editInstruction) {
82+
// Edit existing code
83+
console.log('Processing edit request...');
84+
console.log('Existing files:', existingFiles.map(f => ({ path: f.path, contentLength: f.content.length })));
85+
86+
const updatedFiles = await editApp(existingFiles, editInstruction);
87+
console.log('Updated files from AI:', updatedFiles.map(f => ({ path: f.path, contentLength: f.content.length })));
5088

51-
// Generate the Vue app using OpenAI
52-
let generatedFiles;
53-
if (debug) {
54-
generatedFiles = buggyCode;
89+
// Merge the updated files with the existing files
90+
filesToSandbox = mergeFiles(existingFiles, updatedFiles);
91+
console.log('Final merged files:', filesToSandbox.map(f => ({ path: f.path, contentLength: f.content.length })));
5592
} else {
56-
generatedFiles = await generateApp(description);
93+
// Generate new app
94+
console.log('Processing new generation request...');
95+
if (debug) {
96+
filesToSandbox = buggyCode;
97+
} else {
98+
filesToSandbox = await generateApp(description);
99+
}
57100
}
58101

59102
// Repair the generated code using Benchify's API
60103
// const { data } = await benchify.fixer.run({
61-
// files: generatedFiles.map(file => ({
104+
// files: filesToSandbox.map(file => ({
62105
// path: file.path,
63106
// contents: file.content
64107
// }))
65108
// });
66109

67-
let repairedFiles = generatedFiles;
110+
let repairedFiles = filesToSandbox;
68111
// if (data) {
69112
// const { success, diff } = data;
70113

71114
// if (success && diff) {
72-
// repairedFiles = generatedFiles.map(file => {
115+
// repairedFiles = filesToSandbox.map(file => {
73116
// const patchResult = applyPatch(file.content, diff);
74117
// return {
75118
// ...file,
@@ -83,12 +126,13 @@ export async function POST(request: NextRequest) {
83126

84127
// Return the results to the client
85128
return NextResponse.json({
86-
originalFiles: generatedFiles,
129+
originalFiles: filesToSandbox,
87130
repairedFiles: sandboxResult.allFiles, // Use the allFiles from the sandbox
88131
buildOutput: `Sandbox created with template: ${sandboxResult.template}, ID: ${sandboxResult.sbxId}`,
89132
previewUrl: sandboxResult.url,
90133
buildErrors: sandboxResult.buildErrors,
91134
hasErrors: sandboxResult.hasErrors,
135+
...(editInstruction && { editInstruction }),
92136
});
93137
} catch (error) {
94138
console.error('Error generating app:', error);

app/chat/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,12 @@ export default function ChatPage() {
100100
<div className="w-1/4 min-w-80 border-r border-border bg-card flex-shrink-0">
101101
<ChatInterface
102102
initialPrompt={initialPrompt}
103-
onUpdateResult={setResult}
103+
currentFiles={result?.repairedFiles || result?.originalFiles}
104+
onUpdateResult={(updatedResult) => {
105+
setResult(updatedResult);
106+
// Save updated result to sessionStorage
107+
sessionStorage.setItem('builderResult', JSON.stringify(updatedResult));
108+
}}
104109
/>
105110
</div>
106111

components/ui-builder/chat-interface.tsx

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,24 @@ interface Message {
1818

1919
interface ChatInterfaceProps {
2020
initialPrompt: string;
21+
currentFiles?: z.infer<typeof benchifyFileSchema>;
2122
onUpdateResult: (result: {
2223
repairedFiles?: z.infer<typeof benchifyFileSchema>;
2324
originalFiles?: z.infer<typeof benchifyFileSchema>;
2425
buildOutput: string;
2526
previewUrl: string;
27+
buildErrors?: Array<{
28+
type: 'typescript' | 'build' | 'runtime';
29+
message: string;
30+
file?: string;
31+
line?: number;
32+
column?: number;
33+
}>;
34+
hasErrors?: boolean;
2635
}) => void;
2736
}
2837

29-
export function ChatInterface({ initialPrompt, onUpdateResult }: ChatInterfaceProps) {
38+
export function ChatInterface({ initialPrompt, currentFiles, onUpdateResult }: ChatInterfaceProps) {
3039
const [messages, setMessages] = useState<Message[]>([
3140
{
3241
id: '1',
@@ -64,30 +73,63 @@ export function ChatInterface({ initialPrompt, onUpdateResult }: ChatInterfacePr
6473
};
6574

6675
setMessages(prev => [...prev, userMessage]);
76+
const editInstruction = newMessage;
6777
setNewMessage('');
6878
setIsLoading(true);
6979

7080
try {
71-
// Here you would call your API to process the new request
72-
// For now, we'll add a placeholder response
73-
const assistantMessage: Message = {
81+
// Add thinking message
82+
const thinkingMessage: Message = {
7483
id: (Date.now() + 1).toString(),
7584
type: 'assistant',
7685
content: "I understand your request. Let me update the component for you...",
7786
timestamp: new Date(),
7887
};
79-
80-
setMessages(prev => [...prev, assistantMessage]);
81-
82-
// TODO: Implement actual regeneration with the new prompt
83-
// This would call your generate API with the conversation context
88+
setMessages(prev => [...prev, thinkingMessage]);
89+
90+
// Call the edit API
91+
const response = await fetch('/api/generate', {
92+
method: 'POST',
93+
headers: {
94+
'Content-Type': 'application/json',
95+
},
96+
body: JSON.stringify({
97+
type: 'component',
98+
description: '', // Not used for edits
99+
existingFiles: currentFiles,
100+
editInstruction: editInstruction,
101+
}),
102+
});
103+
104+
console.log('Edit request:', {
105+
existingFiles: currentFiles,
106+
editInstruction: editInstruction,
107+
filesCount: currentFiles?.length || 0
108+
});
109+
110+
if (!response.ok) {
111+
throw new Error('Failed to edit component');
112+
}
113+
114+
const editResult = await response.json();
115+
console.log('Edit response:', editResult);
116+
117+
// Update the result in the parent component
118+
onUpdateResult(editResult);
119+
120+
// Update the thinking message to success
121+
setMessages(prev => prev.map(msg =>
122+
msg.id === thinkingMessage.id
123+
? { ...msg, content: `Great! I've updated the component according to your request: "${editInstruction}"` }
124+
: msg
125+
));
84126

85127
} catch (error) {
86-
console.error('Error processing message:', error);
128+
console.error('Error processing edit:', error);
87129
const errorMessage: Message = {
88130
id: (Date.now() + 1).toString(),
89131
type: 'assistant',
90-
content: "I'm sorry, there was an error processing your request. Please try again.",
132+
content: "I'm sorry, there was an error processing your edit request. Please try again.",
91133
timestamp: new Date(),
92134
};
93135
setMessages(prev => [...prev, errorMessage]);
@@ -137,8 +179,8 @@ export function ChatInterface({ initialPrompt, onUpdateResult }: ChatInterfacePr
137179
>
138180
<div
139181
className={`max-w-[80%] p-3 rounded-lg ${message.type === 'user'
140-
? 'bg-primary text-primary-foreground'
141-
: 'bg-muted text-muted-foreground'
182+
? 'bg-primary text-primary-foreground'
183+
: 'bg-muted text-muted-foreground'
142184
}`}
143185
>
144186
<p className="text-sm whitespace-pre-wrap">{message.content}</p>

lib/openai.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import { streamObject } from 'ai';
33
import { openai } from '@ai-sdk/openai';
44
import { z } from 'zod';
5-
import { REACT_APP_SYSTEM_PROMPT, REACT_APP_USER_PROMPT, TEMPERATURE, MODEL } from './prompts';
5+
import { REACT_APP_SYSTEM_PROMPT, REACT_APP_USER_PROMPT, TEMPERATURE, MODEL, EDIT_SYSTEM_PROMPT, createEditUserPrompt } from './prompts';
6+
import { benchifyFileSchema } from './schemas';
67

78
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
89

@@ -16,7 +17,9 @@ const fileSchema = z.object({
1617
content: z.string()
1718
});
1819

19-
// Generate a Vue application using AI SDK
20+
21+
22+
// Generate a new application using AI SDK
2023
export async function generateApp(
2124
description: string,
2225
): Promise<Array<{ path: string; content: string }>> {
@@ -50,4 +53,41 @@ export async function generateApp(
5053
console.error('Error generating app:', error);
5154
throw error;
5255
}
56+
}
57+
58+
// Edit existing application using AI SDK
59+
export async function editApp(
60+
existingFiles: z.infer<typeof benchifyFileSchema>,
61+
editInstruction: string,
62+
): Promise<Array<{ path: string; content: string }>> {
63+
console.log("Editing app with instruction: ", editInstruction);
64+
65+
try {
66+
const { elementStream } = streamObject({
67+
model: openai('gpt-4o-mini'),
68+
output: 'array',
69+
schema: fileSchema,
70+
temperature: 0.3, // Lower temperature for more consistent edits
71+
messages: [
72+
{ role: 'system', content: EDIT_SYSTEM_PROMPT },
73+
{ role: 'user', content: createEditUserPrompt(existingFiles, editInstruction) }
74+
]
75+
});
76+
77+
const updatedFiles = [];
78+
for await (const file of elementStream) {
79+
updatedFiles.push(file);
80+
}
81+
82+
if (!updatedFiles.length) {
83+
throw new Error("Failed to generate updated files - received empty response");
84+
}
85+
86+
console.log("Generated updated files: ", updatedFiles);
87+
88+
return updatedFiles;
89+
} catch (error) {
90+
console.error('Error editing app:', error);
91+
throw error;
92+
}
5393
}

lib/prompts.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
// lib/prompts.ts
2+
import { z } from 'zod';
3+
import { benchifyFileSchema } from "./schemas";
24

35
export const REACT_APP_SYSTEM_PROMPT = `You are an expert React, TypeScript, and Tailwind CSS developer.
46
You will be generating React application code based on the provided description.
@@ -47,5 +49,39 @@ export const REACT_APP_USER_PROMPT = (description: string) => `
4749
Create a React application with the following requirements:
4850
${description}`;
4951

52+
export const EDIT_SYSTEM_PROMPT = `You are an expert React/TypeScript developer. You will be given existing code files and an edit instruction. Your job is to modify the existing code according to the instruction while maintaining:
53+
54+
1. Code quality and best practices
55+
2. Existing functionality that shouldn't be changed
56+
3. Proper TypeScript types
57+
4. Modern React patterns
58+
5. Tailwind CSS for styling
59+
6. shadcn/ui components where appropriate
60+
61+
Return ONLY the files that need to be changed. Do not return unchanged files.
62+
63+
Rules:
64+
- Only return files that have actual changes
65+
- Make minimal changes necessary to fulfill the instruction
66+
- Keep all imports and dependencies that are still needed
67+
- Add new dependencies only if absolutely necessary
68+
- Use Tailwind classes for styling changes
69+
- Follow React best practices
70+
- Ensure all returned files are complete and valid`;
71+
72+
export function createEditUserPrompt(files: z.infer<typeof benchifyFileSchema>, editInstruction: string): string {
73+
const filesContent = files.map(file =>
74+
`### ${file.path}\n\`\`\`\n${file.content}\n\`\`\``
75+
).join('\n\n');
76+
77+
return `Here are the current files:
78+
79+
${filesContent}
80+
81+
Edit instruction: ${editInstruction}
82+
83+
Please update the code according to this instruction and return all files with their updated content.`;
84+
}
85+
5086
export const TEMPERATURE = 0.7;
5187
export const MODEL = 'gpt-4o';

0 commit comments

Comments
 (0)