Skip to content

Commit 63e90a3

Browse files
authored
feat: add onboarding first step (QuivrHQ#1303)
* refactor: split <MessageRow /> into small components * feat: add onboarding page first step
1 parent 160588c commit 63e90a3

File tree

16 files changed

+240
-62
lines changed

16 files changed

+240
-62
lines changed

frontend/app/chat/[chatId]/components/ChatDialogueArea/ChatDialogue.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useFeatureIsOn } from "@growthbook/growthbook-react";
2+
13
import { useChatContext } from "@/lib/context";
24

35
import { ChatDialogue } from "./components/ChatDialogue";
@@ -11,8 +13,10 @@ export const ChatDialogueArea = (): JSX.Element => {
1113
messages,
1214
notifications
1315
);
16+
const shouldDisplayOnboarding = useFeatureIsOn("onboarding");
1417

15-
const shouldDisplayShortcuts = chatItems.length === 0;
18+
const shouldDisplayShortcuts =
19+
chatItems.length === 0 && !shouldDisplayOnboarding;
1620

1721
if (!shouldDisplayShortcuts) {
1822
return <ChatDialogue chatItems={chatItems} />;
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Link from "next/link";
2+
import { useTranslation } from "react-i18next";
3+
import { RiDownloadLine } from "react-icons/ri";
4+
5+
import Button from "@/lib/components/ui/Button";
6+
7+
import { useStreamText } from "./hooks/useStreamText";
8+
import { MessageRow } from "../QADisplay";
9+
10+
export const Onboarding = (): JSX.Element => {
11+
const { t } = useTranslation(["chat"]);
12+
const assistantMessage = t("onboarding.step_1_message_1");
13+
const step1Text = t("onboarding.step_1_message_2");
14+
15+
const { streamingText: streamingAssistantMessage, isDone: isAssistantDone } =
16+
useStreamText(assistantMessage);
17+
const { streamingText: streamingStep1Text, isDone: isStep1Done } =
18+
useStreamText(step1Text, isAssistantDone);
19+
20+
return (
21+
<MessageRow speaker={"assistant"} brainName={"Quivr"}>
22+
<p>{streamingAssistantMessage}</p>
23+
<div>
24+
{streamingStep1Text}
25+
{isStep1Done && isAssistantDone && (
26+
<Link
27+
href="/documents/doc.pdf"
28+
download
29+
target="_blank"
30+
referrerPolicy="no-referrer"
31+
>
32+
<Button className="bg-black p-2 ml-2 rounded-full inline-flex">
33+
<RiDownloadLine />
34+
</Button>
35+
</Link>
36+
)}
37+
</div>
38+
</MessageRow>
39+
);
40+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { useEffect, useState } from "react";
2+
3+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
4+
export const useStreamText = (text: string, enabled = true) => {
5+
const [streamingText, setStreamingText] = useState<string>("");
6+
const [currentIndex, setCurrentIndex] = useState(0);
7+
8+
const isDone = currentIndex === text.length;
9+
10+
useEffect(() => {
11+
if (!enabled) {
12+
setStreamingText("");
13+
14+
return;
15+
}
16+
17+
const messageInterval = setInterval(() => {
18+
if (currentIndex < text.length) {
19+
setStreamingText((prevText) => prevText + (text[currentIndex] ?? ""));
20+
setCurrentIndex((prevIndex) => prevIndex + 1);
21+
} else {
22+
clearInterval(messageInterval);
23+
}
24+
}, 30);
25+
26+
return () => {
27+
clearInterval(messageInterval);
28+
};
29+
}, [text, currentIndex, enabled]);
30+
31+
return { streamingText, isDone };
32+
};

frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/components/QADisplay/components/MessageRow/MessageRow.tsx

Lines changed: 27 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,35 @@
1-
import React, { useState } from "react";
2-
import { FaCheckCircle, FaCopy } from "react-icons/fa";
3-
import ReactMarkdown from "react-markdown";
4-
5-
import { cn } from "@/lib/utils";
1+
import React from "react";
62

3+
import { CopyButton } from "./components/CopyButton";
4+
import { MessageContent } from "./components/MessageContent";
75
import { QuestionBrain } from "./components/QuestionBrain";
86
import { QuestionPrompt } from "./components/QuestionPrompt";
7+
import { useMessageRow } from "./hooks/useMessageRow";
98

109
type MessageRowProps = {
11-
speaker: string;
12-
text: string;
10+
speaker: "user" | "assistant";
11+
text?: string;
1312
brainName?: string | null;
1413
promptName?: string | null;
14+
children?: React.ReactNode;
1515
};
1616

1717
export const MessageRow = React.forwardRef(
1818
(
19-
{ speaker, text, brainName, promptName }: MessageRowProps,
19+
{ speaker, text, brainName, promptName, children }: MessageRowProps,
2020
ref: React.Ref<HTMLDivElement>
2121
) => {
22-
const isUserSpeaker = speaker === "user";
23-
const [isCopied, setIsCopied] = useState(false);
24-
25-
const handleCopy = () => {
26-
navigator.clipboard.writeText(text).then(
27-
() => setIsCopied(true),
28-
(err) => console.error("Failed to copy!", err)
29-
);
30-
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
31-
};
32-
33-
const containerClasses = cn(
34-
"py-3 px-5 w-fit",
35-
isUserSpeaker
36-
? "bg-msg-gray bg-opacity-60 items-start"
37-
: "bg-msg-purple bg-opacity-60 items-end",
38-
"dark:bg-gray-800 rounded-3xl flex flex-col overflow-hidden scroll-pb-32"
39-
);
40-
41-
const containerWrapperClasses = cn(
42-
"flex flex-col",
43-
isUserSpeaker ? "items-end" : "items-start"
44-
);
45-
46-
const markdownClasses = cn("prose", "dark:prose-invert");
22+
const {
23+
containerClasses,
24+
containerWrapperClasses,
25+
handleCopy,
26+
isCopied,
27+
isUserSpeaker,
28+
markdownClasses,
29+
} = useMessageRow({
30+
speaker,
31+
text,
32+
});
4733

4834
return (
4935
<div className={containerWrapperClasses}>
@@ -53,19 +39,16 @@ export const MessageRow = React.forwardRef(
5339
<QuestionBrain brainName={brainName} />
5440
<QuestionPrompt promptName={promptName} />
5541
</div>
56-
{!isUserSpeaker && (
57-
<button
58-
className="text-gray-500 hover:text-gray-700 transition"
59-
onClick={handleCopy}
60-
title={isCopied ? "Copied!" : "Copy to clipboard"}
61-
>
62-
{isCopied ? <FaCheckCircle /> : <FaCopy />}
63-
</button>
42+
{!isUserSpeaker && text !== undefined && (
43+
<CopyButton handleCopy={handleCopy} isCopied={isCopied} />
6444
)}
6545
</div>
66-
<div data-testid="chat-message-text">
67-
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
68-
</div>
46+
{children ?? (
47+
<MessageContent
48+
text={text ?? ""}
49+
markdownClasses={markdownClasses}
50+
/>
51+
)}
6952
</div>
7053
</div>
7154
);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { FaCheckCircle, FaCopy } from "react-icons/fa";
2+
3+
type CopyButtonProps = {
4+
handleCopy: () => void;
5+
isCopied: boolean;
6+
};
7+
8+
export const CopyButton = ({
9+
handleCopy,
10+
isCopied,
11+
}: CopyButtonProps): JSX.Element => (
12+
<button
13+
className="text-gray-500 hover:text-gray-700 transition"
14+
onClick={handleCopy}
15+
title={isCopied ? "Copied!" : "Copy to clipboard"}
16+
>
17+
{isCopied ? <FaCheckCircle /> : <FaCopy />}
18+
</button>
19+
);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import ReactMarkdown from "react-markdown";
2+
3+
export const MessageContent = ({
4+
text,
5+
markdownClasses,
6+
}: {
7+
text: string;
8+
markdownClasses: string;
9+
}): JSX.Element => (
10+
<div data-testid="chat-message-text">
11+
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
12+
</div>
13+
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useState } from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
type UseMessageRowProps = {
6+
speaker: "user" | "assistant";
7+
text?: string;
8+
};
9+
10+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
11+
export const useMessageRow = ({ speaker, text }: UseMessageRowProps) => {
12+
const isUserSpeaker = speaker === "user";
13+
const [isCopied, setIsCopied] = useState(false);
14+
15+
const handleCopy = () => {
16+
if (text === undefined) {
17+
return;
18+
}
19+
navigator.clipboard.writeText(text).then(
20+
() => setIsCopied(true),
21+
(err) => console.error("Failed to copy!", err)
22+
);
23+
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
24+
};
25+
26+
const containerClasses = cn(
27+
"py-3 px-5 w-fit",
28+
isUserSpeaker ? "bg-msg-gray bg-opacity-60" : "bg-msg-purple bg-opacity-60",
29+
"dark:bg-gray-800 rounded-3xl flex flex-col overflow-hidden scroll-pb-32"
30+
);
31+
32+
const containerWrapperClasses = cn(
33+
"flex flex-col",
34+
isUserSpeaker ? "items-end" : "items-start"
35+
);
36+
37+
const markdownClasses = cn("prose", "dark:prose-invert");
38+
39+
return {
40+
isUserSpeaker,
41+
isCopied,
42+
handleCopy,
43+
containerClasses,
44+
containerWrapperClasses,
45+
markdownClasses,
46+
};
47+
};

frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/index.tsx

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1+
import { useFeatureIsOn } from "@growthbook/growthbook-react";
12
import { useTranslation } from "react-i18next";
23

34
import { ChatItem } from "./components";
5+
import { Onboarding } from "./components/Onboarding/Onboarding";
46
import { useChatDialogue } from "./hooks/useChatDialogue";
7+
import {
8+
chatDialogueContainerClassName,
9+
chatItemContainerClassName,
10+
} from "./styles";
511
import { getKeyFromChatItem } from "./utils/getKeyFromChatItem";
612
import { ChatItemWithGroupedNotifications } from "../../types";
713

@@ -15,17 +21,23 @@ export const ChatDialogue = ({
1521
const { t } = useTranslation(["chat"]);
1622
const { chatListRef } = useChatDialogue();
1723

24+
const shouldDisplayOnboarding = useFeatureIsOn("onboarding");
25+
26+
if (shouldDisplayOnboarding) {
27+
return (
28+
<div className={chatDialogueContainerClassName} ref={chatListRef}>
29+
<Onboarding />
30+
<div className={chatItemContainerClassName}>
31+
{chatItems.map((chatItem) => (
32+
<ChatItem key={getKeyFromChatItem(chatItem)} content={chatItem} />
33+
))}
34+
</div>
35+
</div>
36+
);
37+
}
38+
1839
return (
19-
<div
20-
style={{
21-
display: "flex",
22-
flexDirection: "column",
23-
flex: 1,
24-
overflowY: "auto",
25-
marginBottom: 10,
26-
}}
27-
ref={chatListRef}
28-
>
40+
<div className={chatDialogueContainerClassName} ref={chatListRef}>
2941
{chatItems.length === 0 ? (
3042
<div
3143
data-testid="empty-history-message"
@@ -34,7 +46,7 @@ export const ChatDialogue = ({
3446
{t("ask", { ns: "chat" })}
3547
</div>
3648
) : (
37-
<div className="flex flex-col gap-3">
49+
<div className={chatItemContainerClassName}>
3850
{chatItems.map((chatItem) => (
3951
<ChatItem key={getKeyFromChatItem(chatItem)} content={chatItem} />
4052
))}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const chatItemContainerClassName = "flex flex-col gap-3";
2+
3+
export const chatDialogueContainerClassName =
4+
"flex flex-col flex-1 overflow-y-auto mb-10";

frontend/public/documents/doc.pdf

2.96 KB
Binary file not shown.

0 commit comments

Comments
 (0)