Spaces:
Running
Running
File size: 5,266 Bytes
53c9876 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 | '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>
</>
);
}
|