Spaces:
Running
Running
"use client"; | |
import { useState, useRef, useEffect } from "react"; | |
import { Button } from "@/components/ui/button"; | |
import { Textarea } from "@/components/ui/textarea"; | |
import { ScrollArea } from "@/components/ui/scroll-area"; | |
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
import { SendIcon } from "lucide-react"; | |
import { useLocalStorage } from "react-use"; | |
import { MODELS } from "@/lib/providers"; | |
import { Settings } from "@/components/editor/ask-ai/settings"; | |
interface Message { | |
id: string; | |
role: "user" | "assistant"; | |
content: string; | |
timestamp: Date; | |
} | |
export function ChatInterface() { | |
const [messages, setMessages] = useState<Message[]>([]); | |
const [input, setInput] = useState(""); | |
const [isLoading, setIsLoading] = useState(false); | |
const [provider, setProvider] = useLocalStorage("provider", "auto"); | |
const [model, setModel] = useLocalStorage("model", MODELS[0].value); | |
const [openProvider, setOpenProvider] = useState(false); | |
const scrollAreaRef = useRef<HTMLDivElement>(null); | |
// Auto-scroll to bottom when messages change | |
useEffect(() => { | |
if (scrollAreaRef.current) { | |
scrollAreaRef.current.scrollTop = scrollAreaRef.current.scrollHeight; | |
} | |
}, [messages]); | |
const handleSubmit = async (e: React.FormEvent) => { | |
e.preventDefault(); | |
if (!input.trim() || isLoading) return; | |
// Add user message | |
const userMessage: Message = { | |
id: Date.now().toString(), | |
role: "user", | |
content: input, | |
timestamp: new Date(), | |
}; | |
setMessages((prev) => [...prev, userMessage]); | |
setInput(""); | |
setIsLoading(true); | |
try { | |
// Add assistant message placeholder | |
const assistantMessageId = (Date.now() + 1).toString(); | |
const assistantMessage: Message = { | |
id: assistantMessageId, | |
role: "assistant", | |
content: "", | |
timestamp: new Date(), | |
}; | |
setMessages((prev) => [...prev, assistantMessage]); | |
// Call AI API | |
const response = await fetch("/api/ask-ai", { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify({ | |
prompt: input, | |
provider, | |
model, | |
}), | |
}); | |
if (!response.ok) { | |
throw new Error("Failed to get response from AI"); | |
} | |
const reader = response.body?.getReader(); | |
const decoder = new TextDecoder(); | |
if (reader) { | |
let assistantResponse = ""; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
const chunk = decoder.decode(value); | |
assistantResponse += chunk; | |
// Update the assistant message with the new content | |
setMessages((prev) => | |
prev.map((msg) => | |
msg.id === assistantMessageId | |
? { ...msg, content: assistantResponse } | |
: msg | |
) | |
); | |
} | |
} | |
} catch (error) { | |
console.error("Error:", error); | |
setMessages((prev) => | |
prev.map((msg) => | |
msg.id === (Date.now() + 1).toString() | |
? { ...msg, content: "Sorry, I encountered an error." } | |
: msg | |
) | |
); | |
} finally { | |
setIsLoading(false); | |
} | |
}; | |
return ( | |
<div className="flex flex-col h-full bg-neutral-900 border border-neutral-800 rounded-lg"> | |
<div className="p-4 border-b border-neutral-800"> | |
<div className="flex items-center justify-between"> | |
<h2 className="text-lg font-semibold">Chat with AI</h2> | |
<Settings | |
provider={provider as string} | |
model={model as string} | |
onChange={setProvider} | |
onModelChange={setModel} | |
open={openProvider} | |
error="" | |
isFollowUp={false} | |
onClose={setOpenProvider} | |
/> | |
</div> | |
<p className="text-sm text-neutral-400 mt-1"> | |
Ask anything and get AI-powered responses | |
</p> | |
</div> | |
<ScrollArea className="flex-1 p-4" ref={scrollAreaRef}> | |
{messages.length === 0 ? ( | |
<div className="flex flex-col items-center justify-center h-full text-center"> | |
<div className="bg-neutral-800 p-4 rounded-full mb-4"> | |
<Avatar className="w-12 h-12"> | |
<AvatarFallback>AI</AvatarFallback> | |
</Avatar> | |
</div> | |
<h3 className="text-lg font-medium mb-2">How can I help you today?</h3> | |
<p className="text-neutral-400 max-w-md"> | |
Ask me anything about web development, design, or get help with your project. | |
</p> | |
</div> | |
) : ( | |
<div className="space-y-4"> | |
{messages.map((message) => ( | |
<div | |
key={message.id} | |
className={`flex gap-3 ${ | |
message.role === "user" ? "justify-end" : "justify-start" | |
}`} | |
> | |
{message.role === "assistant" && ( | |
<Avatar className="w-8 h-8"> | |
<AvatarFallback>AI</AvatarFallback> | |
</Avatar> | |
)} | |
<div | |
className={`max-w-[80%] rounded-2xl px-4 py-2 ${ | |
message.role === "user" | |
? "bg-blue-600 text-white rounded-tr-none" | |
: "bg-neutral-800 text-neutral-100 rounded-tl-none" | |
}`} | |
> | |
<p className="whitespace-pre-wrap">{message.content}</p> | |
</div> | |
{message.role === "user" && ( | |
<Avatar className="w-8 h-8"> | |
<AvatarFallback>U</AvatarFallback> | |
</Avatar> | |
)} | |
</div> | |
))} | |
{isLoading && ( | |
<div className="flex gap-3 justify-start"> | |
<Avatar className="w-8 h-8"> | |
<AvatarFallback>AI</AvatarFallback> | |
</Avatar> | |
<div className="bg-neutral-800 text-neutral-100 rounded-2xl rounded-tl-none px-4 py-2"> | |
<div className="flex space-x-2"> | |
<div className="w-2 h-2 rounded-full bg-neutral-400 animate-bounce"></div> | |
<div className="w-2 h-2 rounded-full bg-neutral-400 animate-bounce delay-75"></div> | |
<div className="w-2 h-2 rounded-full bg-neutral-400 animate-bounce delay-150"></div> | |
</div> | |
</div> | |
</div> | |
)} | |
</div> | |
)} | |
</ScrollArea> | |
<form onSubmit={handleSubmit} className="p-4 border-t border-neutral-800"> | |
<div className="flex gap-2"> | |
<Textarea | |
value={input} | |
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setInput(e.target.value)} | |
placeholder="Type your message here..." | |
className="min-h-[40px] flex-1 bg-neutral-800 border-neutral-700 text-white resize-none" | |
disabled={isLoading} | |
onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => { | |
if (e.key === "Enter" && !e.shiftKey) { | |
e.preventDefault(); | |
handleSubmit(e as any); | |
} | |
}} | |
/> | |
<Button | |
type="submit" | |
size="icon" | |
disabled={!input.trim() || isLoading} | |
className="h-10 w-10" | |
> | |
<SendIcon className="h-4 w-4" /> | |
</Button> | |
</div> | |
</form> | |
</div> | |
); | |
} |