Skip to content

Commit

Permalink
feat: Update to scroll behavior and UX tweaks
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Dec 19, 2024
1 parent daaa1cb commit 5cf6c04
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 113 deletions.
59 changes: 41 additions & 18 deletions src/components/ChatBox/ChatBox.scss
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
.scroller {
overflow-y: scroll;
scrollbar-width: thin;
width: 100%;
opacity: 1;
margin-right: 0;
.xpert-chat-scroller {
position: relative;
flex: 1;

.messages-list {
overflow-y: scroll;
scrollbar-width: thin;
position: absolute;
inset: 0;
padding: 1rem 0;
}

&:after {
content: ""; /* Add an empty content area after the chat messages */
display: block;
height: 0;
clear: both;
}
}

.loading {
font-size: 13px;
padding-left: 10px;
.loading {
font-size: 13px;
padding-left: 10px;

&:after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
-webkit-animation: ellipsis steps(4,end) 900ms infinite;
animation: ellipsis steps(4,end) 900ms infinite;
content: "\2026"; /* ascii code for the ellipsis character */
width: 0px;
&:after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
-webkit-animation: ellipsis steps(4,end) 900ms infinite;
animation: ellipsis steps(4,end) 900ms infinite;
content: "\2026"; /* ascii code for the ellipsis character */
width: 0px;
}
}

.separator {
position: absolute;
z-index: 100;
height: 5px;
padding: 5px;
opacity: 0.3;

&--top {
inset: 0 0 auto 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.35), transparent);
}

&--bottom {
inset: auto 0 0 0;
background: linear-gradient(0, rgba(0, 0, 0, 0.35), transparent);
}
}
}

Expand Down
58 changes: 38 additions & 20 deletions src/components/ChatBox/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import Message from '../Message';
import './ChatBox.scss';
import MessageDivider from '../MessageDivider';
Expand All @@ -14,34 +13,53 @@ function isToday(date) {
);
}

const scrollToBottom = (containerRef, smooth = false) => {
setTimeout(() => {
containerRef.current.scrollTo({
top: containerRef.current.scrollHeight,
behavior: smooth ? 'smooth' : 'instant',
});
});
};

// container for all of the messages
const ChatBox = ({ chatboxContainerRef }) => {
const ChatBox = () => {
const firstRender = useRef(true);
const messageContainerRef = useRef();

const { messageList, apiIsLoading } = useSelector(state => state.learningAssistant);
const messagesBeforeToday = messageList.filter((m) => (!isToday(new Date(m.timestamp))));
const messagesToday = messageList.filter((m) => (isToday(new Date(m.timestamp))));

useEffect(() => {
if (firstRender.current) {
scrollToBottom(messageContainerRef);
firstRender.current = false;
return;
}

scrollToBottom(messageContainerRef, true);
}, [messageList.length]);

// message divider should not display if no messages or if all messages sent today.
return (
<div ref={chatboxContainerRef} className="flex-grow-1 scroller d-flex flex-column pb-4">
{messagesBeforeToday.map(({ role, content, timestamp }) => (
<Message key={timestamp} variant={role} message={content} />
))}
{(messageList.length !== 0 && messagesBeforeToday.length !== 0) && (<MessageDivider text="Today" />)}
{messagesToday.map(({ role, content, timestamp }) => (
<Message key={timestamp} variant={role} message={content} />
))}
{apiIsLoading && (
<div className="xpert-chat-scroller">
<div className="messages-list d-flex flex-column" ref={messageContainerRef}>
{messagesBeforeToday.map(({ role, content, timestamp }) => (
<Message key={timestamp} variant={role} message={content} />
))}
{(messageList.length !== 0 && messagesBeforeToday.length !== 0) && (<MessageDivider text="Today" />)}
{messagesToday.map(({ role, content, timestamp }) => (
<Message key={timestamp} variant={role} message={content} />
))}
{apiIsLoading && (
<div className="loading">Xpert is thinking</div>
)}
)}
</div>
<span className="separator separator--top" />
<span className="separator separator--bottom" />
</div>
);
};

ChatBox.propTypes = {
chatboxContainerRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Element) }),
]).isRequired,
};

export default ChatBox;
2 changes: 1 addition & 1 deletion src/components/Message/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PropTypes from 'prop-types';

