Files
portfolio/app/components/BentoChat.tsx
denshooter b162fc8a4f
All checks were successful
Dev Deployment (Zero Downtime) / deploy-dev (push) Successful in 18m31s
fix: prevent page scroll on load by using container scrollTop instead of scrollIntoView in BentoChat
2026-02-23 16:03:32 +01:00

115 lines
4.2 KiB
TypeScript

"use client";
import React, { useState, useEffect, useRef } from "react";
import { Send, Loader2 } from "lucide-react";
interface Message {
id: string;
text: string;
sender: "user" | "bot";
timestamp: Date;
}
interface StoredMessage {
id: string;
text: string;
sender: "user" | "bot";
timestamp: string;
}
export default function BentoChat() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputValue, setInputValue] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [conversationId, setConversationId] = useState<string>("default");
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
try {
const storedId = localStorage.getItem("chatSessionId");
if (storedId) setConversationId(storedId);
else {
const newId = crypto.randomUUID();
localStorage.setItem("chatSessionId", newId);
setConversationId(newId);
}
const storedMsgs = localStorage.getItem("chatMessages");
if (storedMsgs) {
setMessages(JSON.parse(storedMsgs).map((m: StoredMessage) => ({ ...m, timestamp: new Date(m.timestamp) })));
} else {
setMessages([{ id: "welcome", text: "Hi! Ask me anything about Dennis! 🚀", sender: "bot", timestamp: new Date() }]);
}
} catch {}
}, []);
useEffect(() => {
if (messages.length > 0) {
localStorage.setItem("chatMessages", JSON.stringify(messages));
}
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [messages]);
const handleSend = async () => {
if (!inputValue.trim() || isLoading) return;
const userMsg: Message = { id: Date.now().toString(), text: inputValue.trim(), sender: "user", timestamp: new Date() };
setMessages(prev => [...prev, userMsg]);
setInputValue("");
setIsLoading(true);
try {
const res = await fetch("/api/n8n/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMsg.text, conversationId, history: messages.slice(-5).map(m => ({ role: m.sender === "user" ? "user" : "assistant", content: m.text })) }),
});
const data = await res.json();
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), text: data.reply || "Error", sender: "bot", timestamp: new Date() }]);
} catch {
setMessages(prev => [...prev, { id: (Date.now() + 1).toString(), text: "Connection error.", sender: "bot", timestamp: new Date() }]);
} finally {
setIsLoading(true);
setTimeout(() => setIsLoading(false), 500); // Small delay for feel
}
};
return (
<div className="flex flex-col h-full min-h-[300px]">
<div
ref={containerRef}
className="flex-1 overflow-y-auto pr-2 scrollbar-hide space-y-4 mb-4"
>
{messages.map((m) => (
<div key={m.id} className={`flex ${m.sender === "user" ? "justify-end" : "justify-start"}`}>
<div className={`max-w-[90%] rounded-2xl px-4 py-2 text-sm shadow-sm ${m.sender === "user" ? "bg-liquid-purple text-white" : "bg-white dark:bg-stone-800 text-stone-900 dark:text-stone-100 border border-stone-100 dark:border-stone-700"}`}>
{m.text}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-stone-100 dark:bg-stone-800 rounded-2xl px-4 py-2"><Loader2 size={14} className="animate-spin text-stone-400" /></div>
</div>
)}
</div>
<div className="relative">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleSend()}
placeholder="Ask me..."
className="w-full bg-white dark:bg-stone-800 border border-stone-200 dark:border-stone-700 rounded-2xl py-3 pl-4 pr-12 text-sm focus:outline-none focus:ring-2 focus:ring-liquid-purple/30 transition-all shadow-inner dark:text-white"
/>
<button onClick={handleSend} className="absolute right-2 top-1/2 -translate-y-1/2 p-2 text-liquid-purple hover:scale-110 transition-transform">
<Send size={18} />
</button>
</div>
</div>
);
}