Skip to content

Commit

Permalink
UI: Fix bot rendering issue (#380)
Browse files Browse the repository at this point in the history
  • Loading branch information
robertjdominguez authored May 3, 2024
1 parent 726e793 commit 76097ca
Show file tree
Hide file tree
Showing 12 changed files with 398 additions and 278 deletions.
211 changes: 114 additions & 97 deletions src/components/AiChatBot/AiChatBot.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useEffect, useRef, useState } from 'react';
import Markdown from 'markdown-to-jsx'
import Markdown from 'markdown-to-jsx';
import './styles.css';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { CloseIcon, RespondingIconGray, SparklesIcon } from '@site/src/components/AiChatBot/icons';
import { useLocalStorage } from 'usehooks-ts'
import { useLocalStorage } from 'usehooks-ts';
import profilePic from '@site/static/img/docs-bot-profile-pic.webp';
import { v4 as uuidv4 } from 'uuid';

Expand Down Expand Up @@ -38,22 +38,27 @@ const initialMessages: Message[] = [
},
];


export function AiChatBot() {
export function AiChatBot({ style }) {
// Get the docsBotEndpointURL and hasuraVersion from the siteConfig
const {
siteConfig: { customFields },
} = useDocusaurusContext();
// Manage the open state of the popup
const [isOpen, setIsOpen] = useState<boolean>(false);
// Manage the bot responding state
const [isResponding, setIsResponding] = useState<boolean>(false)
const [isResponding, setIsResponding] = useState<boolean>(false);
// Manage the text input
const [input, setInput] = useState<string>('');
// Manage the message thread ID
const [messageThreadId, setMessageThreadId] = useLocalStorage<String>(`hasuraV${customFields.hasuraVersion}ThreadId`, uuidv4())
const [messageThreadId, setMessageThreadId] = useLocalStorage<String>(
`hasuraV${customFields.hasuraVersion}ThreadId`,
uuidv4()
);
// Manage the historical messages
const [messages, setMessages] = useLocalStorage<Message[]>(`hasuraV${customFields.hasuraVersion}BotMessages`, initialMessages);
const [messages, setMessages] = useLocalStorage<Message[]>(
`hasuraV${customFields.hasuraVersion}BotMessages`,
initialMessages
);
// Manage the current message
const [currentMessage, setCurrentMessage] = useState<Message>({ userMessage: '', botResponse: '' });
// Manage scrolling to the end
Expand All @@ -69,23 +74,28 @@ export function AiChatBot() {
// Enables scrolling to the end
const scrollDiv = useRef<HTMLDivElement>(null);

const { docsBotEndpointURL, hasuraVersion, DEV_TOKEN } = customFields as { docsBotEndpointURL: string; hasuraVersion: number; DEV_TOKEN: string };
const { docsBotEndpointURL, hasuraVersion, DEV_TOKEN } = customFields as {
docsBotEndpointURL: string;
hasuraVersion: number;
DEV_TOKEN: string;
};

const storedUserID = localStorage.getItem('hasuraDocsUserID') as string | "null";
const storedUserID = localStorage.getItem('hasuraDocsUserID') as string | 'null';

// Effect to auto-scroll to the bottom if autoScroll is true
useEffect(() => {
if (isAutoScroll) {
scrollDiv.current?.scrollTo({
top: scrollDiv.current.scrollHeight,
behavior: 'smooth'
behavior: 'smooth',
});
}
}, [currentMessage.botResponse]);

// Detect if user scrolls up and disable auto-scrolling
const handleScroll = (e) => {
const atBottom = Math.abs(scrollDiv.current?.scrollHeight - Math.floor(e.target.scrollTop + e.target.clientHeight)) < 2;
const handleScroll = e => {
const atBottom =
Math.abs(scrollDiv.current?.scrollHeight - Math.floor(e.target.scrollTop + e.target.clientHeight)) < 2;
setIsAutoScroll(atBottom);
};

Expand All @@ -99,55 +109,55 @@ export function AiChatBot() {
let websocket;
let reconnectInterval;

const queryDevToken = process.env.NODE_ENV === "development" && DEV_TOKEN ? `&devToken=${DEV_TOKEN}` : "";
const queryDevToken = process.env.NODE_ENV === 'development' && DEV_TOKEN ? `&devToken=${DEV_TOKEN}` : '';


console.log("process.env.NODE_ENV", process.env.NODE_ENV);
console.log('process.env.NODE_ENV', process.env.NODE_ENV);

const connectWebSocket = () => {
websocket = new WebSocket(encodeURI(`${docsBotEndpointURL}?version=${hasuraVersion}&userId=${storedUserID}${queryDevToken}`));
websocket = new WebSocket(
encodeURI(`${docsBotEndpointURL}?version=${hasuraVersion}&userId=${storedUserID}${queryDevToken}`)
);

websocket.onopen = () => {
console.log('Connected to the websocket');
setIsConnecting(false);
clearTimeout(reconnectInterval);
};

websocket.onmessage = (event) => {

let response = { type: "", message: "" };
websocket.onmessage = event => {
let response = { type: '', message: '' };

try {
response = JSON.parse(event.data) as {"type": string, "message": string}
response = JSON.parse(event.data) as { type: string; message: string };
} catch (e) {
console.error("error parsing websocket message", e);
console.error('error parsing websocket message', e);
}

switch (response.type) {
case "endOfStream": {
case 'endOfStream': {
console.log('end of stream');
setMessages((prevMessages: Message[]) => [...prevMessages, currentMessageRef.current]);
setCurrentMessage({ userMessage: '', botResponse: '' });
setIsResponding(false);
break;
}
case "responsePart": {
case 'responsePart': {
setIsResponding(true);
setCurrentMessage(prevState => {
return { ...prevState, botResponse: prevState?.botResponse + response.message };
});
break;
}
case "error": {
console.error("error", response.message);
case 'error': {
console.error('error', response.message);
break;
}
case "loading": {
console.log("loading", response.message);
case 'loading': {
console.log('loading', response.message);
break;
}
default: {
console.error("unknown response type", response.type);
console.error('unknown response type', response.type);
break;
}
}
Expand Down Expand Up @@ -193,14 +203,16 @@ export function AiChatBot() {
ws.send(toSend);
setIsResponding(true);
}

};

const isOnOverviewOrIndex = window.location.href.endsWith("/index") || window.location.href.endsWith("/overview") || window.location.href.endsWith("/overview/")
const isOnOverviewOrIndex =
window.location.href.endsWith('/index') ||
window.location.href.endsWith('/overview') ||
window.location.href.endsWith('/overview/');

return (
<div className={"chat-popup"}>
<div className={isOnOverviewOrIndex ? 'chat-popup-index-and-overviews': 'chat-popup-other-pages'}>
<div className={'chat-popup'}>
<div className={isOnOverviewOrIndex ? 'chat-popup-index-and-overviews' : 'chat-popup-other-pages'}>
{isOpen ? (
<></>
) : (
Expand All @@ -209,84 +221,89 @@ export function AiChatBot() {
</button>
)}
{isOpen && (
<div className={isOnOverviewOrIndex ? '': 'absolute -bottom-11 w-full min-w-[500px] right-[10px]'}>
{
isOpen && (
<button className="close-chat-button" onClick={() => setIsOpen(!isOpen)}>
{CloseIcon} Close Chat
</button>
)
}
<div className={isOnOverviewOrIndex ? '' : 'absolute -bottom-11 w-full min-w-[500px] right-[10px]'}>
{isOpen && (
<button className="close-chat-button" onClick={() => setIsOpen(!isOpen)}>
{CloseIcon} Close Chat
</button>
)}
<div className="chat-window">
<div className="info-bar">
<div className={"bot-name-pic-container"}>
<div className="bot-name">DocsBot</div>
<img src={profilePic} height={30} width={30} className="bot-pic"/>
<div className="info-bar">
<div className={'bot-name-pic-container'}>
<div className="bot-name">DocsBot</div>
<img src={profilePic} height={30} width={30} className="bot-pic" />
</div>
<button
className="clear-button"
onClick={() => {
setMessages(initialMessages);
setCurrentMessage({ userMessage: '', botResponse: '' });
setMessageThreadId(uuidv4());
}}
>
Clear
</button>
</div>
<button className="clear-button" onClick={() => {
setMessages(initialMessages)
setCurrentMessage({ userMessage: '', botResponse: '' });
setMessageThreadId(uuidv4());
}}>Clear</button>
</div>
<div className="messages-container" onScroll={handleScroll} ref={scrollDiv}>
{messages.map((msg, index) => (
<div key={index}>
{msg.userMessage && (
<div className="user-message-container">
<div className="formatted-text message user-message">
<Markdown>{msg.userMessage}</Markdown>
<div className="messages-container" onScroll={handleScroll} ref={scrollDiv}>
{messages.map((msg, index) => (
<div key={index}>
{msg.userMessage && (
<div className="user-message-container">
<div className="formatted-text message user-message">
<Markdown>{msg.userMessage}</Markdown>
</div>
</div>
</div>
)}
{msg.botResponse && (
<div className="bot-message-container">
<div className="formatted-text message bot-message">
<Markdown>{msg.botResponse}</Markdown>
)}
{msg.botResponse && (
<div className="bot-message-container">
<div className="formatted-text message bot-message">
<Markdown>{msg.botResponse}</Markdown>
</div>
</div>
</div>
)}
</div>
))}
<div className="user-message-container">
{currentMessage.userMessage && (
<div className="formatted-text message user-message">
<Markdown>{currentMessage.userMessage}</Markdown>
)}
</div>
)}
</div>
<div>
<div className="bot-message-container">
{currentMessage.botResponse && (
<div className="formatted-text message bot-message">
<Markdown>{currentMessage.botResponse}</Markdown>
))}
<div className="user-message-container">
{currentMessage.userMessage && (
<div className="formatted-text message user-message">
<Markdown>{currentMessage.userMessage}</Markdown>
</div>
)}
</div>
<div className="responding-div">
{isResponding ?
RespondingIconGray : null}
<div>
<div className="bot-message-container">
{currentMessage.botResponse && (
<div className="formatted-text message bot-message">
<Markdown>{currentMessage.botResponse}</Markdown>
</div>
)}
</div>
<div className="responding-div">{isResponding ? RespondingIconGray : null}</div>
</div>
</div>
</div>
{/* Handles scrolling to the end */}
{/*<div ref={messagesEndRef} />*/}
<form
className="input-container"
onSubmit={e => {
e.preventDefault();
handleSubmit();
}}
>
<input disabled={isResponding || isConnecting} className="input-text" value={input} onChange={e => setInput(e.target.value)} />
<button disabled={isResponding || isConnecting} className="input-button" type="submit">
{isConnecting ? "Connecting..." : isResponding ? "Responding..." : "Send"}
</button>
</form>
{/* Handles scrolling to the end */}
{/*<div ref={messagesEndRef} />*/}
<form
className="input-container"
onSubmit={e => {
e.preventDefault();
handleSubmit();
}}
>
<input
disabled={isResponding || isConnecting}
className="input-text"
value={input}
onChange={e => setInput(e.target.value)}
/>
<button disabled={isResponding || isConnecting} className="input-button" type="submit">
{isConnecting ? 'Connecting...' : isResponding ? 'Responding...' : 'Send'}
</button>
</form>
</div>
</div>
)}
</div>
</div>
);
}
}
Loading

0 comments on commit 76097ca

Please sign in to comment.