mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-23 12:26:03 +03:00
feat: track onboarding events (#1388)
https://github.com/StanGirard/quivr/issues/1386
This commit is contained in:
parent
5503e104d4
commit
b9172b7442
@ -15,7 +15,7 @@ def update_user_onboarding(
|
||||
supabase_db = get_supabase_db()
|
||||
updated_onboarding = supabase_db.update_user_onboarding(user_id, onboarding)
|
||||
|
||||
if all(not value for value in onboarding.dict().values()):
|
||||
if all(not value for value in updated_onboarding.dict().values()):
|
||||
supabase_db.remove_user_onboarding(user_id)
|
||||
|
||||
return updated_onboarding
|
||||
|
@ -2,13 +2,11 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { useKnowledgeToFeed } from "./useKnowledgeToFeed";
|
||||
import { useFeedBrainHandler } from "./useFeedBrainHandler";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useFeedBrain = ({
|
||||
@ -19,17 +17,13 @@ export const useFeedBrain = ({
|
||||
closeFeedInput?: () => void;
|
||||
}) => {
|
||||
const { publish } = useToast();
|
||||
const { files, urls } = useKnowledgeToFeed();
|
||||
const { t } = useTranslation(["upload"]);
|
||||
const { updateOnboarding, onboarding } = useOnboarding();
|
||||
const { currentBrainId } = useBrainContext();
|
||||
const { setKnowledgeToFeed, knowledgeToFeed } = useKnowledgeToFeedContext();
|
||||
const [hasPendingRequests, setHasPendingRequests] = useState(false);
|
||||
|
||||
const { handleFeedBrain } = useFeedBrainHandler();
|
||||
const { createChat, deleteChat } = useChatApi();
|
||||
|
||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||
|
||||
const feedBrain = async (): Promise<void> => {
|
||||
if (currentBrainId === null) {
|
||||
publish({
|
||||
@ -56,26 +50,10 @@ export const useFeedBrain = ({
|
||||
dispatchHasPendingRequests?.();
|
||||
closeFeedInput?.();
|
||||
setHasPendingRequests(true);
|
||||
const uploadPromises = files.map((file) =>
|
||||
uploadFileHandler(file, currentBrainId, currentChatId)
|
||||
);
|
||||
const crawlPromises = urls.map((url) =>
|
||||
crawlWebsiteHandler(url, currentBrainId, currentChatId)
|
||||
);
|
||||
|
||||
const updateOnboardingPromise = async () => {
|
||||
if (onboarding.onboarding_a) {
|
||||
await updateOnboarding({
|
||||
onboarding_a: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
...uploadPromises,
|
||||
...crawlPromises,
|
||||
updateOnboardingPromise(),
|
||||
]);
|
||||
await handleFeedBrain({
|
||||
brainId: currentBrainId,
|
||||
chatId: currentChatId,
|
||||
});
|
||||
|
||||
setKnowledgeToFeed([]);
|
||||
} catch (e) {
|
||||
|
@ -0,0 +1,47 @@
|
||||
import { UUID } from "crypto";
|
||||
|
||||
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { useKnowledgeToFeed } from "./useKnowledgeToFeed";
|
||||
|
||||
type FeedBrainProps = {
|
||||
brainId: UUID;
|
||||
chatId: UUID;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useFeedBrainHandler = () => {
|
||||
const { files, urls } = useKnowledgeToFeed();
|
||||
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
|
||||
const { updateOnboarding, onboarding } = useOnboarding();
|
||||
|
||||
const updateOnboardingA = async () => {
|
||||
if (onboarding.onboarding_a) {
|
||||
await updateOnboarding({
|
||||
onboarding_a: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFeedBrain = async ({
|
||||
brainId,
|
||||
chatId,
|
||||
}: FeedBrainProps): Promise<void> => {
|
||||
const uploadPromises = files.map((file) =>
|
||||
uploadFileHandler(file, brainId, chatId)
|
||||
);
|
||||
const crawlPromises = urls.map((url) =>
|
||||
crawlWebsiteHandler(url, brainId, chatId)
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
...uploadPromises,
|
||||
...crawlPromises,
|
||||
updateOnboardingA(),
|
||||
]);
|
||||
};
|
||||
|
||||
return {
|
||||
handleFeedBrain,
|
||||
};
|
||||
};
|
@ -2,6 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useChat } from "@/app/chat/[chatId]/hooks/useChat";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
|
||||
import { QuestionId } from "../../../types";
|
||||
import { questionIdToTradPath } from "../utils";
|
||||
@ -10,6 +11,7 @@ import { questionIdToTradPath } from "../utils";
|
||||
export const useOnboardingQuestion = (questionId: QuestionId) => {
|
||||
const { updateOnboarding } = useOnboarding();
|
||||
const { t } = useTranslation("chat");
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
|
||||
const onboardingStep = questionIdToTradPath[questionId];
|
||||
|
||||
@ -18,6 +20,7 @@ export const useOnboardingQuestion = (questionId: QuestionId) => {
|
||||
const { addQuestion } = useChat();
|
||||
|
||||
const handleSuggestionClick = async () => {
|
||||
trackOnboardingEvent(onboardingStep);
|
||||
await Promise.all([
|
||||
addQuestion(question),
|
||||
updateOnboarding({ [questionId]: false }),
|
||||
|
@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { RiDownloadLine } from "react-icons/ri";
|
||||
|
||||
import Button from "@/lib/components/ui/Button";
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
|
||||
import { useStreamText } from "./hooks/useStreamText";
|
||||
import { stepsContainerStyle } from "./styles";
|
||||
@ -20,6 +21,8 @@ export const Onboarding = (): JSX.Element => {
|
||||
|
||||
const shouldStepBeDisplayed = useFeatureIsOn("onboarding");
|
||||
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
|
||||
const { streamingText: titleStream, isDone: isTitleDisplayed } =
|
||||
useStreamText({
|
||||
text: title,
|
||||
@ -36,12 +39,11 @@ export const Onboarding = (): JSX.Element => {
|
||||
enabled: isStep1Done,
|
||||
});
|
||||
|
||||
const { streamingText: secondStepStrem, isDone: isStep2Done } = useStreamText(
|
||||
{
|
||||
const { streamingText: secondStepStream, isDone: isStep2Done } =
|
||||
useStreamText({
|
||||
text: step2,
|
||||
enabled: isStep1DetailsDone,
|
||||
}
|
||||
);
|
||||
});
|
||||
const { streamingText: thirdStepStream } = useStreamText({
|
||||
text: step3,
|
||||
enabled: isStep2Done,
|
||||
@ -62,10 +64,13 @@ export const Onboarding = (): JSX.Element => {
|
||||
{firstStepDetailsStream}
|
||||
{isStep1DetailsDone && (
|
||||
<Link
|
||||
href="/documents/doc.pdf"
|
||||
href="/documents/quivr_documentation.pdf"
|
||||
download
|
||||
target="_blank"
|
||||
referrerPolicy="no-referrer"
|
||||
onClick={() => {
|
||||
trackOnboardingEvent("QUIVR_DOCUMENTATION_DOWNLOADED");
|
||||
}}
|
||||
>
|
||||
<Button className="bg-black p-2 ml-2 rounded-full inline-flex">
|
||||
<RiDownloadLine />
|
||||
@ -74,7 +79,7 @@ export const Onboarding = (): JSX.Element => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p>{secondStepStrem}</p>
|
||||
<p>{secondStepStream}</p>
|
||||
<p>{thirdStepStream}</p>
|
||||
</div>
|
||||
</MessageRow>
|
||||
|
@ -11,6 +11,8 @@ import { useChatApi } from "@/lib/api/chat/useChatApi";
|
||||
import { useChatContext } from "@/lib/context";
|
||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
import { useQuestion } from "./useQuestion";
|
||||
@ -25,6 +27,8 @@ export const useChat = () => {
|
||||
const [chatId, setChatId] = useState<string | undefined>(
|
||||
params?.chatId as string | undefined
|
||||
);
|
||||
const { isOnboarding } = useOnboarding();
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
const [generatingAnswer, setGeneratingAnswer] = useState(false);
|
||||
const router = useRouter();
|
||||
const { messages } = useChatContext();
|
||||
@ -64,10 +68,17 @@ export const useChat = () => {
|
||||
});
|
||||
}
|
||||
|
||||
void track("QUESTION_ASKED", {
|
||||
brainId: currentBrainId,
|
||||
promptId: currentPromptId,
|
||||
});
|
||||
if (isOnboarding) {
|
||||
void trackOnboardingEvent("QUESTION_ASKED", {
|
||||
brainId: currentBrainId,
|
||||
promptId: currentPromptId,
|
||||
});
|
||||
} else {
|
||||
void track("QUESTION_ASKED", {
|
||||
brainId: currentBrainId,
|
||||
promptId: currentPromptId,
|
||||
});
|
||||
}
|
||||
|
||||
const chatConfig = getChatsConfigFromLocalStorage();
|
||||
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { ChatsListItem } from "./ChatsListItem";
|
||||
|
||||
export const WelcomeChat = (): JSX.Element => {
|
||||
const { t } = useTranslation("chat");
|
||||
const chat: ChatEntity = {
|
||||
chat_name: t("welcome"),
|
||||
// @ts-expect-error because we don't need to pass all the props
|
||||
chat_id: "",
|
||||
};
|
||||
const { updateOnboarding } = useOnboarding();
|
||||
|
||||
return (
|
||||
<ChatsListItem
|
||||
onDelete={() => void updateOnboarding({ onboarding_a: false })}
|
||||
editable={false}
|
||||
chat={chat}
|
||||
/>
|
||||
);
|
||||
};
|
@ -0,0 +1,19 @@
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
|
||||
import { useWelcomeChat } from "./hooks/useWelcomeChat";
|
||||
import { ChatsListItem } from "../ChatsListItem";
|
||||
|
||||
export const WelcomeChat = (): JSX.Element => {
|
||||
const { chat, handleWelcomeChatDelete } = useWelcomeChat();
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
|
||||
return (
|
||||
<div onClick={() => trackOnboardingEvent("WELCOME_CHAT_CLICKED")}>
|
||||
<ChatsListItem
|
||||
onDelete={() => void handleWelcomeChatDelete()}
|
||||
editable={false}
|
||||
chat={chat}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ChatEntity } from "@/app/chat/[chatId]/types";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useWelcomeChat = () => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { updateOnboarding } = useOnboarding();
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
|
||||
const chat: ChatEntity = {
|
||||
chat_name: t("welcome"),
|
||||
// @ts-expect-error because we don't need to pass all the props
|
||||
chat_id: "",
|
||||
};
|
||||
|
||||
const handleWelcomeChatDelete = () => {
|
||||
trackOnboardingEvent("WELCOME_CHAT_DELETED");
|
||||
void updateOnboarding({ onboarding_a: false });
|
||||
};
|
||||
|
||||
return {
|
||||
chat,
|
||||
handleWelcomeChatDelete,
|
||||
};
|
||||
};
|
@ -0,0 +1 @@
|
||||
export * from "./WelcomeChat";
|
@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { useSupabase } from "@/lib/context/SupabaseProvider";
|
||||
import { useToast } from "@/lib/hooks";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
import { useOnboardingTracker } from "@/lib/hooks/useOnboardingTracker";
|
||||
import { redirectToLogin } from "@/lib/router/redirectToLogin";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
@ -19,6 +21,8 @@ export const useCrawler = () => {
|
||||
const { t } = useTranslation(["translation", "upload"]);
|
||||
const [urlToCrawl, setUrlToCrawl] = useState<string>("");
|
||||
const { track } = useEventTracking();
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
const { isOnboarding } = useOnboarding();
|
||||
|
||||
if (session === null) {
|
||||
redirectToLogin();
|
||||
@ -37,7 +41,11 @@ export const useCrawler = () => {
|
||||
|
||||
return;
|
||||
}
|
||||
void track("URL_CRAWLED");
|
||||
if (isOnboarding) {
|
||||
void trackOnboardingEvent("URL_CRAWLED");
|
||||
} else {
|
||||
void track("URL_CRAWLED");
|
||||
}
|
||||
addKnowledgeToFeed({
|
||||
source: "crawl",
|
||||
url: urlToCrawl,
|
||||
|
@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { FeedItemUploadType } from "@/app/chat/[chatId]/components/ActionsBar/types";
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
import { useOnboarding } from "./useOnboarding";
|
||||
import { useOnboardingTracker } from "./useOnboardingTracker";
|
||||
import { useToast } from "./useToast";
|
||||
import { useKnowledgeToFeedContext } from "../context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
|
||||
import { acceptedFormats } from "../helpers/acceptedFormats";
|
||||
@ -12,6 +14,8 @@ import { acceptedFormats } from "../helpers/acceptedFormats";
|
||||
export const useCustomDropzone = () => {
|
||||
const { knowledgeToFeed, addKnowledgeToFeed, setShouldDisplayFeedCard } =
|
||||
useKnowledgeToFeedContext();
|
||||
const { isOnboarding } = useOnboarding();
|
||||
const { trackOnboardingEvent } = useOnboardingTracker();
|
||||
|
||||
const files: File[] = (
|
||||
knowledgeToFeed.filter((c) => c.source === "upload") as FeedItemUploadType[]
|
||||
@ -49,7 +53,12 @@ export const useCustomDropzone = () => {
|
||||
text: t("alreadyAdded", { fileName: file.name, ns: "upload" }),
|
||||
});
|
||||
} else {
|
||||
void track("FILE_UPLOADED");
|
||||
if (isOnboarding) {
|
||||
void trackOnboardingEvent("FILE_UPLOADED");
|
||||
} else {
|
||||
void track("FILE_UPLOADED");
|
||||
}
|
||||
|
||||
addKnowledgeToFeed({
|
||||
source: "upload",
|
||||
file: file,
|
||||
|
@ -29,6 +29,8 @@ export const useOnboarding = () => {
|
||||
onboarding_b3: false,
|
||||
};
|
||||
|
||||
const isOnboarding = Object.values(onboarding).some((value) => value);
|
||||
|
||||
const updateOnboardingHandler = async (
|
||||
newOnboardingStatus: Partial<Onboarding>
|
||||
) => {
|
||||
@ -49,5 +51,6 @@ export const useOnboarding = () => {
|
||||
shouldDisplayOnboardingAInstructions,
|
||||
shouldDisplayWelcomeChat,
|
||||
updateOnboarding: updateOnboardingHandler,
|
||||
isOnboarding,
|
||||
};
|
||||
};
|
||||
|
26
frontend/lib/hooks/useOnboardingTracker.ts
Normal file
26
frontend/lib/hooks/useOnboardingTracker.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
|
||||
|
||||
import { useOnboarding } from "./useOnboarding";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useOnboardingTracker = () => {
|
||||
const { onboarding } = useOnboarding();
|
||||
|
||||
const { track } = useEventTracking();
|
||||
|
||||
const trackOnboardingEvent = (
|
||||
event: string,
|
||||
properties: Record<string, unknown> = {}
|
||||
): void => {
|
||||
void track(event, {
|
||||
HOW_TO_USER_QUIVR: onboarding.onboarding_b1,
|
||||
WHAT_IS_QUIVR: onboarding.onboarding_b2,
|
||||
WHAT_IS_BRAIN: onboarding.onboarding_b3,
|
||||
...properties,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
trackOnboardingEvent,
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user