Jesse Johnson
New commit for backend deployment: 2025-09-25_13-24-03
c59d808
import React, { useState, useEffect, useRef } from "react";
import { Send } from "lucide-react";
import MessageBubble from "./MessageBubble";
import { useChat } from "@ai-sdk/react";
import { TextStreamChatTransport } from "ai";
import { generateChatId, getTextMessagesFromParts } from "@/lib/chat";
import { CHAT_STATUS } from "@/constants/chat";
import { useRouter, useSearchParams } from "next/navigation";
import useChatStore from "@/store/chatStore";
import EmptyChat from "./EmptyChat";
const ChatInterface = () => {
const router = useRouter();
const searchParams = useSearchParams();
const chatParam = searchParams.get("chat");
const chatId = chatParam || generateChatId();
const initialMessages = useChatStore((state) => state.messages[chatId]);
const saveChat = useChatStore((state) => state.addMessages);
const [newMessage, setNewMessage] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [autoScroll, setAutoScroll] = useState(true);
const [isLoading, setIsLoading] = useState(true);
const { messages, sendMessage, status } = useChat({
id: chatId,
messages: initialMessages,
transport: new TextStreamChatTransport({
api: `${process.env.NEXT_PUBLIC_API_URL}/chat`,
}),
onFinish: (message) => {
saveChat(chatId, message.messages);
},
});
useEffect(() => {
if (autoScroll && containerRef.current) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, autoScroll]);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const handleScroll = () => {
const isAtBottom =
container.scrollTop + container.clientHeight >= container.scrollHeight;
setAutoScroll(isAtBottom);
};
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}, []);
useEffect(() => {
if (newMessage.trim() === "") return;
if (chatId) {
router.replace(`?chat=${chatId}`);
}
}, [chatId, router, newMessage]);
const handleSendMessage = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (newMessage.trim() === "") return;
sendMessage({
text: newMessage,
});
setNewMessage("");
setAutoScroll(true);
};
const allMessages = getTextMessagesFromParts(messages);
useEffect(() => {
setIsLoading(false);
}, []);
return (
<div className="flex flex-col h-full bg-tertiary-100 rounded-b-xl">
<div
ref={containerRef}
className="flex-1 overflow-y-auto p-4 custom-scrollbar"
>
{!isLoading && allMessages.length === 0 && (
<div className="space-y-4 md:px-10 xl:px-30 h-full">
<EmptyChat onSuggestionClick={setNewMessage} />
</div>
)}
{allMessages.length > 0 && (
<div className="space-y-4 md:px-10 xl:px-30">
{allMessages.map((message) => (
<MessageBubble
key={message.id}
message={message}
isSender={message.isSender}
/>
))}
{status === CHAT_STATUS.SUBMITTED && (
<MessageBubble
message={{ text: "Thinking..." }}
isSender={false}
/>
)}
{status === CHAT_STATUS.ERROR && (
<MessageBubble
message={{ text: "An error occurred. Please try again later." }}
isSender={false}
/>
)}
<div ref={messagesEndRef} />
</div>
)}
</div>
<form onSubmit={handleSendMessage} className="pb-4 px-4 md:px-10 xl:px-30">
<div className="relative">
<input
type="text"
value={newMessage}
id="chat-input"
onChange={(e) => setNewMessage(e.target.value)}
className="block w-full p-6 md:ps-10 text-sm text-primary-700 border border-tertiary-200 bg-tertiary-200/10 rounded-xl focus:outline-primary-500 focus:ring-primary-500"
placeholder="Ask me anything about cooking, recipes, or ingredients..."
required
/>
<button
type="submit"
className="cursor-pointer text-grey-100 absolute end-3 bottom-3 bg-primary-600 enabled:hover:bg-primary-700 focus:ring-4 focus:outline-none focus:ring-primary-300 font-medium rounded-xl text-sm px-3 py-3 disabled:bg-primary-500 disabled:cursor-not-allowed"
disabled={
status === CHAT_STATUS.SUBMITTED || newMessage.trim() === ""
}
>
<Send size={18} />
</button>
</div>
</form>
</div>
);
};
export default ChatInterface;