feat: add onboarding step 3 (#1324)

* feat: add OnboardingContext

* feat: add Step3 boilerplate

* feat: activate step3

* feat: add <Step3/> content

* feat: add shouldStream guard on useStreamText
This commit is contained in:
Mamadou DICKO 2023-10-04 16:26:56 +02:00 committed by GitHub
parent 945178de39
commit 85eba3da70
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 180 additions and 46 deletions

View File

@ -16,7 +16,7 @@ export const ActionsBar = (): JSX.Element => {
return ( return (
<> <>
{hasPendingRequests && ( {hasPendingRequests && (
<div className="flex mt-1 flex-col md:flex-row w-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-2 md:p-6 pl-6"> <div className="flex mt-1 flex-col md:flex-row w-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl bg-white dark:bg-black border border-black/10 dark:border-white/25 p-2 md:p-6 pl-6 mb-3">
<div className="flex flex-1 items-center mb-2 md:mb-0"> <div className="flex flex-1 items-center mb-2 md:mb-0">
<span className="text-sm md:text-1xl">{t("feedingBrain")}</span> <span className="text-sm md:text-1xl">{t("feedingBrain")}</span>
</div> </div>

View File

@ -1,3 +1,5 @@
/*eslint max-lines: ["error", 200 ]*/
import { UUID } from "crypto"; import { UUID } from "crypto";
import { useParams, useRouter } from "next/navigation"; import { useParams, useRouter } from "next/navigation";
import { useState } from "react"; import { useState } from "react";
@ -10,6 +12,7 @@ import { useChatContext } from "@/lib/context";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext"; import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { useOnboardingContext } from "@/lib/hooks/useOnboardingContext";
import { FeedItemCrawlType, FeedItemUploadType } from "../../../types"; import { FeedItemCrawlType, FeedItemUploadType } from "../../../types";
@ -23,7 +26,7 @@ export const useFeedBrainInChat = ({
const { t } = useTranslation(["upload"]); const { t } = useTranslation(["upload"]);
const router = useRouter(); const router = useRouter();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext(); const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setCurrentStep } = useOnboardingContext();
const { currentBrainId } = useBrainContext(); const { currentBrainId } = useBrainContext();
const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeToFeedContext(); const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeToFeedContext();
const [hasPendingRequests, setHasPendingRequests] = useState(false); const [hasPendingRequests, setHasPendingRequests] = useState(false);
@ -61,6 +64,7 @@ export const useFeedBrainInChat = ({
return; return;
} }
try { try {
setCurrentStep("UPLOADED");
dispatchHasPendingRequests(); dispatchHasPendingRequests();
setShouldDisplayFeedCard(false); setShouldDisplayFeedCard(false);
setHasPendingRequests(true); setHasPendingRequests(true);

View File

@ -1,16 +1,13 @@
import { useState } from "react";
import { Step1 } from "./components"; import { Step1 } from "./components";
import { Step2 } from "./components/Step2"; import { Step2 } from "./components/Step2";
import { OnboardingState } from "../types"; import { Step3 } from "./components/Step3";
export const Onboarding = (): JSX.Element => { export const Onboarding = (): JSX.Element => {
const [currentStep, setCurrentStep] = useState<OnboardingState>("DOWNLOAD");
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2 mb-3">
<Step1 changeStateTo={setCurrentStep} currentStep={currentStep} /> <Step1 />
<Step2 currentStep={currentStep} /> <Step2 />
<Step3 />
</div> </div>
); );
}; };

View File

@ -4,27 +4,25 @@ import { useTranslation } from "react-i18next";
import { RiDownloadLine } from "react-icons/ri"; import { RiDownloadLine } from "react-icons/ri";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import { OnboardingState } from "@/lib/context/OnboardingContext/types";
import { useOnboardingContext } from "@/lib/hooks/useOnboardingContext";
import { MessageRow } from "../../QADisplay"; import { MessageRow } from "../../QADisplay";
import { OnboardingState } from "../../types";
import { checkIfShouldDisplayStep } from "../helpers/checkIfShouldDisplayStep"; import { checkIfShouldDisplayStep } from "../helpers/checkIfShouldDisplayStep";
import { useStreamText } from "../hooks/useStreamText"; import { useStreamText } from "../hooks/useStreamText";
import { stepsContainerStyle } from "../styles"; import { stepsContainerStyle } from "../styles";
type Step1Props = { const stepId: OnboardingState = "DOWNLOAD";
currentStep: OnboardingState;
changeStateTo: (state: OnboardingState) => void;
};
export const Step1 = ({ export const Step1 = (): JSX.Element => {
currentStep, const { currentStep, setCurrentStep } = useOnboardingContext();
changeStateTo,
}: Step1Props): JSX.Element => {
const shouldStepBeDisplayed = checkIfShouldDisplayStep({ const shouldStepBeDisplayed = checkIfShouldDisplayStep({
currentStep, currentStep,
step: "DOWNLOAD", step: stepId,
}); });
const shouldStreamMessage = currentStep === stepId;
const { t } = useTranslation(["chat"]); const { t } = useTranslation(["chat"]);
const firstMessage = t("onboarding.download_message_1"); const firstMessage = t("onboarding.download_message_1");
const secondMessageStream = t("onboarding.download_message_2"); const secondMessageStream = t("onboarding.download_message_2");
@ -33,11 +31,13 @@ export const Step1 = ({
useStreamText({ useStreamText({
text: firstMessage, text: firstMessage,
enabled: shouldStepBeDisplayed, enabled: shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
}); });
const { streamingText: firstMessageStrem, isDone: isStep1Done } = const { streamingText: firstMessageStrem, isDone: isStep1Done } =
useStreamText({ useStreamText({
text: secondMessageStream, text: secondMessageStream,
enabled: isAssistantDone && shouldStepBeDisplayed, enabled: isAssistantDone && shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
}); });
if (!shouldStepBeDisplayed) { if (!shouldStepBeDisplayed) {
@ -56,7 +56,7 @@ export const Step1 = ({
download download
target="_blank" target="_blank"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
onClick={() => changeStateTo("UPLOAD")} onClick={() => setCurrentStep("UPLOAD")}
> >
<Button className="bg-black p-2 ml-2 rounded-full inline-flex"> <Button className="bg-black p-2 ml-2 rounded-full inline-flex">
<RiDownloadLine /> <RiDownloadLine />

View File

@ -1,35 +1,40 @@
import { Fragment } from "react"; import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { OnboardingState } from "@/lib/context/OnboardingContext/types";
import { useOnboardingContext } from "@/lib/hooks/useOnboardingContext";
import { MessageRow } from "../../QADisplay"; import { MessageRow } from "../../QADisplay";
import { OnboardingState } from "../../types";
import { checkIfShouldDisplayStep } from "../helpers/checkIfShouldDisplayStep"; import { checkIfShouldDisplayStep } from "../helpers/checkIfShouldDisplayStep";
import { useStreamText } from "../hooks/useStreamText"; import { useStreamText } from "../hooks/useStreamText";
import { stepsContainerStyle } from "../styles"; import { stepsContainerStyle } from "../styles";
type Step1Props = { const stepId: OnboardingState = "UPLOAD";
currentStep: OnboardingState;
};
export const Step2 = ({ currentStep }: Step1Props): JSX.Element => { export const Step2 = (): JSX.Element => {
const { currentStep } = useOnboardingContext();
const shouldStepBeDisplayed = checkIfShouldDisplayStep({ const shouldStepBeDisplayed = checkIfShouldDisplayStep({
currentStep, currentStep,
step: "UPLOAD", step: stepId,
}); });
const { t } = useTranslation(["chat"]); const { t } = useTranslation(["chat"]);
const firstMessage = t("onboarding.upload_message_1"); const firstMessage = t("onboarding.upload_message_1");
const secondMessageStream = t("onboarding.upload_message_2"); const secondMessageStream = t("onboarding.upload_message_2");
const shouldStreamMessage = currentStep === stepId;
const { streamingText: streamingAssistantMessage, isDone: isAssistantDone } = const { streamingText: streamingAssistantMessage, isDone: isAssistantDone } =
useStreamText({ useStreamText({
text: firstMessage, text: firstMessage,
enabled: shouldStepBeDisplayed, enabled: shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
}); });
const { streamingText: firstMessageStream } = useStreamText({ const { streamingText: firstMessageStream } = useStreamText({
text: secondMessageStream, text: secondMessageStream,
enabled: isAssistantDone && shouldStepBeDisplayed, enabled: isAssistantDone && shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
}); });
if (!shouldStepBeDisplayed) { if (!shouldStepBeDisplayed) {

View File

@ -0,0 +1,51 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { OnboardingState } from "@/lib/context/OnboardingContext/types";
import { useOnboardingContext } from "@/lib/hooks/useOnboardingContext";
import { MessageRow } from "../../QADisplay";
import { checkIfShouldDisplayStep } from "../helpers/checkIfShouldDisplayStep";
import { useStreamText } from "../hooks/useStreamText";
import { stepsContainerStyle } from "../styles";
const stepId: OnboardingState = "UPLOADED";
export const Step3 = (): JSX.Element => {
const { currentStep } = useOnboardingContext();
const shouldStepBeDisplayed = checkIfShouldDisplayStep({
currentStep,
step: stepId,
});
const { t } = useTranslation(["chat"]);
const firstMessage = t("onboarding.last_step");
const secondMessageStream = t("onboarding.ask_question_to_file");
const shouldStreamMessage = currentStep === stepId;
const { streamingText: streamingAssistantMessage, isDone: isAssistantDone } =
useStreamText({
text: firstMessage,
enabled: shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
});
const { streamingText: firstMessageStream } = useStreamText({
text: secondMessageStream,
enabled: isAssistantDone && shouldStepBeDisplayed,
shouldStream: shouldStreamMessage,
});
if (!shouldStepBeDisplayed) {
return <Fragment />;
}
return (
<MessageRow speaker={"assistant"} brainName={"Quivr"}>
<div className={stepsContainerStyle}>
<p>{streamingAssistantMessage}</p>
<p>{firstMessageStream}</p>
</div>
</MessageRow>
);
};

View File

@ -1,8 +1,9 @@
import { OnboardingState } from "../../types"; import { OnboardingState } from "@/lib/context/OnboardingContext/types";
const onboardingStepToState: Record<OnboardingState, OnboardingState[]> = { const requiredStateForDisplaying: Record<OnboardingState, OnboardingState[]> = {
DOWNLOAD: ["DOWNLOAD", "UPLOAD"], DOWNLOAD: ["DOWNLOAD", "UPLOAD", "UPLOADED"],
UPLOAD: ["UPLOAD"], UPLOAD: ["UPLOAD", "UPLOADED"],
UPLOADED: ["UPLOADED"],
}; };
type CheckIfShouldDisplayStepProps = { type CheckIfShouldDisplayStepProps = {
@ -14,5 +15,5 @@ export const checkIfShouldDisplayStep = ({
currentStep, currentStep,
step, step,
}: CheckIfShouldDisplayStepProps): boolean => { }: CheckIfShouldDisplayStepProps): boolean => {
return onboardingStepToState[step].includes(currentStep); return requiredStateForDisplaying[step].includes(currentStep);
}; };

View File

@ -3,9 +3,14 @@ import { useEffect, useState } from "react";
type UseStreamTextProps = { type UseStreamTextProps = {
text: string; text: string;
enabled?: boolean; enabled?: boolean;
shouldStream?: boolean;
}; };
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useStreamText = ({ text, enabled = true }: UseStreamTextProps) => { export const useStreamText = ({
text,
enabled = true,
shouldStream = true,
}: UseStreamTextProps) => {
const [streamingText, setStreamingText] = useState<string>(""); const [streamingText, setStreamingText] = useState<string>("");
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
@ -18,6 +23,13 @@ export const useStreamText = ({ text, enabled = true }: UseStreamTextProps) => {
return; return;
} }
if (!shouldStream) {
setStreamingText(text);
setCurrentIndex(text.length);
return;
}
const messageInterval = setInterval(() => { const messageInterval = setInterval(() => {
if (currentIndex < text.length) { if (currentIndex < text.length) {
setStreamingText((prevText) => prevText + (text[currentIndex] ?? "")); setStreamingText((prevText) => prevText + (text[currentIndex] ?? ""));
@ -30,7 +42,7 @@ export const useStreamText = ({ text, enabled = true }: UseStreamTextProps) => {
return () => { return () => {
clearInterval(messageInterval); clearInterval(messageInterval);
}; };
}, [text, currentIndex, enabled]); }, [text, currentIndex, enabled, shouldStream]);
return { streamingText, isDone }; return { streamingText, isDone };
}; };

View File

@ -1 +0,0 @@
export type OnboardingState = "DOWNLOAD" | "UPLOAD";

View File

@ -18,7 +18,8 @@ export const useChatsList = () => {
const fetchAllChats = async () => { const fetchAllChats = async () => {
try { try {
const response = await getChats(); const response = await getChats();
setAllChats(response.reverse());
return response.reverse();
} catch (error) { } catch (error) {
console.error(error); console.error(error);
publish({ publish({

View File

@ -3,6 +3,7 @@ import { ReactNode } from "react";
import { ChatProvider, KnowledgeToFeedProvider } from "@/lib/context"; import { ChatProvider, KnowledgeToFeedProvider } from "@/lib/context";
import { ChatsProvider } from "@/lib/context/ChatsProvider/chats-provider"; import { ChatsProvider } from "@/lib/context/ChatsProvider/chats-provider";
import { OnboardingContextProvider } from "@/lib/context/OnboardingContext/knowledgeToFeed-provider";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
@ -23,10 +24,12 @@ const Layout = ({ children }: LayoutProps): JSX.Element => {
<KnowledgeToFeedProvider> <KnowledgeToFeedProvider>
<ChatsProvider> <ChatsProvider>
<ChatProvider> <ChatProvider>
<div className="relative h-full w-full flex justify-stretch items-stretch"> <OnboardingContextProvider>
<ChatsList /> <div className="relative h-full w-full flex justify-stretch items-stretch">
{children} <ChatsList />
</div> {children}
</div>
</OnboardingContextProvider>
</ChatProvider> </ChatProvider>
</ChatsProvider> </ChatsProvider>
</KnowledgeToFeedProvider> </KnowledgeToFeedProvider>

View File

@ -0,0 +1,33 @@
"use client";
import { createContext, useState } from "react";
import { OnboardingState } from "./types";
type OnboardingContextType = {
currentStep: OnboardingState;
setCurrentStep: React.Dispatch<React.SetStateAction<OnboardingState>>;
};
export const OnboardingContext = createContext<
OnboardingContextType | undefined
>(undefined);
export const OnboardingContextProvider = ({
children,
}: {
children: React.ReactNode;
}): JSX.Element => {
const [currentStep, setCurrentStep] = useState<OnboardingState>("DOWNLOAD");
return (
<OnboardingContext.Provider
value={{
currentStep,
setCurrentStep,
}}
>
{children}
</OnboardingContext.Provider>
);
};

View File

@ -0,0 +1 @@
export type OnboardingState = "DOWNLOAD" | "UPLOAD" | "UPLOADED";

View File

@ -0,0 +1,15 @@
import { useContext } from "react";
import { OnboardingContext } from "../context/OnboardingContext/knowledgeToFeed-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useOnboardingContext = () => {
const context = useContext(OnboardingContext);
if (!context) {
throw new Error(
"useOnboardingContext must be used within a OnboardingContextProvider"
);
}
return context;
};

View File

@ -41,6 +41,8 @@
"upload_message_2":"Step 2: Now Drag and drop it on the chat or in the 📎", "upload_message_2":"Step 2: Now Drag and drop it on the chat or in the 📎",
"how_to_use_quivr": "How to user Quivr", "how_to_use_quivr": "How to user Quivr",
"what_is_quivr":"What is Quivr ?", "what_is_quivr":"What is Quivr ?",
"what_is_brain": "What is a brain ?" "what_is_brain": "What is a brain ?",
"last_step":"Last step 🥳",
"ask_question_to_file":"Ask a question to your file. Ex : 'What are you talking about ?'"
} }
} }

View File

@ -41,6 +41,8 @@
"upload_message_2": "Paso 2: Ahora, arrástralo y suéltalo en el chat o en el 📎", "upload_message_2": "Paso 2: Ahora, arrástralo y suéltalo en el chat o en el 📎",
"how_to_use_quivr": "Cómo usar Quivr", "how_to_use_quivr": "Cómo usar Quivr",
"what_is_quivr": "¿Qué es Quivr?", "what_is_quivr": "¿Qué es Quivr?",
"what_is_brain": "¿Qué es un cerebro?" "what_is_brain": "¿Qué es un cerebro?",
"last_step":"Último paso 🥳",
"ask_question_to_file":"Haz una pregunta a tu archivo. Ej: '¿De qué estás hablando?'"
} }
} }

View File

@ -41,6 +41,8 @@
"upload_message_2": "Étape 2 : Maintenant, faites glisser et déposez-le dans le chat ou dans 📎", "upload_message_2": "Étape 2 : Maintenant, faites glisser et déposez-le dans le chat ou dans 📎",
"how_to_use_quivr": "Comment utiliser Quivr", "how_to_use_quivr": "Comment utiliser Quivr",
"what_is_quivr": "Qu'est-ce que Quivr ?", "what_is_quivr": "Qu'est-ce que Quivr ?",
"what_is_brain": "Qu'est-ce qu'un cerveau ?" "what_is_brain": "Qu'est-ce qu'un cerveau ?",
"last_step":"Dernière étape 🥳",
"ask_question_to_file":"Posez une question à votre fichier. Ex : 'De quoi parlez-vous ?'"
} }
} }

View File

@ -41,6 +41,8 @@
"upload_message_2": "Passo 2: Agora, arraste e solte no chat ou na 📎", "upload_message_2": "Passo 2: Agora, arraste e solte no chat ou na 📎",
"how_to_use_quivr": "Como usar o Quivr", "how_to_use_quivr": "Como usar o Quivr",
"what_is_quivr": "O que é o Quivr?", "what_is_quivr": "O que é o Quivr?",
"what_is_brain": "O que é um cérebro?" "what_is_brain": "O que é um cérebro?",
"last_step":"Dernière étape 🥳",
"ask_question_to_file":"Posez une question à votre fichier. Ex : 'De quoi parlez-vous ?'"
} }
} }

View File

@ -41,6 +41,8 @@
"upload_message_2": "Шаг 2: Теперь перетащите его в чат или в 📎", "upload_message_2": "Шаг 2: Теперь перетащите его в чат или в 📎",
"how_to_use_quivr": "Как использовать Quivr", "how_to_use_quivr": "Как использовать Quivr",
"what_is_quivr": "Что такое Quivr?", "what_is_quivr": "Что такое Quivr?",
"what_is_brain": "Что такое мозг?" "what_is_brain": "Что такое мозг?",
"last_step":"Последний шаг 🥳",
"ask_question_to_file":"Задайте вопрос своему файлу. Например: 'О чем вы говорите?'"
} }
} }

View File

@ -42,6 +42,8 @@
"upload_message_2": "第2步现在将其拖放到聊天框或 📎 中", "upload_message_2": "第2步现在将其拖放到聊天框或 📎 中",
"how_to_use_quivr": "如何使用Quivr", "how_to_use_quivr": "如何使用Quivr",
"what_is_quivr": "什么是Quivr", "what_is_quivr": "什么是Quivr",
"what_is_brain": "什么是大脑?" "what_is_brain": "什么是大脑?",
"last_step":"最后一步 🥳",
"ask_question_to_file":"向您的文件提问。例如:'你在谈论什么?'"
} }
} }