omnidev / components /space /ask-ai /chat-interface.tsx
fokan's picture
Initial commit
13e47b9
raw
history blame
7.61 kB
"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>
);
}