feat: add onboarding first step (#1303)

* refactor: split <MessageRow /> into small components

* feat: add onboarding page first step
This commit is contained in:
Mamadou DICKO 2023-10-03 11:25:16 +02:00 committed by GitHub
parent 160588cfae
commit 63e90a317c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 438 additions and 62 deletions

View File

@ -1,3 +1,5 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useChatContext } from "@/lib/context";
import { ChatDialogue } from "./components/ChatDialogue";
@ -11,8 +13,10 @@ export const ChatDialogueArea = (): JSX.Element => {
messages,
notifications
);
const shouldDisplayOnboarding = useFeatureIsOn("onboarding");
const shouldDisplayShortcuts = chatItems.length === 0;
const shouldDisplayShortcuts =
chatItems.length === 0 && !shouldDisplayOnboarding;
if (!shouldDisplayShortcuts) {
return <ChatDialogue chatItems={chatItems} />;

View File

@ -0,0 +1,40 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { RiDownloadLine } from "react-icons/ri";
import Button from "@/lib/components/ui/Button";
import { useStreamText } from "./hooks/useStreamText";
import { MessageRow } from "../QADisplay";
export const Onboarding = (): JSX.Element => {
const { t } = useTranslation(["chat"]);
const assistantMessage = t("onboarding.step_1_message_1");
const step1Text = t("onboarding.step_1_message_2");
const { streamingText: streamingAssistantMessage, isDone: isAssistantDone } =
useStreamText(assistantMessage);
const { streamingText: streamingStep1Text, isDone: isStep1Done } =
useStreamText(step1Text, isAssistantDone);
return (
<MessageRow speaker={"assistant"} brainName={"Quivr"}>
<p>{streamingAssistantMessage}</p>
<div>
{streamingStep1Text}
{isStep1Done && isAssistantDone && (
<Link
href="/documents/doc.pdf"
download
target="_blank"
referrerPolicy="no-referrer"
>
<Button className="bg-black p-2 ml-2 rounded-full inline-flex">
<RiDownloadLine />
</Button>
</Link>
)}
</div>
</MessageRow>
);
};

View File

@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useStreamText = (text: string, enabled = true) => {
const [streamingText, setStreamingText] = useState<string>("");
const [currentIndex, setCurrentIndex] = useState(0);
const isDone = currentIndex === text.length;
useEffect(() => {
if (!enabled) {
setStreamingText("");
return;
}
const messageInterval = setInterval(() => {
if (currentIndex < text.length) {
setStreamingText((prevText) => prevText + (text[currentIndex] ?? ""));
setCurrentIndex((prevIndex) => prevIndex + 1);
} else {
clearInterval(messageInterval);
}
}, 30);
return () => {
clearInterval(messageInterval);
};
}, [text, currentIndex, enabled]);
return { streamingText, isDone };
};

View File

@ -1,49 +1,35 @@
import React, { useState } from "react";
import { FaCheckCircle, FaCopy } from "react-icons/fa";
import ReactMarkdown from "react-markdown";
import { cn } from "@/lib/utils";
import React from "react";
import { CopyButton } from "./components/CopyButton";
import { MessageContent } from "./components/MessageContent";
import { QuestionBrain } from "./components/QuestionBrain";
import { QuestionPrompt } from "./components/QuestionPrompt";
import { useMessageRow } from "./hooks/useMessageRow";
type MessageRowProps = {
speaker: string;
text: string;
speaker: "user" | "assistant";
text?: string;
brainName?: string | null;
promptName?: string | null;
children?: React.ReactNode;
};
export const MessageRow = React.forwardRef(
(
{ speaker, text, brainName, promptName }: MessageRowProps,
{ speaker, text, brainName, promptName, children }: MessageRowProps,
ref: React.Ref<HTMLDivElement>
) => {
const isUserSpeaker = speaker === "user";
const [isCopied, setIsCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(text).then(
() => setIsCopied(true),
(err) => console.error("Failed to copy!", err)
);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
};
const containerClasses = cn(
"py-3 px-5 w-fit",
isUserSpeaker
? "bg-msg-gray bg-opacity-60 items-start"
: "bg-msg-purple bg-opacity-60 items-end",
"dark:bg-gray-800 rounded-3xl flex flex-col overflow-hidden scroll-pb-32"
);
const containerWrapperClasses = cn(
"flex flex-col",
isUserSpeaker ? "items-end" : "items-start"
);
const markdownClasses = cn("prose", "dark:prose-invert");
const {
containerClasses,
containerWrapperClasses,
handleCopy,
isCopied,
isUserSpeaker,
markdownClasses,
} = useMessageRow({
speaker,
text,
});
return (
<div className={containerWrapperClasses}>
@ -53,19 +39,16 @@ export const MessageRow = React.forwardRef(
<QuestionBrain brainName={brainName} />
<QuestionPrompt promptName={promptName} />
</div>
{!isUserSpeaker && (
<button
className="text-gray-500 hover:text-gray-700 transition"
onClick={handleCopy}
title={isCopied ? "Copied!" : "Copy to clipboard"}
>
{isCopied ? <FaCheckCircle /> : <FaCopy />}
</button>
{!isUserSpeaker && text !== undefined && (
<CopyButton handleCopy={handleCopy} isCopied={isCopied} />
)}
</div>
<div data-testid="chat-message-text">
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
</div>
{children ?? (
<MessageContent
text={text ?? ""}
markdownClasses={markdownClasses}
/>
)}
</div>
</div>
);

View File

@ -0,0 +1,19 @@
import { FaCheckCircle, FaCopy } from "react-icons/fa";
type CopyButtonProps = {
handleCopy: () => void;
isCopied: boolean;
};
export const CopyButton = ({
handleCopy,
isCopied,
}: CopyButtonProps): JSX.Element => (
<button
className="text-gray-500 hover:text-gray-700 transition"
onClick={handleCopy}
title={isCopied ? "Copied!" : "Copy to clipboard"}
>
{isCopied ? <FaCheckCircle /> : <FaCopy />}
</button>
);

View File

@ -0,0 +1,13 @@
import ReactMarkdown from "react-markdown";
export const MessageContent = ({
text,
markdownClasses,
}: {
text: string;
markdownClasses: string;
}): JSX.Element => (
<div data-testid="chat-message-text">
<ReactMarkdown className={markdownClasses}>{text}</ReactMarkdown>
</div>
);

View File

@ -0,0 +1,47 @@
import { useState } from "react";
import { cn } from "@/lib/utils";
type UseMessageRowProps = {
speaker: "user" | "assistant";
text?: string;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useMessageRow = ({ speaker, text }: UseMessageRowProps) => {
const isUserSpeaker = speaker === "user";
const [isCopied, setIsCopied] = useState(false);
const handleCopy = () => {
if (text === undefined) {
return;
}
navigator.clipboard.writeText(text).then(
() => setIsCopied(true),
(err) => console.error("Failed to copy!", err)
);
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
};
const containerClasses = cn(
"py-3 px-5 w-fit",
isUserSpeaker ? "bg-msg-gray bg-opacity-60" : "bg-msg-purple bg-opacity-60",
"dark:bg-gray-800 rounded-3xl flex flex-col overflow-hidden scroll-pb-32"
);
const containerWrapperClasses = cn(
"flex flex-col",
isUserSpeaker ? "items-end" : "items-start"
);
const markdownClasses = cn("prose", "dark:prose-invert");
return {
isUserSpeaker,
isCopied,
handleCopy,
containerClasses,
containerWrapperClasses,
markdownClasses,
};
};

View File

@ -1,7 +1,13 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { ChatItem } from "./components";
import { Onboarding } from "./components/Onboarding/Onboarding";
import { useChatDialogue } from "./hooks/useChatDialogue";
import {
chatDialogueContainerClassName,
chatItemContainerClassName,
} from "./styles";
import { getKeyFromChatItem } from "./utils/getKeyFromChatItem";
import { ChatItemWithGroupedNotifications } from "../../types";
@ -15,17 +21,23 @@ export const ChatDialogue = ({
const { t } = useTranslation(["chat"]);
const { chatListRef } = useChatDialogue();
const shouldDisplayOnboarding = useFeatureIsOn("onboarding");
if (shouldDisplayOnboarding) {
return (
<div
style={{
display: "flex",
flexDirection: "column",
flex: 1,
overflowY: "auto",
marginBottom: 10,
}}
ref={chatListRef}
>
<div className={chatDialogueContainerClassName} ref={chatListRef}>
<Onboarding />
<div className={chatItemContainerClassName}>
{chatItems.map((chatItem) => (
<ChatItem key={getKeyFromChatItem(chatItem)} content={chatItem} />
))}
</div>
</div>
);
}
return (
<div className={chatDialogueContainerClassName} ref={chatListRef}>
{chatItems.length === 0 ? (
<div
data-testid="empty-history-message"
@ -34,7 +46,7 @@ export const ChatDialogue = ({
{t("ask", { ns: "chat" })}
</div>
) : (
<div className="flex flex-col gap-3">
<div className={chatItemContainerClassName}>
{chatItems.map((chatItem) => (
<ChatItem key={getKeyFromChatItem(chatItem)} content={chatItem} />
))}

View File

@ -0,0 +1,4 @@
export const chatItemContainerClassName = "flex flex-col gap-3";
export const chatDialogueContainerClassName =
"flex flex-col flex-1 overflow-y-auto mb-10";

View File

@ -0,0 +1,198 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Type /Catalog
/Outlines 2 0 R
/Pages 3 0 R
>>
endobj
2 0 obj
<<
/Type /Outlines
/Count 0
>>
endobj
3 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 6 0 R ]
>>
endobj
4 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 5 0 R
>>
endobj
5 0 obj
<< /Length 1074 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( A Simple PDF File ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( This is a small demonstration .pdf file - ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 628.8480 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 616.8960 Td
( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj
ET
BT
/F1 0010 Tf
69.2500 604.9440 Td
( more text. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 592.9920 Td
( And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 569.0880 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 557.1360 Td
( text. And more text. And more text. Even more. Continued on page 2 ...) Tj
ET
endstream
endobj
6 0 obj
<<
/Type /Page
/Parent 3 0 R
/Resources <<
/Font <<
/F1 9 0 R
>>
/ProcSet 8 0 R
>>
/MediaBox [0 0 612.0000 792.0000]
/Contents 7 0 R
>>
endobj
7 0 obj
<< /Length 676 >>
stream
2 J
BT
0 0 0 rg
/F1 0027 Tf
57.3750 722.2800 Td
( Simple PDF File 2 ) Tj
ET
BT
/F1 0010 Tf
69.2500 688.6080 Td
( ...continued from page 1. Yet more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 676.6560 Td
( And more text. And more text. And more text. And more text. And more ) Tj
ET
BT
/F1 0010 Tf
69.2500 664.7040 Td
( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj
ET
BT
/F1 0010 Tf
69.2500 652.7520 Td
( paint dry. And more text. And more text. And more text. And more text. ) Tj
ET
BT
/F1 0010 Tf
69.2500 640.8000 Td
( Boring. More, a little more text. The end, and just as well. ) Tj
ET
endstream
endobj
8 0 obj
[/PDF /Text]
endobj
9 0 obj
<<
/Type /Font
/Subtype /Type1
/Name /F1
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
>>
endobj
10 0 obj
<<
/Creator (Rave \(http://www.nevrona.com/rave\))
/Producer (Nevrona Designs)
/CreationDate (D:20060301072826)
>>
endobj
xref
0 11
0000000000 65535 f
0000000019 00000 n
0000000093 00000 n
0000000147 00000 n
0000000222 00000 n
0000000390 00000 n
0000001522 00000 n
0000001690 00000 n
0000002423 00000 n
0000002456 00000 n
0000002574 00000 n
trailer
<<
/Size 11
/Root 1 0 R
/Info 10 0 R
>>
startxref
2714
%%EOF

View File

@ -36,5 +36,9 @@
"new_prompt": "Create new prompt",
"feed_brain_placeholder": "Choose which @brain you want to feed with these files",
"feedingBrain": "Your newly added knowledge is being processed, you can keep chatting in the meantime !",
"add_content_card_button_tooltip": "Add knowledge to a brain"
"add_content_card_button_tooltip": "Add knowledge to a brain",
"onboarding":{
"step_1_message_1": "Hi 👋🏻 Want to discover Quivr ? 😇",
"step_1_message_2": "Step 1: Download “Quivr documentation”"
}
}

View File

@ -37,5 +37,9 @@
"new_prompt": "Crear nueva instrucción",
"feed_brain_placeholder" : "Elige cuál @cerebro quieres alimentar con estos archivos",
"feedingBrain": "Su conocimiento recién agregado se está procesando, ¡puede seguir chateando mientras tanto!",
"add_content_card_button_tooltip": "Agregar conocimiento a un cerebro"
"add_content_card_button_tooltip": "Agregar conocimiento a un cerebro",
"onboarding":{
"step_1_message_1": "Hola 👋🏻 ¿Quieres descubrir Quivr? 😇",
"step_1_message_2": "Paso 1: Descargar la documentación de “Quivr”"
}
}

View File

@ -37,5 +37,9 @@
"new_prompt": "Créer un nouveau prompt",
"feed_brain_placeholder" : "Choisissez le @cerveau que vous souhaitez nourrir avec ces fichiers",
"feedingBrain": "Vos nouvelles connaissances sont en cours de traitement. Vous pouvez continuer à discuter en attendant !",
"add_content_card_button_tooltip": "Ajouter des connaissances à un cerveau"
"add_content_card_button_tooltip": "Ajouter des connaissances à un cerveau",
"onboarding":{
"step_1_message_1": "Salut 👋🏻 Envie de découvrir Quivr ? 😇",
"step_1_message_2": "Étape 1 : Téléchargez la documentation de “Quivr”"
}
}

View File

@ -37,5 +37,9 @@
"new_prompt": "Criar novo prompt",
"feed_brain_placeholder" : "Escolha qual @cérebro você deseja alimentar com esses arquivos",
"feedingBrain": "Seu conhecimento recém-adicionado está sendo processado, você pode continuar conversando enquanto isso!",
"add_content_card_button_tooltip": "Adicionar conhecimento a um cérebro"
"add_content_card_button_tooltip": "Adicionar conhecimento a um cérebro",
"onboarding":{
"step_1_message_1": "Oi 👋🏻 Quer descobrir o Quivr? 😇",
"step_1_message_2": "Passo 1: Baixe a documentação do 'Quivr'"
}
}

View File

@ -37,5 +37,9 @@
"new_prompt": "Создать новый запрос",
"feed_brain_placeholder" : "Выберите, какой @мозг вы хотите питать этими файлами",
"feedingBrain": "Ваш недавно добавленный знаний обрабатывается, вы можете продолжить общение в это время!",
"add_content_card_button_tooltip": "Добавить знаний в мозг"
"add_content_card_button_tooltip": "Добавить знаний в мозг",
"onboarding":{
"step_1_message_1": "Привет 👋🏻 Хочешь узнать о Quivr? 😇",
"step_1_message_2": "Шаг 1: Скачайте документацию Quivr"
}
}

View File

@ -37,5 +37,9 @@
"new_prompt": "新提示",
"feed_brain_placeholder" : "选择要用这些文件喂养的 @大脑",
"feedingBrain": "您新添加的知识正在处理中,同时您可以继续聊天!",
"add_content_card_button_tooltip": "向大脑添加知识"
"add_content_card_button_tooltip": "向大脑添加知识",
"onboarding":{
"step_1_message_1": "你好 👋🏻 想要发现 Quivr 吗? 😇",
"step_1_message_2": "步骤 1下载“Quivr 文档”"
}
}