const Message = ({ variant, message }) => (
<div
className={`message ${variant} ${variant === 'user' ? 'align-self-end' : ''} text-left my-1 mx-2 py-2 px-3`}
className={`message ${variant} ${variant === 'user' ? 'align-self-end' : ''} text-left my-1 mx-4 py-2 px-3`}
data-hj-suppress
>
<ReactMarkdown>
Expand Down
11 changes: 11 additions & 0 deletions src/components/MessageForm/MessageForm.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
.message-form {
padding: 0.75rem 1.5rem;

.send-message-input {
.pgn__form-control-floating-label {
color: #ADADAD;
}

input {
border-radius: 1rem;
border: 1px solid #C8C8CC;
}
}

.pgn__form-control-decorator-group {
margin-inline-end: 0;
}

button {
color: #8F8F8F;
}
}
22 changes: 10 additions & 12 deletions src/components/MessageForm/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,16 @@ const MessageForm = ({ courseId, shouldAutofocus, unitId }) => {

return (
<Form className="message-form w-100" onSubmit={handleSubmitMessage} data-testid="message-form">
<Form.Group>
<Form.Control
data-hj-suppress
disabled={apiIsLoading}
floatingLabel="Write a message"
onChange={handleUpdateCurrentMessage}
trailingElement={getSubmitButton()}
value={currentMessage}
ref={inputRef}
className="send-message-input"
/>
</Form.Group>
<Form.Control
data-hj-suppress
disabled={apiIsLoading}
floatingLabel="Write a message"
onChange={handleUpdateCurrentMessage}
trailingElement={getSubmitButton()}
value={currentMessage}
ref={inputRef}
className="send-message-input"
/>
</Form>
);
};
Expand Down
36 changes: 20 additions & 16 deletions src/components/Sidebar/Sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,45 @@
background-color: white;
width: 100%;
max-width: 25rem;
/* Add smooth scrolling behavior */
scroll-behavior: smooth;

h1 {
font-size: 1.25rem;
line-height: 1.75rem;
}

button.chat-close {
top: 0;
right: 0;
top: 0.75rem;
right: 1.5rem;
height: 1.5rem;
width: 1.5rem;

.btn-icon__icon {
width: 1.375rem !important;
height: 1.375rem !important;
}
}

.sidebar-header {
display: flex;
align-items: center;
background: linear-gradient(to left, rgb(58, 101, 108) 0px, rgb(0, 29, 34));
width: 100%;
height: 60px;
height: 48px;
padding: 0 1.5rem;


svg {
height: 30px;
display: block;
height: 24px;
}
}

.sidebar-footer {

}

.trial-header {
font-size: 0.9em;
background-color: #F49974;
}
}

.separator {
z-index: 100;
width: 100%;
height: 5px;
padding: 5px;
background: -webkit-linear-gradient(270deg, rgba(0, 0, 0, 0.35), transparent);
background: linear-gradient(180deg, rgba(0, 0, 0, 0.35), transparent);
opacity: 0.3;
}
54 changes: 8 additions & 46 deletions src/components/Sidebar/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useEffect } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import PropTypes from 'prop-types';

Expand Down Expand Up @@ -37,42 +37,6 @@ const Sidebar = ({

const { track } = useTrackEvent();

const chatboxContainerRef = useRef(null);

// this use effect is intended to scroll to the bottom of the chat window, in the case
// that a message is larger than the chat window height.
useEffect(() => {
const messageContainer = chatboxContainerRef.current;

if (messageContainer) {
const { scrollHeight, clientHeight } = messageContainer;
const maxScrollTop = scrollHeight - clientHeight;
const duration = 200;

if (maxScrollTop > 0) {
const startTime = Date.now();
const endTime = startTime + duration;

const scroll = () => {
const currentTime = Date.now();
const timeFraction = (currentTime - startTime) / duration;
const scrollTop = maxScrollTop * timeFraction;

messageContainer.scrollTo({
top: scrollTop,
behavior: 'smooth',
});

if (currentTime < endTime) {
requestAnimationFrame(scroll);
}
};

requestAnimationFrame(scroll);
}
}
}, [messageList, isOpen, apiError]);

const handleClick = () => {
setIsOpen(false);

Expand Down Expand Up @@ -120,10 +84,10 @@ const Sidebar = ({

const getSidebar = () => (
<div
className="h-100 d-flex flex-column justify-content-stretch"
className="d-flex flex-column h-100"
data-testid="sidebar-xpert"
>
<div className="p-3 sidebar-header" data-testid="sidebar-xpert-header">
<div className="sidebar-header" data-testid="sidebar-xpert-header">
<XpertLogo />
</div>
{upgradeable
Expand All @@ -132,11 +96,7 @@ const Sidebar = ({
{getDaysRemainingMessage()}
</div>
)}
<span className="separator" />
<ChatBox
chatboxContainerRef={chatboxContainerRef}
messageList={messageList}
/>
<ChatBox messageList={messageList} />
{
apiError
&& (
Expand All @@ -145,7 +105,9 @@ const Sidebar = ({
</div>
)
}
{getMessageForm()}
<div className="sidebar-footer">
{getMessageForm()}
</div>
</div>
);

Expand All @@ -165,7 +127,7 @@ const Sidebar = ({
data-testid="sidebar"
>
<IconButton
className="chat-close position-absolute m-2 border-0"
className="chat-close position-absolute border-0"
src={Close}
iconAs={Icon}
onClick={handleClick}
Expand Down

0 comments on commit 5cf6c04

Please sign in to comment.