@kshrd/chatbotwidget
Version:
A Rchatbot widget for web applications built with Next.js
305 lines (280 loc) • 9.86 kB
JavaScript
'use client';
import Image from 'next/image';
import { useState, useEffect, useRef } from 'react';
import closeIcon from '../../public/cross.png';
import catIcon from '../../public/cat.png';
import sent from '../../public/sent.png';
import { generateExternalSession } from '../services/chatbot.services';
export default function Chat({ defaultText, apiKey, sessionId, projectId }) {
const [isExpanded, setIsExpanded] = useState(false);
const [messages, setMessages] = useState([
{ type: 'bot', text: defaultText ? defaultText : "Hello, How can I help you?" },
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const chatRef = useRef(null);
const wsRef = useRef(null); // WebSocket reference
useEffect(() => {
if (isExpanded) {
chatRef.current?.scrollTo({ top: chatRef.current.scrollHeight, behavior: 'smooth' });
}
}, [messages, isExpanded]);
// Establish WebSocket connection
useEffect(() => {
const createSesion = async () => {
try {
const api_key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwcm9qZWN0X25hbWUiOiJ0ZXhib3QiLCJlbWFpbCI6InN0cmluZ0BnbWFpbC5jb20iLCJwcm9qZWN0X2lkIjoxfQ.Rl2sl3KllysJslqAnr5QCqQf4n8b52OnFuNK0woYqAw"
const data = await generateExternalSession(api_key);
console.log("session_id: ", data?.session_id?.id)
} catch (e) { }
}
wsRef.current = new WebSocket('wss://api2.texbot.space/ws/generate-response');
wsRef.current.onopen = () => {
console.log('WebSocket connection established.');
createSesion();
};
wsRef.current.onmessage = (event) => {
console.log('WebSocket message received:', event.data);
try {
const data = JSON.parse(event.data);
if (data.type === 'error') {
setMessages((prevMessages) => [
...prevMessages,
{ type: 'bot', text: `Error: ${data.content}` }, // Show error message
]);
} else if (data.type === 'paragraph' && data.content?.trim()) {
// Only append if content is non-empty
setMessages((prevMessages) => {
const lastMessage = prevMessages[prevMessages.length - 1];
if (lastMessage && lastMessage.type === 'bot') {
return [
...prevMessages.slice(0, -1), // Keep all messages except the last
{ type: 'bot', text: lastMessage.text + data.content }, // Append new content
];
}
return [
...prevMessages,
{ type: 'bot', text: data.content }, // Add a new bot message if no existing bot message
];
});
} else {
// Handle unexpected formats or empty content
console.warn('Unexpected or empty content received:', data);
}
} catch (error) {
console.error('Error parsing WebSocket response:', error);
setMessages((prevMessages) => [
...prevMessages,
{ type: 'bot', text: 'Error parsing server response.' },
]);
}
setIsLoading(false); // Stop loading after processing the response
};
wsRef.current.onclose = () => {
console.log('WebSocket connection closed.');
};
return () => {
wsRef.current?.close();
};
}, []);
const handleSend = () => {
if (input.trim() === '') return;
setMessages((prevMessages) => [...prevMessages, { type: 'user', text: input }]);
setInput(''); // Clear input after sending
setIsLoading(true); // Start loading
// Send user message to the WebSocket server
const payload = {
input: {
input: input, // User's input message
external_session_id: sessionId, // Session ID
project_id: projectId, // Project ID
},
};
wsRef.current?.send(JSON.stringify(payload));
};
const handleKeyDown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
};
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// Function to check screen width and set isMobile
const checkScreenWidth = () => {
setIsMobile(window.innerWidth <= 400);
};
// Initial check
checkScreenWidth();
// Add event listener to update on window resize
window.addEventListener('resize', checkScreenWidth);
// Cleanup event listener on unmount
return () => window.removeEventListener('resize', checkScreenWidth);
}, []);
return (
<div
style={{
position: 'fixed',
bottom: '30px',
right: '40px',
zIndex: '1000',
transition: 'transform 0.3s ease',
width: isMobile ? (isExpanded ? '315px' : '80px') : (isExpanded ? '450px' : '80px'),
height: isMobile ? (isExpanded ? '550px' : '80px') : (isExpanded ? '550px' : '80px'),
borderRadius: isExpanded ? '20px' : '29%',
overflow: 'hidden',
backgroundColor: isExpanded ? 'transparent' : 'white',
padding: '10px',
boxShadow: isExpanded
? 'none' // shadow for expanded state
: '0 4px 10px rgba(0, 0, 0, 0.25)',
}}
onClick={() => !isExpanded && setIsExpanded(true)}
>
{isExpanded ? (
<div
style={{
display: 'flex',
flexDirection: 'column',
height: '100%',
border: '1px solid white',
borderRadius: '24px',
boxShadow: '0 -4px 10px rgba(0, 0, 0, 0.2), 0 4px 10px rgba(0, 0, 0, 0.1)',
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
backgroundColor: 'white',
color: '#004B93',
borderTopLeftRadius: '16px',
borderTopRightRadius: '16px',
borderBottom: '1px solid #004B93',
}}
>
<span style={{ fontWeight: 'bold', fontSize: isMobile ? "14px" : "20px" }}>AI Assistant</span>
<button
onClick={() => setIsExpanded(false)}
style={{ background: 'none', border: 'none', cursor: 'pointer' }}
>
<Image src={closeIcon} width={20} height={20} alt="close" />
</button>
</div>
<ul
ref={chatRef}
style={{
flex: 1,
overflowY: 'auto',
padding: '16px',
backgroundColor: '#ebf4ff',
margin: 0,
listStyle: 'none',
}}
>
{messages.map((message, index) => (
<ul
key={index}
style={{
width: '100%',
display: 'flex',
marginBottom: '8px',
justifyContent: message.type === 'user' ? 'end' : 'start',
}}
>
<li
style={{
padding: '12px',
fontSize: isMobile ? "14px" : "16px",
backgroundColor: message.type === 'user' ? '#004B93' : 'white',
color: message.type === 'user' ? 'white' : '#4a4a4a',
borderRadius: message.type === 'user' ? '16px 16px 0px 16px' : '16px 16px 16px 0px',
maxWidth: '80%',
textAlign: 'left',
wordBreak: 'break-word',
overflowWrap: 'break-word',
}}
>
{message.text}
</li>
</ul>
))}
{isLoading && (
<div
style={{
display: 'flex',
justifyContent: 'center',
padding: '12px',
fontSize: '14px',
backgroundColor: 'white',
color: '#4a4a4a',
borderRadius: '16px 16px 16px 0px',
maxWidth: '15%',
width: 'auto',
marginBottom: '8px',
}}
>
{/* Typing... */}
<div
className="loader"
style={{
marginLeft: '5%',
}}
></div>
</div>
)}
</ul>
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '12px',
borderTop: '1px solid #004B93',
backgroundColor: 'white',
borderBottomLeftRadius: '16px',
borderBottomRightRadius: '16px',
}}
>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Send a message"
style={{
flex: 1,
padding: '8px',
border: 'none',
outline: 'none',
borderRadius: '12px',
marginRight: '8px',
}}
disabled={isLoading} // Disable input while loading
/>
<Image
onClick={handleSend}
src={sent}
width={30}
height={30}
alt="send"
style={{ cursor: 'pointer' }}
/>
</div>
</div>
) : (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
width: '100%',
height: '100%',
cursor: 'pointer',
}}>
<Image src={catIcon} width={50} height={50} alt="chatbot" />
</div>
)}
</div>
);
}