Skip to content

Add loading state BEN-1074 #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 0 additions & 64 deletions app/builder/page.tsx

This file was deleted.

110 changes: 110 additions & 0 deletions app/chat/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
'use client';

import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { ChatInterface } from '@/components/ui-builder/chat-interface';
import { PreviewCard } from '@/components/ui-builder/preview-card';
import { benchifyFileSchema } from '@/lib/schemas';
import { z } from 'zod';

type GenerationResult = {
repairedFiles?: z.infer<typeof benchifyFileSchema>;
originalFiles?: z.infer<typeof benchifyFileSchema>;
buildOutput: string;
previewUrl: string;
};

export default function ChatPage() {
const router = useRouter();
const [result, setResult] = useState<GenerationResult | null>(null);
const [initialPrompt, setInitialPrompt] = useState<string>('');
const [isGenerating, setIsGenerating] = useState(false);

useEffect(() => {
// Get the prompt and check if we have an existing result
const storedPrompt = sessionStorage.getItem('initialPrompt');
const storedResult = sessionStorage.getItem('builderResult');

if (!storedPrompt) {
// If no prompt found, redirect back to home
router.push('/');
return;
}

setInitialPrompt(storedPrompt);

if (storedResult) {
// If we have a stored result, use it
setResult(JSON.parse(storedResult));
} else {
// If no result, start the generation process
setIsGenerating(true);
startGeneration(storedPrompt);
}
}, [router]);

const startGeneration = async (prompt: string) => {
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'component',
description: prompt,
preview: true,
}),
});

if (!response.ok) {
throw new Error('Failed to generate component');
}

const generationResult = await response.json();

// Store the result
sessionStorage.setItem('builderResult', JSON.stringify(generationResult));
setResult(generationResult);
setIsGenerating(false);
} catch (error) {
console.error('Error generating component:', error);
setIsGenerating(false);
// Handle error state
}
};

// Show loading spinner if we don't have prompt yet
if (!initialPrompt) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-background flex">
{/* Chat Interface - Left Side */}
<div className="w-1/4 min-w-80 border-r border-border bg-card flex-shrink-0">
<ChatInterface
initialPrompt={initialPrompt}
onUpdateResult={setResult}
/>
</div>

{/* Preview Area - Right Side */}
<div className="flex-1 p-4 overflow-hidden">
<PreviewCard
previewUrl={result?.previewUrl}
code={result?.repairedFiles || result?.originalFiles || []}
isGenerating={isGenerating}
prompt={initialPrompt}
/>
</div>
</div>
);
}
130 changes: 120 additions & 10 deletions components/ui-builder/preview-card.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,70 @@
import { useEffect, useState } from 'react';
import { CheckCircle, Circle, Loader2 } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { benchifyFileSchema } from "@/lib/schemas";
import { z } from "zod";
import { CodeEditor } from "./code-editor";

interface Step {
id: string;
label: string;
description: string;
}

const GENERATION_STEPS: Step[] = [
{
id: 'analyzing',
label: 'Analyzing Request',
description: 'Understanding your requirements and design specifications',
},
{
id: 'generating',
label: 'Generating Code',
description: 'Creating UI components with AI assistance',
},
{
id: 'building',
label: 'Building Project',
description: 'Setting up development environment and dependencies',
},
{
id: 'deploying',
label: 'Creating Preview',
description: 'Deploying your project for live preview',
},
];

interface PreviewCardProps {
previewUrl: string;
previewUrl?: string;
code: z.infer<typeof benchifyFileSchema>;
isGenerating?: boolean;
prompt?: string;
}

export function PreviewCard({ previewUrl, code }: PreviewCardProps) {
export function PreviewCard({ previewUrl, code, isGenerating = false, prompt }: PreviewCardProps) {
const files = code || [];
const [currentStep, setCurrentStep] = useState(0);

useEffect(() => {
if (isGenerating) {
// Reset to first step when generation starts
setCurrentStep(0);

// Automatically advance through steps for visual feedback
const stepTimer = setInterval(() => {
setCurrentStep(prev => {
// Cycle through steps, but don't go past the last one
if (prev < GENERATION_STEPS.length - 1) {
return prev + 1;
}
return prev;
});
}, 2000); // Advance every 2 seconds

return () => clearInterval(stepTimer);
}
}, [isGenerating]);

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

<TabsContent value="preview" className="flex-1 m-0">
<div className="w-full h-full overflow-hidden rounded-md border bg-background">
<iframe
title="Preview"
src={previewUrl}
className="w-full h-full"
sandbox="allow-scripts allow-same-origin"
/>
</div>
{isGenerating && prompt ? (
// Show loading progress inside the preview tab
<div className="w-full h-full flex items-center justify-center rounded-md border bg-background">
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>Building Your UI</CardTitle>
<p className="text-sm text-muted-foreground mt-2">
"{prompt.substring(0, 100)}{prompt.length > 100 ? '...' : ''}"
</p>
</CardHeader>
<CardContent className="space-y-4">
{GENERATION_STEPS.map((step, index) => {
const isCompleted = index < currentStep;
const isCurrent = index === currentStep;

return (
<div key={step.id} className="flex items-start space-x-3">
<div className="flex-shrink-0 mt-1">
{isCompleted ? (
<CheckCircle className="h-5 w-5 text-green-500" />
) : isCurrent ? (
<Loader2 className="h-5 w-5 text-primary animate-spin" />
) : (
<Circle className="h-5 w-5 text-muted-foreground" />
)}
</div>
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium ${isCompleted ? 'text-green-700 dark:text-green-400' :
isCurrent ? 'text-primary' :
'text-muted-foreground'
}`}>
{step.label}
</p>
<p className={`text-xs ${isCompleted || isCurrent ? 'text-muted-foreground' : 'text-muted-foreground/60'
}`}>
{step.description}
</p>
</div>
</div>
);
})}
</CardContent>
</Card>
</div>
) : previewUrl ? (
// Show the actual preview iframe when ready
<div className="w-full h-full overflow-hidden rounded-md border bg-background">
<iframe
title="Preview"
src={previewUrl}
className="w-full h-full"
sandbox="allow-scripts allow-same-origin"
/>
</div>
) : (
// Show loading spinner if no preview URL yet
<div className="w-full h-full flex items-center justify-center rounded-md border bg-background">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">Loading your project...</p>
</div>
</div>
)}
</TabsContent>

<TabsContent value="code" className="flex-1 m-0">
Expand Down
Loading