HydroSense / components /ChatBot.tsx
dpv007's picture
Clean sample deploy
53c9876
'use client';
import { useState, useRef, useEffect } from 'react';
import { MessageCircle, X, Send, Loader2 } from 'lucide-react';
interface ChatBotProps {
context: any;
}
export function ChatBot({ context }: ChatBotProps) {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<{role: string, content: string}[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages, isOpen]);
const sendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!input.trim() || isLoading) return;
const userMsg = input.trim();
setInput('');
const newMessages = [...messages, { role: 'user', content: userMsg }];
setMessages(newMessages);
setIsLoading(true);
try {
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
messages: newMessages,
context: context || { status: 'No live telemetry available' }
}),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.error || 'Failed to fetch');
}
if (!response.body) throw new Error('No response body');
const reader = response.body.getReader();
const decoder = new TextDecoder();
let assistantMsg = '';
setMessages([...newMessages, { role: 'assistant', content: '' }]);
while (true) {
const { value, done } = await reader.read();
if (done) break;
const textChunk = decoder.decode(value, { stream: true });
assistantMsg += textChunk;
setMessages((prev) => {
const lastIndex = prev.length - 1;
const updated = [...prev];
updated[lastIndex] = { role: 'assistant', content: assistantMsg };
return updated;
});
}
} catch (error: any) {
console.error('Chat error:', error);
setMessages((prev) => [...prev, { role: 'assistant', content: `Sorry, I encountered an error: ${error.message}` }]);
} finally {
setIsLoading(false);
}
};
return (
<>
{/* Chat Button */}
<button
onClick={() => setIsOpen(true)}
className={`fixed bottom-6 right-6 p-4 bg-blue-600 hover:bg-blue-500 text-white rounded-full shadow-lg transition-transform duration-300 ${isOpen ? 'scale-0' : 'scale-100 hover:scale-110'} z-50`}
>
<MessageCircle size={28} />
</button>
{/* Chat Window */}
<div className={`fixed bottom-6 right-6 w-80 sm:w-96 h-[500px] max-h-[80vh] bg-white border border-gray-200 rounded-2xl shadow-2xl flex flex-col transition-all duration-300 origin-bottom-right z-50 ${isOpen ? 'scale-100 opacity-100' : 'scale-0 opacity-0 pointer-events-none'}`}>
<div className="shrink-0 bg-gradient-to-r from-blue-600 to-indigo-600 p-4 rounded-t-2xl flex justify-between items-center text-white">
<div>
<h3 className="font-bold">🤖 Splashy</h3>
</div>
<button onClick={() => setIsOpen(false)} className="text-white hover:text-blue-200 transition-colors">
<X size={20} />
</button>
</div>
<div className="flex-1 p-4 overflow-y-auto bg-gray-50 flex flex-col gap-3 custom-scrollbar">
{messages.length === 0 && (
<div className="text-center text-gray-500 text-sm mt-4">
<p>Hi! I can answer questions about the current water quality data.</p>
<p className="mt-2 text-xs">Try asking: "Is the water potable?"</p>
</div>
)}
{messages.map((m, i) => (
<div key={i} className={`max-w-[85%] p-3 rounded-2xl text-sm ${m.role === 'user' ? 'bg-blue-600 text-white self-end rounded-tr-sm' : 'bg-white border border-gray-200 text-gray-800 self-start rounded-tl-sm shadow-sm'}`}>
{m.content}
</div>
))}
{isLoading && messages[messages.length - 1]?.role === 'user' && (
<div className="self-start text-gray-500 p-2">
<Loader2 className="animate-spin w-5 h-5" />
</div>
)}
<div ref={messagesEndRef} />
</div>
<form onSubmit={sendMessage} className="shrink-0 p-3 bg-white border-t border-gray-200 rounded-b-2xl flex gap-2">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Ask about the telemetry..."
className="flex-1 bg-gray-100 text-gray-800 px-4 py-2 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
/>
<button
type="submit"
disabled={!input.trim() || isLoading}
className="p-2 bg-blue-600 text-white rounded-full hover:bg-blue-500 disabled:opacity-50 transition-colors shrink-0"
>
<Send size={18} />
</button>
</form>
</div>
</>
);
}