Spaces:
Running
Running
File size: 8,477 Bytes
a27839e |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
"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; |