Skip to content

Commit be0391c

Browse files
authored
Add editing functionality BEN-1079 (#29)
### TL;DR Added code editing capabilities to the UI builder, allowing users to modify existing components through chat instructions. ### What changed? - Added `editApp` function in `lib/openai.ts` to handle code modifications - Created new prompt templates for editing in `lib/prompts.ts` - Extended the component schema to support editing instructions and existing files - Implemented file merging logic to combine updated files with existing ones - Updated the chat interface to send edit requests with current files - Added session storage to persist builder results - Enhanced logging for better debugging of edit operations ### How to test? 1. Generate a component using the UI builder 2. Use the chat interface to request changes (e.g., "Add a dark mode toggle") 3. Verify that the component updates according to your instructions 4. Check that the changes persist when refreshing the page (via sessionStorage) 5. Examine the console logs to verify the edit flow is working correctly ### Why make this change? This enhancement significantly improves the user experience by allowing iterative refinement of generated components without starting from scratch. Users can now have a conversation with the AI to progressively improve their components, making the tool more flexible and powerful for real-world development workflows.
2 parents 8315d10 + bdceb72 commit be0391c

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)