Skip to content

Commit

Permalink
Add exampleQuestions prop. Adjust default empty state design. Refacto…
Browse files Browse the repository at this point in the history
…r tests.
  • Loading branch information
cjcenizal committed Apr 19, 2024
1 parent c22a920 commit 086d1e6
Show file tree
Hide file tree
Showing 26 changed files with 577 additions and 45 deletions.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,13 @@ Configure the title in the header of the chatbot window.

Configure the placeholder text in the chatbot's input.

##### `exampleQuestions` (optional)

Configure the example questions that will be suggested to the user when there are no messages to display.

##### `emptyStateDisplay` (optional)

Configure JSX content to render in the messages window when there are no messages to display.
Configure JSX content to render in the messages window when there are no messages to display. When configured, this will supersede `exampleQuestions`.

##### `isInitiallyOpen` (optional)

Expand Down
2 changes: 1 addition & 1 deletion docs/package-lock.json

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

14 changes: 12 additions & 2 deletions docs/src/components/ConfigurationDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Props = {
onUpdateIsStreamingEnabled: (isStreamingEnabled: boolean) => void;
language: SummaryLanguage;
onUpdateLanguage: (language: SummaryLanguage) => void;
exampleQuestions: string;
onUpdateExampleQuestions: (event: React.ChangeEvent<HTMLInputElement>) => void;
};

