Skip to content

Commit f3012fe

Browse files
author
Juan Castaño
committed
Add loading state
1 parent 4412949 commit f3012fe

File tree

4 files changed

+236
-116
lines changed

4 files changed

+236
-116
lines changed

app/builder/page.tsx

Lines changed: 0 additions & 64 deletions
This file was deleted.

app/chat/page.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
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+
type GenerationResult = {
11+
repairedFiles?: z.infer<typeof benchifyFileSchema>;
12+
originalFiles?: z.infer<typeof benchifyFileSchema>;
13+
buildOutput: string;
14+
previewUrl: string;
15+
};
16+
17+
export default function ChatPage() {
18+
const router = useRouter();
19+
const [result, setResult] = useState<GenerationResult | null>(null);
20+
const [initialPrompt, setInitialPrompt] = useState<string>('');
21+
const [isGenerating, setIsGenerating] = useState(false);
22+
23+
useEffect(() => {
24+
// Get the prompt and check if we have an existing result
25+
const storedPrompt = sessionStorage.getItem('initialPrompt');
26+
const storedResult = sessionStorage.getItem('builderResult');
27+
28+
if (!storedPrompt) {
29+
// If no prompt found, redirect back to home
30+
router.push('/');
31+
return;
32+
}
33+
34+
setInitialPrompt(storedPrompt);
35+
36+
if (storedResult) {
37+
// If we have a stored result, use it
38+
setResult(JSON.parse(storedResult));
39+
} else {
40+
// If no result, start the generation process
41+
setIsGenerating(true);
42+
startGeneration(storedPrompt);
43+
}
44+
}, [router]);
45+
46+
const startGeneration = async (prompt: string) => {
47+
try {
48+
const response = await fetch('/api/generate', {
49+
method: 'POST',
50+
headers: {
51+
'Content-Type': 'application/json',
52+
},
53+
body: JSON.stringify({
54+
type: 'component',
55+
description: prompt,
56+
preview: true,
57+
}),
58+
});
59+
60+
if (!response.ok) {
61+
throw new Error('Failed to generate component');
62+
}
63+
64+
const generationResult = await response.json();
65+
66+
// Store the result
67+
sessionStorage.setItem('builderResult', JSON.stringify(generationResult));
68+
setResult(generationResult);
69+
setIsGenerating(false);
70+
} catch (error) {
71+
console.error('Error generating component:', error);
72+
setIsGenerating(false);
73+
// Handle error state
74+
}
75+
};
76+
77+
// Show loading spinner if we don't have prompt yet
78+
if (!initialPrompt) {
79+
return (
80+
<div className="min-h-screen flex items-center justify-center">
81+
<div className="text-center">
82+
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto mb-4"></div>
83+
<p className="text-muted-foreground">Loading...</p>
84+
</div>
85+
</div>
86+
);
87+
}
88+
89+
return (
90+
<div className="min-h-screen bg-background flex">
91+
{/* Chat Interface - Left Side */}
92+
<div className="w-1/4 min-w-80 border-r border-border bg-card flex-shrink-0">
93+
<ChatInterface
94+
initialPrompt={initialPrompt}
95+
onUpdateResult={setResult}
96+
/>
97+
</div>
98+
99+
{/* Preview Area - Right Side */}
100+
<div className="flex-1 p-4 overflow-hidden">
101+
<PreviewCard
102+
previewUrl={result?.previewUrl}
103+
code={result?.repairedFiles || result?.originalFiles || []}
104+
isGenerating={isGenerating}
105+
prompt={initialPrompt}
106+
/>
107+
</div>
108+
</div>
109+
);
110+
}

components/ui-builder/preview-card.tsx

Lines changed: 120 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,70 @@
1+
import { useEffect, useState } from 'react';
2+
import { CheckCircle, Circle, Loader2 } from 'lucide-react';
13
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
4+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
import { benchifyFileSchema } from "@/lib/schemas";
36
import { z } from "zod";
47
import { CodeEditor } from "./code-editor";
58

