FrontPilot / src /components /ChatInterface.tsx
[dyad]
Init Dyad app
a27839e
"use client";
import { useRef, useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Send, Loader2, Sparkles, User, Bot, Check, X, GitMerge, Code, FileEdit, Eye } from "lucide-react";
import { ChatMessage } from "@/services/gemini";
import { DiffResult, generateDiffSummary } from "@/services/diffPatch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
interface ChangeProposal {
filename: string;
diff: DiffResult;
}
interface ChatInterfaceProps {
prompt: string;
setPrompt: (prompt: string) => void;
messages: ChatMessage[];
isLoading: boolean;
onSubmit: (e: React.FormEvent) => void;
pendingModification: { changes: ChangeProposal[] } | null;
onAccept: () => void;
onReject: () => void;
onReview: (diff: DiffResult) => void;
selectedElement: { tagName: string; description: string } | null;
onClearSelectedElement: () => void;
}
const loadingMessages = [
"Thinking...",
"Warming up the AI...",
"Analyzing your request...",
"Consulting with the code spirits...",
"Brewing a fresh batch of code...",
"Thinking more deeply...",
"Structuring the layout...",
"Compiling pixels...",
"Almost done...",
"Putting on the finishing touches..."
];
const ChatInterface = ({
prompt,
setPrompt,
messages,
isLoading,
onSubmit,
pendingModification,
onAccept,
onReject,
onReview,
selectedElement,
onClearSelectedElement,
}: ChatInterfaceProps) => {
const chatContainerRef = useRef<HTMLDivElement>(null);
const [loadingMessage, setLoadingMessage] = useState(loadingMessages[0]);
useEffect(() => {
if (chatContainerRef.current) {
chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight;
}
}, [messages, isLoading, pendingModification]);
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (isLoading) {
let messageIndex = 0;
setLoadingMessage(loadingMessages[0]);
intervalId = setInterval(() => {
messageIndex = (messageIndex + 1) % loadingMessages.length;
setLoadingMessage(loadingMessages[messageIndex]);
}, 3500);
}
return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
}, [isLoading]);
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!prompt.trim() || isLoading) return;
onSubmit(e);
// Clear the prompt after submission
setPrompt("");
};
const renderMessage = (message: ChatMessage, index: number) => (
<div key={index} className={`flex animate-in ${message.role === "user" ? "justify-end" : "justify-start"}`}>
<div className={`max-w-[85%] rounded-2xl p-4 ${message.role === "user" ? "bg-primary text-primary-foreground rounded-br-none" : "bg-card border rounded-bl-none shadow-sm"}`}>
<div className="flex items-center mb-1">
{message.role === "user" ? <User className="h-4 w-4 mr-2" /> : <Bot className="h-4 w-4 mr-2" />}
<span className="font-medium text-sm">{message.role === "user" ? "You" : "Assistant"}</span>
</div>
<div className="text-sm whitespace-pre-wrap">{message.content}</div>
</div>
</div>
);
return (
<div className="h-full flex flex-col relative bg-secondary/30">
<div ref={chatContainerRef} className="flex-1 overflow-y-auto p-4 space-y-4 pb-40">
{messages.length === 0 && !isLoading && (
<div className="text-center text-muted-foreground mt-8 animate-in">
<div className="mx-auto w-16 h-16 bg-gradient-to-r from-blue-100 to-indigo-100 rounded-full flex items-center justify-center mb-4">
<Sparkles className="h-8 w-8 text-primary" />
</div>
<h3 className="font-bold text-lg mb-2 text-foreground">Describe your website prototype</h3>
<p className="text-sm mb-4">Example: "Create a landing page and an about page..."</p>
</div>
)}
{messages.map(renderMessage)}
{pendingModification && (
<div className="animate-in">
<div className="flex items-center p-4 bg-card rounded-t-2xl border-b">
<GitMerge className="h-5 w-5 mr-3 text-primary" />
<div>
<h4 className="font-medium text-foreground">Code Modification Proposed</h4>
<p className="text-sm text-muted-foreground">Review the changes below.</p>
</div>
</div>
<div className="space-y-2 p-4 bg-card rounded-b-2xl border-x border-b">
{pendingModification.changes.map((change, index) => {
const summary = generateDiffSummary(change.diff);
return (
<Card key={index} className="bg-secondary/50">
<CardContent className="p-3 flex items-center justify-between">
<div className="flex items-center">
<FileEdit className="h-4 w-4 text-muted-foreground mr-3" />
<div>
<p className="text-sm font-mono font-medium">{change.filename}</p>
<p className="text-xs text-muted-foreground">
<span className="text-green-600">+{summary.added}</span>, <span className="text-red-600">-{summary.removed}</span> lines
</p>
</div>
</div>
<Button size="sm" variant="outline" onClick={() => onReview(change.diff)}>
<Eye className="h-4 w-4 mr-1" /> Review
</Button>
</CardContent>
</Card>
);
})}
<div className="flex flex-wrap items-center gap-2 pt-2">
<Button size="sm" onClick={onAccept} className="bg-green-600 hover:bg-green-700"><Check className="h-4 w-4 mr-1" /> Accept All</Button>
<Button size="sm" onClick={onReject} variant="destructive"><X className="h-4 w-4 mr-1" /> Reject All</Button>
</div>
</div>
</div>
)}
{isLoading && (
<div className="flex items-center p-4 bg-card rounded-2xl border rounded-bl-none shadow-sm animate-in">
<Loader2 className="h-4 w-4 animate-spin text-primary mr-3" />
<span className="text-sm text-muted-foreground">{loadingMessage}</span>
</div>
)}
</div>
<div className="absolute bottom-0 left-0 right-0 p-4 bg-secondary/30">
{selectedElement && (
<div className="bg-card border rounded-lg p-2.5 mb-2 flex items-center justify-between animate-in fade-in slide-in-from-bottom-2 duration-300 shadow-sm">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Code className="h-5 w-5 text-muted-foreground flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="font-mono text-sm font-medium text-foreground">{selectedElement.tagName}</p>
<p className="text-xs text-muted-foreground truncate">{selectedElement.description}</p>
</div>
</div>
<Button variant="ghost" size="icon" className="h-7 w-7 flex-shrink-0" onClick={onClearSelectedElement}>
<X className="h-4 w-4" />
</Button>
</div>
)}
<form onSubmit={handleFormSubmit}>
<div className="relative">
<Textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the website you want to create or request a modification..."
className="w-full resize-none border border-border/50 bg-background/60 backdrop-blur-lg focus:border-primary focus:ring-primary rounded-2xl shadow-xl py-4 pl-4 pr-16"
rows={3}
disabled={isLoading || !!pendingModification}
/>
<Button type="submit" disabled={isLoading || !prompt.trim() || !!pendingModification} className="absolute right-3 bottom-3 h-10 w-10 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 shadow-lg flex items-center justify-center transition-transform hover:scale-110">
<Send className="h-4 w-4" />
</Button>
</div>
</form>
</div>
</div>
);
};
export default ChatInterface;