export const ConfigurationDrawer = ({
Expand All @@ -61,7 +63,9 @@ export const ConfigurationDrawer = ({
isStreamingEnabled,
onUpdateIsStreamingEnabled,
language,
onUpdateLanguage
onUpdateLanguage,
exampleQuestions,
onUpdateExampleQuestions
}: Props) => {
return (
<VuiDrawer
Expand Down Expand Up @@ -126,6 +130,12 @@ export const ConfigurationDrawer = ({

<VuiSpacer size="m" />

<VuiFormGroup label="Example questions" labelFor="exampleQuestions">
<VuiTextInput value={exampleQuestions} onChange={onUpdateExampleQuestions} fullWidth />
</VuiFormGroup>

<VuiSpacer size="m" />

<VuiLabel>Enable Streaming</VuiLabel>

<VuiSpacer size="xs" />
Expand All @@ -143,7 +153,7 @@ export const ConfigurationDrawer = ({
<VuiSelect
value={language}
onChange={(evt) => {
onUpdateLanguage(evt.target.value);
onUpdateLanguage(evt.target.value as SummaryLanguage);
}}
options={SUMMARY_LANGUAGES.filter((lang) => lang !== "auto").map((lang) => ({
text: lang,
Expand Down
25 changes: 23 additions & 2 deletions docs/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const generateCodeSnippet = (
inputSize?: string,
emptyStateDisplay?: string,
isStreamingEnabled?: boolean,
language?: SummaryLanguage
language?: SummaryLanguage,
exampleQuestions?: string
) => {
const props = [
`customerId="${customerId === "" ? "<Your Vectara customer ID>" : customerId}"`,
Expand All @@ -58,6 +59,15 @@ const generateCodeSnippet = (
props.push(`placeholder=${formatStringProp(placeholder)}`);
}

if (exampleQuestions) {
props.push(
`exampleQuestions={[${exampleQuestions
.split(",")
.map((value) => formatStringProp(value.trim()))
.join(", ")}]}`
);
}

if (inputSize) {
props.push(`inputSize="${inputSize}"`);
}
Expand Down Expand Up @@ -102,6 +112,7 @@ const App = () => {
const [isStreamingEnabled, setIsStreamingEnabled] = useState<boolean>(true);
const [language, setLanguage] = useState<SummaryLanguage>("eng");
const [emptyStateJsx, setEmptyStateJsx] = useState<string>("");
const [exampleQuestions, setExampleQuestions] = useState<string>("What is Vectara?, How does RAG work?");

const onUpdateCorpusIds = useCallback((e: ChangeEvent<HTMLInputElement>) => {
const sanitizedValue = e.target.value.trim();
Expand Down Expand Up @@ -133,13 +144,19 @@ const App = () => {
setEmptyStateJsx(e.target.value);
}, []);

const onUpdateExampleQuestions = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setExampleQuestions(e.target.value);
}, []);

const CustomEmptyStateDisplay = useCallback(() => {
return (
// @ts-ignore
<JsxParser jsx={emptyStateJsx} />
);
}, [emptyStateJsx]);

const parsedExampleQuestions = exampleQuestions && exampleQuestions.split(",");

return (
<>
<VuiAppHeader
Expand Down Expand Up @@ -196,6 +213,7 @@ const App = () => {
title={title === "" ? undefined : title}
placeholder={placeholder}
inputSize={inputSize}
exampleQuestions={parsedExampleQuestions}
emptyStateDisplay={emptyStateJsx === "" ? undefined : <CustomEmptyStateDisplay />}
isInitiallyOpen={isChatbotForcedOpen}
zIndex={9}
Expand Down Expand Up @@ -238,7 +256,8 @@ const App = () => {
inputSize,
emptyStateJsx,
isStreamingEnabled,
language
language,
exampleQuestions
)}
</VuiCode>
<VuiSpacer size="xxl" />
Expand Down Expand Up @@ -327,6 +346,8 @@ export const App = () => {
onUpdateIsStreamingEnabled={setIsStreamingEnabled}
language={language}
onUpdateLanguage={setLanguage}
exampleQuestions={exampleQuestions}
onUpdateExampleQuestions={onUpdateExampleQuestions}
/>
</div>
</VuiAppContent>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

38 changes: 16 additions & 22 deletions src/components/ChatView.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,18 @@
import { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { VuiButtonSecondary, VuiFlexContainer, VuiFlexItem, VuiSpacer } from "../vui";
import { Fragment, ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { VuiButtonSecondary, VuiContextProvider, VuiFlexContainer, VuiFlexItem, VuiSpacer } from "../vui";
import { QueryInput } from "./QueryInput";
import { ChatItem } from "./ChatItem";
import { useChat } from "../useChat";
import { Loader } from "./Loader";
import { ChatBubbleIcon, MinimizeIcon } from "./Icons";
import { MinimizeIcon } from "./Icons";
import { SummaryLanguage } from "types";
import { ExampleQuestions } from "./exampleQuestions/ExampleQuestions";

const inputSizeToQueryInputSize = {
large: "l",
medium: "m"
} as const;

const DefaultEmptyMessagesState = () => (
<VuiFlexContainer
className="vrcbEmptyMessages"
spacing="none"
alignItems="center"
justifyContent="center"
direction="column"
>
<ChatBubbleIcon size="150px" color="#000000" />
Ask anything.
</VuiFlexContainer>
);

interface Props {
customerId: string;
corpusIds: string[];
Expand All @@ -33,6 +21,7 @@ interface Props {
language: SummaryLanguage;
title?: string;
placeholder?: string;
exampleQuestions?: string[];
inputSize?: "large" | "medium";
emptyStateDisplay?: ReactNode;
isInitiallyOpen?: boolean;
Expand All @@ -50,8 +39,9 @@ export const ChatView = ({
apiKey,
title = "My Chatbot",
placeholder = "Chat with your AI Assistant",
exampleQuestions,
inputSize = "large",
emptyStateDisplay = <DefaultEmptyMessagesState />,
emptyStateDisplay,
isInitiallyOpen,
zIndex = 9999,
enableStreaming,
Expand Down Expand Up @@ -124,17 +114,17 @@ export const ChatView = ({
const hasContent = isLoading || messageHistory.length > 0 || activeMessage;
const isRequestDisabled = isLoading || isStreamingResponse || query.trim().length === 0;

const onSendQuery = () => {
if (isRequestDisabled) return;
sendMessage({ query });
const onSendQuery = (queryOverride?: string) => {
if (isRequestDisabled && !queryOverride) return;
sendMessage({ query: queryOverride ?? query });
setQuery("");
};

const spacer = historyItems.length === 0 ? null : <VuiSpacer size={activeMessage ? "m" : "l"} />;

useEffect(updateScrollPosition, [isLoading, activeMessage]);

return isOpen ? (
const content = isOpen ? (
<div className="vrcbChatbotWrapper" style={{ zIndex }}>
<VuiFlexContainer className="vrcbHeader" spacing="none" direction="row">
<VuiFlexItem grow={1} alignItems="center">
Expand All @@ -152,7 +142,9 @@ export const ChatView = ({
<VuiFlexItem className="vrcbMessagesWrapper" basis="fill">
<div ref={appLayoutRef}>
{!hasContent ? (
emptyStateDisplay
emptyStateDisplay ?? (
<ExampleQuestions exampleQuestions={exampleQuestions ?? []} onSubmitChat={onSendQuery} />
)
) : (
<>
<VuiSpacer size="xs" />
Expand Down Expand Up @@ -204,4 +196,6 @@ export const ChatView = ({
{title}
</button>
);

return <VuiContextProvider>{content}</VuiContextProvider>;
};
3 changes: 0 additions & 3 deletions src/components/chatView.scss
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,7 @@ $chatbotPosition: $sizeS;
}

.vrcbEmptyMessages {
color: $colorFullShade;
font-weight: $fontWeightBold;
height: 100%;
opacity: 0.3;
}

.vrcbRetryButton {
Expand Down
20 changes: 20 additions & 0 deletions src/components/exampleQuestions/DefaultEmptyPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { VuiFlexContainer, VuiText, VuiTextColor } from "vui";
import { ChatBubbleIcon } from "../Icons";

export const DefaultEmptyPrompt = () => (
<VuiFlexContainer
className="vrcbEmptyMessages"
spacing="none"
alignItems="center"
justifyContent="center"
direction="column"
>
<ChatBubbleIcon size="80px" color="#cbcdde" />

<VuiText>
<p>
<VuiTextColor color="subdued">Ask anything.</VuiTextColor>
</p>
</VuiText>
</VuiFlexContainer>
);
33 changes: 33 additions & 0 deletions src/components/exampleQuestions/ExampleQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { VuiSpacer, VuiTextColor, VuiTitle, VuiTopicButton } from "../../vui";

type Props = {
children?: React.ReactNode;
onClick: () => void;
title?: string;
};

export const ExampleQuestion = ({ children, onClick, title }: Props) => {
const content = (
<>
{title && (
<>
<VuiTitle size="s">
<p>
<VuiTextColor color="primary">{title}</VuiTextColor>
</p>
</VuiTitle>

{children && <VuiSpacer size="xxs" />}
</>
)}

{children}
</>
);

return (
<VuiTopicButton onClick={onClick} data-testid="exampleQuestion">
{content}
</VuiTopicButton>
);
};
35 changes: 35 additions & 0 deletions src/components/exampleQuestions/ExampleQuestions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { VuiGrid, VuiSpacer, VuiText, VuiTextColor } from "../../vui";
import { ExampleQuestion } from "./ExampleQuestion";
import { DefaultEmptyPrompt } from "./DefaultEmptyPrompt";

type Props = {
exampleQuestions: string[];
onSubmitChat: (query: string) => void;
};

export const ExampleQuestions = ({ exampleQuestions, onSubmitChat }: Props) => {
const hasExampleQuestions = exampleQuestions.length > 0;
if (!hasExampleQuestions) return <DefaultEmptyPrompt />;

return (
<div className="vrcbExampleQuestionsContainer">
<VuiText>
<p>
<VuiTextColor color="subdued">Try out these example questions</VuiTextColor>
</p>
</VuiText>

<VuiSpacer size="m" />

<VuiGrid columns={3}>
{exampleQuestions.map((exampleQuestion) => (
<ExampleQuestion
key={exampleQuestion}
onClick={() => onSubmitChat(exampleQuestion)}
title={exampleQuestion}
/>
))}
</VuiGrid>
</div>
);
};
9 changes: 9 additions & 0 deletions src/components/exampleQuestions/exampleQuestions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@import "../../vui/styleUtils/index.scss";

.vrcbExampleQuestionsContainer {
padding: $sizeM;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
1 change: 1 addition & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

@import "./components/chatView.scss";
@import "./components/loader.scss";
@import "./components/exampleQuestions/exampleQuestions.scss";
@import "./vui/_index.scss";
Loading

0 comments on commit 086d1e6

Please sign in to comment.