9+
interface Step {
10+
id: string;
11+
label: string;
12+
description: string;
13+
}
14+
15+
const GENERATION_STEPS: Step[] = [
16+
{
17+
id: 'analyzing',
18+
label: 'Analyzing Request',
19+
description: 'Understanding your requirements and design specifications',
20+
},
21+
{
22+
id: 'generating',
23+
label: 'Generating Code',
24+
description: 'Creating UI components with AI assistance',
25+
},
26+
{
27+
id: 'building',
28+
label: 'Building Project',
29+
description: 'Setting up development environment and dependencies',
30+
},
31+
{
32+
id: 'deploying',
33+
label: 'Creating Preview',
34+
description: 'Deploying your project for live preview',
35+
},
36+
];
37+
638
interface PreviewCardProps {
7-
previewUrl: string;
39+
previewUrl?: string;
840
code: z.infer<typeof benchifyFileSchema>;
41+
isGenerating?: boolean;
42+
prompt?: string;
943
}
1044

11-
export function PreviewCard({ previewUrl, code }: PreviewCardProps) {
45+
export function PreviewCard({ previewUrl, code, isGenerating = false, prompt }: PreviewCardProps) {
1246
const files = code || [];
47+
const [currentStep, setCurrentStep] = useState(0);
48+
49+
useEffect(() => {
50+
if (isGenerating) {
51+
// Reset to first step when generation starts
52+
setCurrentStep(0);
53+
54+
// Automatically advance through steps for visual feedback
55+
const stepTimer = setInterval(() => {
56+
setCurrentStep(prev => {
57+
// Cycle through steps, but don't go past the last one
58+
if (prev < GENERATION_STEPS.length - 1) {
59+
return prev + 1;
60+
}
61+
return prev;
62+
});
63+
}, 2000); // Advance every 2 seconds
64+
65+
return () => clearInterval(stepTimer);
66+
}
67+
}, [isGenerating]);
1368

1469
return (
1570
<div className="h-full">
@@ -20,14 +75,69 @@ export function PreviewCard({ previewUrl, code }: PreviewCardProps) {
2075
</TabsList>
2176

2277
<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>
78+
{isGenerating && prompt ? (
79+
// Show loading progress inside the preview tab
80+
<div className="w-full h-full flex items-center justify-center rounded-md border bg-background">
81+
<Card className="w-full max-w-md mx-auto">
82+
<CardHeader>
83+
<CardTitle>Building Your UI</CardTitle>
84+
<p className="text-sm text-muted-foreground mt-2">
85+
"{prompt.substring(0, 100)}{prompt.length > 100 ? '...' : ''}"
86+
</p>
87+
</CardHeader>
88+
<CardContent className="space-y-4">
89+
{GENERATION_STEPS.map((step, index) => {
90+
const isCompleted = index < currentStep;
91+
const isCurrent = index === currentStep;
92+
93+
return (
94+
<div key={step.id} className="flex items-start space-x-3">
95+
<div className="flex-shrink-0 mt-1">
96+
{isCompleted ? (
97+
<CheckCircle className="h-5 w-5 text-green-500" />
98+
) : isCurrent ? (
99+
<Loader2 className="h-5 w-5 text-primary animate-spin" />
100+
) : (
101+
<Circle className="h-5 w-5 text-muted-foreground" />
102+
)}
103+
</div>
104+
<div className="flex-1 min-w-0">
105+
<p className={`text-sm font-medium ${isCompleted ? 'text-green-700 dark:text-green-400' :
106+
isCurrent ? 'text-primary' :
107+
'text-muted-foreground'
108+
}`}>
109+
{step.label}
110+
</p>
111+
<p className={`text-xs ${isCompleted || isCurrent ? 'text-muted-foreground' : 'text-muted-foreground/60'
112+
}`}>
113+
{step.description}
114+
</p>
115+
</div>
116+
</div>
117+
);
118+
})}
119+
</CardContent>
120+
</Card>
121+
</div>
122+
) : previewUrl ? (
123+
// Show the actual preview iframe when ready
124+
<div className="w-full h-full overflow-hidden rounded-md border bg-background">
125+
<iframe
126+
title="Preview"
127+
src={previewUrl}
128+
className="w-full h-full"
129+
sandbox="allow-scripts allow-same-origin"
130+
/>
131+
</div>
132+
) : (
133+
// Show loading spinner if no preview URL yet
134+
<div className="w-full h-full flex items-center justify-center rounded-md border bg-background">
135+
<div className="text-center">
136+
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto mb-4"></div>
137+
<p className="text-muted-foreground">Loading your project...</p>
138+
</div>
139+
</div>
140+
)}
31141
</TabsContent>
32142

33143
<TabsContent value="code" className="flex-1 m-0">

0 commit comments

Comments
 (0)