Skip to content

Commit 533a0e5

Browse files
authored
Add chat interface BEN-1073 (#23)
### TL;DR Added a dedicated builder page with a chat interface for iterative UI component development. ### What changed? - Created a new `/builder` page that displays the generated UI component alongside a chat interface - Implemented a `ChatInterface` component that allows users to refine their UI through conversation - Modified the home page to redirect to the builder page after initial component generation - Updated the `PreviewCard` component to better fit the new layout with full-height display - Added session storage to persist generated components between pages - Implemented a "Start Over" button to reset the process ### How to test? 1. Navigate to the home page and enter a component description 2. After generation, you should be redirected to the new builder page 3. Verify the chat interface appears on the left with your initial prompt 4. Test the preview/code tabs on the right side 5. Try entering a new message in the chat interface 6. Test the "Start Over" button to ensure it clears the session and returns to home ### Why make this change? This change improves the user experience by separating the initial generation from the refinement process. Users can now iteratively improve their UI components through a chat-based interface while maintaining context of their original request. The split layout provides more screen space for both the preview and the conversation, creating a more intuitive workflow for building and refining UI components.
2 parents c59d8e5 + 46bab4a commit 533a0e5

File tree

5 files changed

+292
-58
lines changed

5 files changed

+292
-58
lines changed

app/builder/page.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
'use client';
2+
3+
import { useEffect, useState } from 'react';
4+
import { useRouter } from 'next/navigation';
5+
import { ChatInterface } from '@/components/ui-builder/chat-interface';
6+
import { PreviewCard } from '@/components/ui-builder/preview-card';
7+
import { benchifyFileSchema } from '@/lib/schemas';
8+
import { z } from 'zod';
9+
10+
export default function BuilderPage() {
11+
const router = useRouter();
12+
const [result, setResult] = useState<{
13+
repairedFiles?: z.infer<typeof benchifyFileSchema>;
14+
originalFiles?: z.infer<typeof benchifyFileSchema>;
15+
buildOutput: string;
16+
previewUrl: string;
17+
} | null>(null);
18+
const [initialPrompt, setInitialPrompt] = useState<string>('');
19+
20+
useEffect(() => {
21+
// Get the result from sessionStorage
22+
const storedResult = sessionStorage.getItem('builderResult');
23+
const storedPrompt = sessionStorage.getItem('initialPrompt');
24+
25+
if (storedResult && storedPrompt) {
26+
setResult(JSON.parse(storedResult));
27+
setInitialPrompt(storedPrompt);
28+
} else {
29+
// If no result found, redirect back to home
30+
router.push('/');
31+
}
32+
}, [router]);
33+
34+
if (!result) {
35+
return (
36+
<div className="min-h-screen flex items-center justify-center">
37+
<div className="text-center">
38+
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto mb-4"></div>
39+
<p className="text-muted-foreground">Loading your project...</p>
40+
</div>
41+
</div>
42+
);
43+
}
44+
45+
return (
46+
<div className="min-h-screen bg-background flex">
47+
{/* Chat Interface - Left Side */}
48+
<div className="w-1/4 min-w-80 border-r border-border bg-card flex-shrink-0">
49+
<ChatInterface
50+
initialPrompt={initialPrompt}
51+
onUpdateResult={setResult}
52+
/>
53+
</div>
54+
55+
{/* Preview Area - Right Side */}
56+
<div className="flex-1 p-4 overflow-hidden">
57+
<PreviewCard
58+
previewUrl={result.previewUrl}
59+
code={result.repairedFiles || result.originalFiles || []}
60+
/>
61+
</div>
62+
</div>
63+
);
64+
}

app/page.tsx

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
// app/page.tsx
22
'use client';
33

4-
import { useEffect, useState } from 'react';
54
import { PromptForm } from '@/components/ui-builder/prompt-form';
6-
import { PreviewCard } from '@/components/ui-builder/preview-card';
75
import { Card, CardContent } from '@/components/ui/card';
8-
import { benchifyFileSchema } from '@/lib/schemas';
9-
import { z } from 'zod';
106

117
export default function Home() {
12-
const [result, setResult] = useState<{
13-
repairedFiles?: z.infer<typeof benchifyFileSchema>;
14-
originalFiles?: z.infer<typeof benchifyFileSchema>;
15-
buildOutput: string;
16-
previewUrl: string;
17-
} | null>(null);
18-
19-
useEffect(() => {
20-
if (result) {
21-
console.log(result);
22-
}
23-
}, [result]);
24-
258
return (
269
<main className="min-h-screen flex flex-col items-center justify-center bg-background">
2710
<div className="w-full max-w-3xl mx-auto">
@@ -31,18 +14,11 @@ export default function Home() {
3114
<p className="text-lg text-muted-foreground mb-8 text-center">
3215
Generate UI components with AI and automatically repair issues with Benchify
3316
</p>
34-
{!result ? (
35-
<Card className="border-border bg-card">
36-
<CardContent className="pt-6">
37-
<PromptForm onGenerate={setResult} />
38-
</CardContent>
39-
</Card>
40-
) : (
41-
<PreviewCard
42-
previewUrl={result.previewUrl}
43-
code={result.repairedFiles || result.originalFiles || []}
44-
/>
45-
)}
17+
<Card className="border-border bg-card">
18+
<CardContent className="pt-6">
19+
<PromptForm />
20+
</CardContent>
21+
</Card>
4622
</div>
4723
</main>
4824
);
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
'use client';
2+
3+
import { useState, useRef, useEffect } from 'react';
4+
import { Send, RotateCcw } from 'lucide-react';
5+
import { Button } from '@/components/ui/button';
6+
import { Input } from '@/components/ui/input';
7+
import { ScrollArea } from '@/components/ui/scroll-area';
8+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
9+
import { benchifyFileSchema } from '@/lib/schemas';
10+
import { z } from 'zod';
11+
12+
interface Message {
13+
id: string;
14+
type: 'user' | 'assistant';
15+
content: string;
16+
timestamp: Date;
17+
}
18+
19+
interface ChatInterfaceProps {
20+
initialPrompt: string;
21+
onUpdateResult: (result: {
22+
repairedFiles?: z.infer<typeof benchifyFileSchema>;
23+
originalFiles?: z.infer<typeof benchifyFileSchema>;
24+
buildOutput: string;
25+
previewUrl: string;
26+
}) => void;
27+
}
28+
29+
export function ChatInterface({ initialPrompt, onUpdateResult }: ChatInterfaceProps) {
30+
const [messages, setMessages] = useState<Message[]>([
31+
{
32+
id: '1',
33+
type: 'user',
34+
content: initialPrompt,
35+
timestamp: new Date(),
36+
},
37+
{
38+
id: '2',
39+
type: 'assistant',
40+
content: "I've generated your UI component! You can see the preview on the right. What would you like to modify or improve?",
41+
timestamp: new Date(),
42+
},
43+
]);
44+
const [newMessage, setNewMessage] = useState('');
45+
const [isLoading, setIsLoading] = useState(false);
46+
const messagesEndRef = useRef<HTMLDivElement>(null);
47+
48+
const scrollToBottom = () => {
49+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
50+
};
51+
52+
useEffect(() => {
53+
scrollToBottom();
54+
}, [messages]);
55+
56+
const handleSendMessage = async () => {
57+
if (!newMessage.trim() || isLoading) return;
58+
59+
const userMessage: Message = {
60+
id: Date.now().toString(),
61+
type: 'user',
62+
content: newMessage,
63+
timestamp: new Date(),
64+
};
65+
66+
setMessages(prev => [...prev, userMessage]);
67+
setNewMessage('');
68+
setIsLoading(true);
69+
70+
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 = {
74+
id: (Date.now() + 1).toString(),
75+
type: 'assistant',
76+
content: "I understand your request. Let me update the component for you...",
77+
timestamp: new Date(),
78+
};
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
84+
85+
} catch (error) {
86+
console.error('Error processing message:', error);
87+
const errorMessage: Message = {
88+
id: (Date.now() + 1).toString(),
89+
type: 'assistant',
90+
content: "I'm sorry, there was an error processing your request. Please try again.",
91+
timestamp: new Date(),
92+
};
93+
setMessages(prev => [...prev, errorMessage]);
94+
} finally {
95+
setIsLoading(false);
96+
}
97+
};
98+
99+
const handleKeyPress = (e: React.KeyboardEvent) => {
100+
if (e.key === 'Enter' && !e.shiftKey) {
101+
e.preventDefault();
102+
handleSendMessage();
103+
}
104+
};
105+
106+
const handleStartOver = () => {
107+
// Clear session storage and redirect to home
108+
sessionStorage.removeItem('builderResult');
109+
sessionStorage.removeItem('initialPrompt');
110+
window.location.href = '/';
111+
};
112+
113+
return (
114+
<Card className="h-full flex flex-col border-0 rounded-none">
115+
<CardHeader className="pb-3 border-b">
116+
<div className="flex items-center justify-between">
117+
<CardTitle className="text-lg">Chat</CardTitle>
118+
<Button
119+
variant="ghost"
120+
size="sm"
121+
onClick={handleStartOver}
122+
className="h-8 px-2"
123+
>
124+
<RotateCcw className="h-4 w-4 mr-1" />
125+
Start Over
126+
</Button>
127+
</div>
128+
</CardHeader>
129+
130+
<CardContent className="flex-1 flex flex-col p-0">
131+
<ScrollArea className="flex-1 p-4">
132+
<div className="space-y-4">
133+
{messages.map((message) => (
134+
<div
135+
key={message.id}
136+
className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`}
137+
>
138+
<div
139+
className={`max-w-[80%] p-3 rounded-lg ${message.type === 'user'
140+
? 'bg-primary text-primary-foreground'
141+
: 'bg-muted text-muted-foreground'
142+
}`}
143+
>
144+
<p className="text-sm whitespace-pre-wrap">{message.content}</p>
145+
<p className="text-xs opacity-70 mt-1">
146+
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
147+
</p>
148+
</div>
149+
</div>
150+
))}
151+
{isLoading && (
152+
<div className="flex justify-start">
153+
<div className="bg-muted text-muted-foreground p-3 rounded-lg">
154+
<div className="flex items-center space-x-2">
155+
<div className="animate-pulse flex space-x-1">
156+
<div className="w-2 h-2 bg-current rounded-full"></div>
157+
<div className="w-2 h-2 bg-current rounded-full"></div>
158+
<div className="w-2 h-2 bg-current rounded-full"></div>
159+
</div>
160+
<span className="text-sm">Thinking...</span>
161+
</div>
162+
</div>
163+
</div>
164+
)}
165+
</div>
166+
<div ref={messagesEndRef} />
167+
</ScrollArea>
168+
169+
<div className="p-4 border-t">
170+
<div className="flex space-x-2">
171+
<Input
172+
value={newMessage}
173+
onChange={(e) => setNewMessage(e.target.value)}
174+
onKeyPress={handleKeyPress}
175+
placeholder="Describe your changes..."
176+
disabled={isLoading}
177+
className="flex-1"
178+
/>
179+
<Button
180+
onClick={handleSendMessage}
181+
disabled={!newMessage.trim() || isLoading}
182+
size="sm"
183+
>
184+
<Send className="h-4 w-4" />
185+
</Button>
186+
</div>
187+
</div>
188+
</CardContent>
189+
</Card>
190+
);
191+
}
Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { Card, CardContent } from "@/components/ui/card";
21
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
32
import { benchifyFileSchema } from "@/lib/schemas";
43
import { z } from "zod";
@@ -13,30 +12,30 @@ export function PreviewCard({ previewUrl, code }: PreviewCardProps) {
1312
const files = code || [];
1413

1514
return (
16-
<Card className="border-border bg-card">
17-
<CardContent>
18-
<Tabs defaultValue="preview" className="w-full">
19-
<TabsList className="mb-4">
20-
<TabsTrigger value="preview">Preview</TabsTrigger>
21-
<TabsTrigger value="code">Code</TabsTrigger>
22-
</TabsList>
15+
<div className="h-full">
16+
<Tabs defaultValue="preview" className="w-full h-full flex flex-col">
17+
<TabsList className="mb-4 self-start">
18+
<TabsTrigger value="preview">Preview</TabsTrigger>
19+
<TabsTrigger value="code">Code</TabsTrigger>
20+
</TabsList>
2321

24-
<TabsContent value="preview" className="w-full">
25-
<div className="w-full overflow-hidden rounded-md border">
26-
<iframe
27-
title="Preview"
28-
src={previewUrl}
29-
className="w-full h-[700px]"
30-
sandbox="allow-scripts allow-same-origin"
31-
/>
32-
</div>
33-
</TabsContent>
22+
<TabsContent value="preview" className="flex-1 m-0">
23+
<div className="w-full h-full overflow-hidden rounded-md border bg-background">
24+
<iframe
25+
title="Preview"
26+
src={previewUrl}
27+
className="w-full h-full"
28+
sandbox="allow-scripts allow-same-origin"
29+
/>
30+
</div>
31+
</TabsContent>
3432

35-
<TabsContent value="code" className="w-full h-[700px]">
33+
<TabsContent value="code" className="flex-1 m-0">
34+
<div className="h-full">
3635
<CodeEditor files={files} />
37-
</TabsContent>
38-
</Tabs>
39-
</CardContent>
40-
</Card>
36+
</div>
37+
</TabsContent>
38+
</Tabs>
39+
</div>
4140
);
4241
}

0 commit comments

Comments
 (0)