Spaces:
Running
Running
| "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; |