Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DOCSP-32954] Add ErrorBanner, React-Transition-Group #182

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions chat-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,13 @@
"@leafygreen-ui/toggle": "^10.0.15",
"@leafygreen-ui/typography": "^16.5.4",
"@lg-chat/avatar": "^2.0.6",
"@lg-chat/input-bar": "^3.1.3",
"@lg-chat/message": "^2.0.8",
"@lg-chat/message-feed": "^2.0.7",
"@lg-chat/chat-disclaimer": "^2.0.0",
"@lg-chat/chat-window": "^1.0.4",
"@lg-chat/fixed-chat-window": "^1.1.1",
"@lg-chat/input-bar": "^3.1.3",
"@lg-chat/leafygreen-chat-provider": "^1.0.2",
"@lg-chat/message": "^2.0.8",
"@lg-chat/message-feed": "^2.0.7",
"@lg-chat/message-prompts": "^1.0.2",
"@lg-chat/message-rating": "^1.1.3",
"@microsoft/fetch-event-source": "^2.0.1",
Expand All @@ -106,6 +106,7 @@
"esbuild": "^0.17.19",
"prettier": "^2.8.8",
"react-markdown": "^8.0.7",
"react-transition-group": "^4.4.5",
"stream-browserify": "^3.0.0"
},
"devDependencies": {
Expand Down
112 changes: 97 additions & 15 deletions chat-ui/src/DevCenterChatbot.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { css } from "@emotion/css";
import Banner from "@leafygreen-ui/banner";
import LeafyGreenProvider, {
useDarkMode,
} from "@leafygreen-ui/leafygreen-provider";
Expand All @@ -17,9 +18,13 @@ import {
import { LeafyGreenChatProvider } from "@lg-chat/leafygreen-chat-provider";
import { Message as LGMessage, MessageSourceType } from "@lg-chat/message";
import { MessageFeed } from "@lg-chat/message-feed";
import { MessagePrompt, MessagePrompts } from "@lg-chat/message-prompts";
import {
MessagePrompts as LGMessagePrompts,
MessagePrompt,
} from "@lg-chat/message-prompts";
import { MessageRatingProps } from "@lg-chat/message-rating";
import { Fragment, useEffect, useState } from "react";
import { Fragment, useEffect, useRef, useState } from "react";
import { Transition } from "react-transition-group";
import { CharacterCount } from "./InputBar";
import { UserProvider } from "./UserProvider";
import { MessageData } from "./services/conversations";
Expand Down Expand Up @@ -337,7 +342,8 @@ function ChatbotModal({
<InputBar
inputBarValue={inputBarValue}
onSubmit={() => handleSubmit(inputBarValue)}
hasError={promptIsTooLong()}
inputBarHasError={promptIsTooLong()}
conversationError={conversation.error}
disabled={!!conversation.error}
disableSend={awaitingReply || promptIsTooLong()}
textareaProps={{
Expand All @@ -361,20 +367,35 @@ function ChatbotModal({
const MAX_INPUT_CHARACTERS = 300;
interface InputBarProps extends LGInputBarProps {
inputBarValue: string;
hasError: boolean;
inputBarHasError: boolean;
conversationError: string | undefined;
}

const InputBar = (props: InputBarProps) => {
const { inputBarValue, hasError, ...LGInputBarProps } = props;
const {
inputBarValue,
inputBarHasError,
conversationError,
...LGInputBarProps
} = props;
const { darkMode } = useDarkMode();

if (conversationError)
return (
<div className={styles.chatbot_input_area}>
<ErrorBanner darkMode={darkMode} message={conversationError} />
</div>
);
Comment on lines +383 to +388
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think conversation errors are the concern of the input bar, especially given that we use it here to short circuit rendering. In the docs Chatbot component we conditionally render the InputBar based on conversation error state rather than have the InputBar handle the conditional internally.

Could we refactor here to use that pattern instead? It'll make it easier to combine this all into shared components.


return (
<div className={styles.chatbot_input_area}>
<LGInputBar
className={
hasError ?? false ? styles.chatbot_input_error_border : undefined
inputBarHasError ?? false
? styles.chatbot_input_error_border
: undefined
}
shouldRenderGradient={!hasError}
shouldRenderGradient={!inputBarHasError}
{...LGInputBarProps}
/>
<div
Expand All @@ -397,6 +418,24 @@ const InputBar = (props: InputBarProps) => {
);
};

type ErrorBannerProps = {
message?: string;
darkMode?: boolean;
};

function ErrorBanner({
message = "Something went wrong.",
darkMode = false,
}: ErrorBannerProps) {
return (
<Banner darkMode={darkMode} variant="danger">
{message}
<br />
Reload the page to start a new conversation.
</Banner>
);
}

const DisclaimerText = () => {
return (
<LGDisclaimerText
Expand Down Expand Up @@ -443,7 +482,6 @@ const Message = ({
renderRating,
conversation,
}: MessageProp) => {
const [suggestedPromptIdx, setSuggestedPromptIdx] = useState(-1);
const user = useUser();

return (
Expand Down Expand Up @@ -526,26 +564,70 @@ const Message = ({
: messageData.content}
</LGMessage>
{displaySuggestedPrompts && (
<div className={styles.message_prompts}>
<MessagePrompts label="Suggested Prompts">
{suggestedPrompts.map((sp, idx) => (
<MessagePrompts
messagePrompts={suggestedPrompts}
messagePromptsOnClick={suggestedPromptOnClick}
/>
)}
</Fragment>
);
};

type MessagePromptsProps = {
messagePrompts: string[];
messagePromptsOnClick: (prompt: string) => void;
};

const MessagePrompts = ({
messagePrompts,
messagePromptsOnClick,
}: MessagePromptsProps) => {
const [inProp, setInProp] = useState(true);
const [suggestedPromptIdx, setSuggestedPromptIdx] = useState(-1);
const nodeRef = useRef(null);

const duration = 300;

const defaultStyle = {
transition: `opacity ${duration}ms ease-in`,
opacity: 0,
};

const transitionStyles = {
entering: { opacity: 1 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
unmounted: { opacity: 0 },
};
Comment on lines +591 to +602
Copy link
Collaborator

@nlarew nlarew Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use emotion for this instead of the jsx style prop. Also at this point we're not actually using any of these states so RTG isn't adding anything until we wire it up.

Since we're only doing CSS transitions right now I think it'd be easier to switch to CSSTransition - then we don't have to manually process the state. We just write the CSS we want for each state and it's wired up automatically.

You should be able to put all this in the message_prompts css (note the different state names compared to regular Transition):

  message_prompts: css`
    margin-left: 70px;
    @media screen and (max-width: 804px) {
      margin-left: 50px;
    }

    transition: opacity ${duration}ms ease-in;

    &-enter {
      opacity: 0;
    }
    &-enter-active {
      opacity: 1;
    }
    &-exit {
      opacity: 1;
    }
    &-exit-active {
      opacity: 0;
    }
  `,

then you'll replace Transition with CSSTransition, add the prop classNames={styles.message_prompts}, and then add ref={nodeRef} to the div that has the message_prompts style.


return (
<Transition in={inProp} timeout={duration} nodeRef={nodeRef}>
{(state) => (
<div
className={styles.message_prompts}
style={{ ...defaultStyle, ...transitionStyles[state] }}
>
Comment on lines +605 to +610
Copy link
Collaborator

@nlarew nlarew Sep 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per my earlier comment about using emotion & CSSTransition. Also make sure to import CSSTransition and delete the closing }) too!

Suggested change
<Transition in={inProp} timeout={duration} nodeRef={nodeRef}>
{(state) => (
<div
className={styles.message_prompts}
style={{ ...defaultStyle, ...transitionStyles[state] }}
>
<CSSTransition
in={inProp}
timeout={duration}
nodeRef={nodeRef}
classNames={styles.message_prompts}
>
<div
className={styles.message_prompts}
ref={nodeRef}
>

<LGMessagePrompts label="Suggested Prompts">
{messagePrompts.map((sp, idx) => (
<MessagePrompt
key={idx}
onClick={() => {
setSuggestedPromptIdx(idx);
setInProp(false);
setTimeout(() => {
suggestedPromptOnClick(suggestedPrompts[idx]);
}, 300);
messagePromptsOnClick(messagePrompts[idx]);
}, duration);
}}
selected={idx === suggestedPromptIdx}
>
{sp}
</MessagePrompt>
))}
</MessagePrompts>
</LGMessagePrompts>
</div>
)}
</Fragment>
</Transition>
);
};

Expand Down
4 changes: 3 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.