Skip to content

Commit

Permalink
Docs: Bot: Update
Browse files Browse the repository at this point in the history
PR-URL: hasura/graphql-engine-mono#11079
GitOrigin-RevId: 48da2933e84289a5afca007ed054f5070bc11eaf
  • Loading branch information
seanparkross authored and hasura-bot committed Nov 14, 2024
1 parent ee3c1a5 commit 5c7e20f
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 195 deletions.
28 changes: 19 additions & 9 deletions docs/docusaurus.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const path = require('path');
const lightCodeTheme = require('prism-react-renderer/themes/vsLight');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');

const BOT_ROUTES = {
development: 'ws://localhost:8000/bot/query',
production: 'wss://website-api.hasura.io/docs-services/docs-server/bot/query',
staging: 'wss://website-api.stage.hasura.io/docs-services/docs-server/bot/query',
};

/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Hasura GraphQL Docs',
Expand All @@ -20,15 +26,19 @@ const config = {
staticDirectories: ['static', 'public'],
customFields: {
docsBotEndpointURL: (() => {
switch (process.env.release_mode) {
case 'development':
return 'ws://localhost:8000/hasura-docs-ai';
case 'production':
return 'wss://website-api.hasura.io/chat-bot/hasura-docs-ai';
case 'staging':
return 'wss://website-api.stage.hasura.io/chat-bot/hasura-docs-ai';
default:
return 'ws://localhost:8000/hasura-docs-ai'; // default to development if no match
if (process.env.CF_PAGES === '1') {
return BOT_ROUTES.staging; // if we're on CF pages, use the staging environment
} else {
switch (process.env.release_mode) {
case 'development':
return BOT_ROUTES.development; // if we're on the development environment, use the local server
case 'production':
return BOT_ROUTES.production;
case 'staging':
return BOT_ROUTES.production; // if we're in full staging on GCP and not cloudflare pages, use the production environment
default:
return BOT_ROUTES.development; // default to development if no match (env var not generally set on local dev)
}
}
})(),
hasuraVersion: 2,
Expand Down
155 changes: 107 additions & 48 deletions docs/src/components/AiChatBot/AiChatBot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,18 @@ import './styles.css';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { CloseIcon, RespondingIconGray, SparklesIcon } from '@site/src/components/AiChatBot/icons';
import { useLocalStorage } from 'usehooks-ts';
import profilePic from '@site/static/img/docs-bot-profile-pic.webp';
// @ts-ignore
import profilePic from './docs-bot-profile-pic.webp';
import { v4 as uuidv4 } from 'uuid';
import ThumbsDown from './thumbs-down.svg';
import { BadBotResponse, BotWebsocketEvent, WebsocketPayload } from '@site/src/components/AiChatBot/types';

interface Message {
userMessage: string;
botResponse: string;
id?: string;
}

interface Query {
previousMessages: Message[];
currentUserInput: string;
}

// Websocket Event data types (stringified)
// { type: "loading", message: "Processing your request..." }
// { type: "responsePart", message: "...part of response..." }
// { type: "error", message: "error description" }
// { type: "endOfStream", message: "End of stream..." }

const initialMessages: Message[] = [
{
userMessage: '',
Expand Down Expand Up @@ -51,10 +44,13 @@ export function AiChatBot({ style }) {
// Manage the text input
const [input, setInput] = useState<string>('');
// Manage the message thread ID
const [messageThreadId, setMessageThreadId] = useLocalStorage<String>(
const [messageThreadId, setMessageThreadId] = useLocalStorage<string>(
`hasuraV${customFields.hasuraVersion}ThreadId`,
uuidv4()
);
// Manage the responseQuality
const [badResponse, setBadResponse] = useState<BadBotResponse | null>(null);

// Manage the historical messages
const [messages, setMessages] = useLocalStorage<Message[]>(
`hasuraV${customFields.hasuraVersion}BotMessages`,
Expand Down Expand Up @@ -83,7 +79,8 @@ export function AiChatBot({ style }) {

const sanitizeInput = (input: string): string => {
const sanitized = DOMPurify.sanitize(input.trim());
return sanitized.replace(/&/g, '&amp;')
return sanitized
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
Expand All @@ -104,7 +101,7 @@ export function AiChatBot({ style }) {
behavior: 'smooth',
});
}
}, [currentMessage.botResponse]);
}, [currentMessage.botResponse, badResponse]);

// Detect if user scrolls up and disable auto-scrolling
const handleScroll = e => {
Expand All @@ -125,8 +122,6 @@ export function AiChatBot({ style }) {

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

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

const connectWebSocket = () => {
websocket = new WebSocket(
encodeURI(`${docsBotEndpointURL}?version=${hasuraVersion}&userId=${storedUserID}${queryDevToken}`)
Expand All @@ -138,11 +133,13 @@ export function AiChatBot({ style }) {
clearTimeout(reconnectInterval);
};

// Handle incoming messages
websocket.onmessage = event => {
let response = { type: '', message: '' };
let response: BotWebsocketEvent;

try {
response = JSON.parse(event.data) as { type: string; message: string };
response = JSON.parse(event.data) as { type: BotWebsocketEvent['type']; message: string };
// TODO check if this is the correct type
} catch (e) {
console.error('error parsing websocket message', e);
}
Expand Down Expand Up @@ -213,14 +210,44 @@ export function AiChatBot({ style }) {
}

if (ws) {
const toSend = JSON.stringify({ previousMessages: messages, currentUserInput: input, messageThreadId });
setCurrentMessage({ userMessage: sanitizedInput, botResponse: '' });
const messageId = uuidv4();
setCurrentMessage({ userMessage: sanitizedInput, botResponse: '', id: messageId });
setInput('');
ws.send(toSend);
const chatPayload: WebsocketPayload = {
payloadType: 'chatMessage',
chatMessage: {
previousMessages: messages,
currentUserInput: sanitizedInput,
messageId,
messageThreadId,
},
responseQuality: null,
};
ws.send(JSON.stringify(chatPayload));
setIsResponding(true);
}
};

const handleBadBotResponse = async () => {
if (badResponse) {
console.log('responseQuality', badResponse);
// TODO SANITIZE AND VALIDATE RESPONSE QUALITY!!!
if (ws) {
const badBotResponsePayload: WebsocketPayload = {
payloadType: 'badBotResponse',
badBotResponse: {
messageId: badResponse.messageId,
responseText: badResponse.responseText,
},
chatMessage: null,
};

ws.send(JSON.stringify(badBotResponsePayload));
}
setBadResponse(null);
}
};

const renderMessage = (content: string) => {
return (
<Markdown
Expand All @@ -229,7 +256,7 @@ export function AiChatBot({ style }) {
a: {
props: {
target: '_blank',
rel: 'noopener noreferrer'
rel: 'noopener noreferrer',
},
},
},
Expand All @@ -246,22 +273,16 @@ export function AiChatBot({ style }) {
window.location.href.endsWith('/overview/');

return (
<div className={'chat-popup'}>
<div className={isOnOverviewOrIndex ? 'chat-popup-index-and-overviews' : 'chat-popup-other-pages'}>
{isOpen ? (
<></>
) : (
<button className="open-chat-button" onClick={() => setIsOpen(!isOpen)}>
{SparklesIcon} Hasura Docs AI Chat
</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="chat-popup">
<button className="open-chat-button" onClick={() => setIsOpen(true)}>
{SparklesIcon} Docs Assistant
</button>
{isOpen && (
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
<div onClick={e => e.stopPropagation()}>
<button className="close-chat-button" onClick={() => setIsOpen(false)}>
{CloseIcon} Close Chat
</button>
<div className="chat-window">
<div className="info-bar">
<div className={'bot-name-pic-container'}>
Expand All @@ -272,7 +293,7 @@ export function AiChatBot({ style }) {
className="clear-button"
onClick={() => {
setMessages(initialMessages);
setCurrentMessage({ userMessage: '', botResponse: '' });
setCurrentMessage({ userMessage: '', botResponse: '', id: '' });
setMessageThreadId(uuidv4());
}}
>
Expand All @@ -284,16 +305,12 @@ export function AiChatBot({ style }) {
<div key={index}>
{msg.userMessage && (
<div className="user-message-container">
<div className="formatted-text message user-message">
{renderMessage(msg.userMessage)}
</div>
<div className="formatted-text message user-message">{renderMessage(msg.userMessage)}</div>
</div>
)}
{msg.botResponse && (
<div className="bot-message-container">
<div className="formatted-text message bot-message">
{renderMessage(msg.botResponse)}
</div>
<div className="formatted-text message bot-message">{renderMessage(msg.botResponse)}</div>
</div>
)}
</div>
Expand All @@ -313,12 +330,54 @@ export function AiChatBot({ style }) {
</div>
)}
</div>
{messages.length > 3 && !isResponding && (
<form
id="bad-response-form"
className="bad-response-form"
onSubmit={e => {
e.preventDefault();
handleBadBotResponse();
}}
>
<div className={'flex'}>
<button
className="thumbs-down-button"
type="button"
onClick={() => {
setBadResponse({
responseText: null,
messageId: messages.at(-1).id ?? '',
});
}}
>
<ThumbsDown className={'mb-4'} />
</button>
</div>
<div>
{badResponse !== null && (
<div className="bad-response-container">
<textarea
rows={4}
onChange={e =>
setBadResponse(prevState => ({ ...prevState, responseText: e.target.value }))
}
placeholder={'Sorry about that. Please tell us how we can improve.'}
></textarea>
<button className="feedback-submit-button" type={'submit'}>
Submit Feedback
</button>
</div>
)}
</div>
</form>
)}
<div className="responding-div">{isResponding ? RespondingIconGray : null}</div>
</div>
</div>
{/* Handles scrolling to the end */}
{/*<div ref={messagesEndRef} />*/}
<form
id={'chat-form'}
className="input-container"
onSubmit={e => {
e.preventDefault();
Expand All @@ -338,8 +397,8 @@ export function AiChatBot({ style }) {
</form>
</div>
</div>
)}
</div>
</div>
)}
</div>
);
}
Binary file not shown.
61 changes: 51 additions & 10 deletions docs/src/components/AiChatBot/icons.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,54 @@
import React from "react";
import React from 'react';

export const SparklesIcon = <svg className="sparkles-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 30" x="0px" y="0px"><path d="m15.6968,12.4905l-2.8295,1.4148c-.8491.4245-1.5375,1.113-1.962,1.962l-1.4148,2.8295c-.2021.4042-.7789.4042-.981,0l-1.4148-2.8295c-.4245-.8491-1.113-1.5375-1.962-1.962l-2.8295-1.4148c-.4042-.2021-.4042-.7789,0-.981l2.8295-1.4148c.8491-.4246,1.5375-1.113,1.962-1.9621l1.4148-2.8295c.2021-.4042.7789-.4042.981,0l1.4148,2.8295c.4245.8491,1.113,1.5375,1.962,1.9621l2.8295,1.4148c.4042.2021.4042.7789,0,.981Zm6.1732,6.2993l-1.2127-.6063c-.3639-.182-.6589-.477-.8409-.8409l-.6063-1.2126c-.0866-.1732-.3338-.1732-.4204,0l-.6063,1.2126c-.1819.3639-.477.6589-.8409.8409l-1.2127.6063c-.1732.0866-.1732.3338,0,.4204l1.2127.6063c.3639.1819.6589.477.8409.8409l.6063,1.2126c.0866.1732.3338.1732.4204,0l.6063-1.2126c.1819-.3639.477-.6589.8409-.8409l1.2127-.6063c.1732-.0866.1732-.3338,0-.4204Zm0-14l-1.2127-.6063c-.3639-.182-.6589-.477-.8409-.8409l-.6063-1.2126c-.0866-.1732-.3338-.1732-.4204,0l-.6063,1.2126c-.1819.3639-.477.6589-.8409.8409l-1.2127.6063c-.1732.0866-.1732.3338,0,.4204l1.2127.6063c.3639.1819.6589.477.8409.8409l.6063,1.2126c.0866.1732.3338.1732.4204,0l.6063-1.2126c.1819-.3639.477-.6589.8409-.8409l1.2127-.6063c.1732-.0866.1732-.3338,0-.4204Z"/><text x="0" y="39" fill="#111111" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Royyan Wijaya</text><text x="0" y="44" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg>
export const SparklesIcon = (
<svg className="sparkles-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 30" x="0px" y="0px">
<path d="m15.6968,12.4905l-2.8295,1.4148c-.8491.4245-1.5375,1.113-1.962,1.962l-1.4148,2.8295c-.2021.4042-.7789.4042-.981,0l-1.4148-2.8295c-.4245-.8491-1.113-1.5375-1.962-1.962l-2.8295-1.4148c-.4042-.2021-.4042-.7789,0-.981l2.8295-1.4148c.8491-.4246,1.5375-1.113,1.962-1.9621l1.4148-2.8295c.2021-.4042.7789-.4042.981,0l1.4148,2.8295c.4245.8491,1.113,1.5375,1.962,1.9621l2.8295,1.4148c.4042.2021.4042.7789,0,.981Zm6.1732,6.2993l-1.2127-.6063c-.3639-.182-.6589-.477-.8409-.8409l-.6063-1.2126c-.0866-.1732-.3338-.1732-.4204,0l-.6063,1.2126c-.1819.3639-.477.6589-.8409.8409l-1.2127.6063c-.1732.0866-.1732.3338,0,.4204l1.2127.6063c.3639.1819.6589.477.8409.8409l.6063,1.2126c.0866.1732.3338.1732.4204,0l.6063-1.2126c.1819-.3639.477-.6589.8409-.8409l1.2127-.6063c.1732-.0866.1732-.3338,0-.4204Zm0-14l-1.2127-.6063c-.3639-.182-.6589-.477-.8409-.8409l-.6063-1.2126c-.0866-.1732-.3338-.1732-.4204,0l-.6063,1.2126c-.1819.3639-.477.6589-.8409.8409l-1.2127.6063c-.1732.0866-.1732.3338,0,.4204l1.2127.6063c.3639.1819.6589.477.8409.8409l.6063,1.2126c.0866.1732.3338.1732.4204,0l.6063-1.2126c.1819-.3639.477-.6589.8409-.8409l1.2127-.6063c.1732-.0866.1732-.3338,0-.4204Z" />
<text
x="0"
y="39"
fill="#111111"
font-size="5px"
font-weight="bold"
font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif"
>
Created by Royyan Wijaya
</text>
<text
x="0"
y="44"
fill="#000000"
font-size="5px"
font-weight="bold"
font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif"
>
from the Noun Project
</text>
</svg>
);

export const CloseIcon = <svg className="close-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 30" fill="none" x="0px" y="0px"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.69292 18.3071C5.30239 17.9166 5.30239 17.2834 5.69292 16.8929L16.8929 5.69292C17.2834 5.30239 17.9166 5.30239 18.3071 5.69292C18.6977 6.08344 18.6977 6.71661 18.3071 7.10713L7.10713 18.3071C6.71661 18.6977 6.08344 18.6977 5.69292 18.3071Z" fill="white"/><path fill-rule="evenodd" clip-rule="evenodd" d="M5.69292 5.69292C6.08344 5.30239 6.71661 5.30239 7.10713 5.69292L18.3071 16.8929C18.6977 17.2834 18.6977 17.9166 18.3071 18.3071C17.9166 18.6977 17.2834 18.6977 16.8929 18.3071L5.69292 7.10713C5.30239 6.71661 5.30239 6.08344 5.69292 5.69292Z" fill="white"/></svg>
export const CloseIcon = (
<svg className="close-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 30" fill="none" x="0px" y="0px">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.69292 18.3071C5.30239 17.9166 5.30239 17.2834 5.69292 16.8929L16.8929 5.69292C17.2834 5.30239 17.9166 5.30239 18.3071 5.69292C18.6977 6.08344 18.6977 6.71661 18.3071 7.10713L7.10713 18.3071C6.71661 18.6977 6.08344 18.6977 5.69292 18.3071Z"
fill="white"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5.69292 5.69292C6.08344 5.30239 6.71661 5.30239 7.10713 5.69292L18.3071 16.8929C18.6977 17.2834 18.6977 17.9166 18.3071 18.3071C17.9166 18.6977 17.2834 18.6977 16.8929 18.3071L5.69292 7.10713C5.30239 6.71661 5.30239 6.08344 5.69292 5.69292Z"
fill="white"
/>
</svg>
);

export const RespondingIconGray = <svg width="30" height="30" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" fill="none" r="10" stroke="#BABABA" strokeWidth="2">
<animate attributeName="r" from="8" to="20" dur="1.5s" begin="0s" repeatCount="indefinite"/>
<animate attributeName="opacity" from="1" to="0" dur="1.5s" begin="0s" repeatCount="indefinite"/>
</circle>
<circle cx="20" cy="20" fill="#BABABA" r="10"/>
</svg>
export const RespondingIconGray = (
<svg width="30" height="30" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg">
<circle cx="20" cy="20" fill="none" r="10" stroke="#BABABA" strokeWidth="2">
<animate attributeName="r" from="8" to="20" dur="1.5s" begin="0s" repeatCount="indefinite" />
<animate attributeName="opacity" from="1" to="0" dur="1.5s" begin="0s" repeatCount="indefinite" />
</circle>
<circle cx="20" cy="20" fill="#BABABA" r="10" />
</svg>
);
Loading

0 comments on commit 5c7e20f

Please sign in to comment.