mirror of
https://github.com/StanGirard/quivr.git
synced 2025-01-04 17:23:06 +03:00
feat: add Welcome chat (#1365)
https://github.com/StanGirard/quivr/issues/1361 https://github.com/StanGirard/quivr/assets/63923024/cc4b1c0a-363a-49f3-8306-181151554b34 --------- Co-authored-by: Zineb El Bachiri <100568984+gozineb@users.noreply.github.com>
This commit is contained in:
parent
bbf5e12f3c
commit
9293b7d782
@ -1,3 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
@ -19,6 +20,7 @@ vi.mock("../hooks/useChatDialogue", () => ({
|
||||
chatListRef: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
describe("ChatDialogue", () => {
|
||||
it("should render chat messages correctly", () => {
|
||||
@ -37,14 +39,22 @@ describe("ChatDialogue", () => {
|
||||
messages,
|
||||
[]
|
||||
);
|
||||
const { getAllByTestId } = render(<ChatDialogue chatItems={chatItems} />);
|
||||
const { getAllByTestId } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatDialogue chatItems={chatItems} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
expect(getAllByTestId("brain-tags")).toBeDefined();
|
||||
expect(getAllByTestId("prompt-tags")).toBeDefined();
|
||||
expect(getAllByTestId("chat-message-text")).toBeDefined();
|
||||
});
|
||||
|
||||
it("should render placeholder text when history is empty", () => {
|
||||
const { getByTestId } = render(<ChatDialogue chatItems={[]} />);
|
||||
const { getByTestId } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ChatDialogue chatItems={[]} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId("empty-history-message")).toBeDefined();
|
||||
});
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useFeatureIsOn } from "@growthbook/growthbook-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { ChatItem } from "./components";
|
||||
import { Onboarding } from "./components/Onboarding/Onboarding";
|
||||
import { useChatDialogue } from "./hooks/useChatDialogue";
|
||||
@ -21,9 +22,9 @@ export const ChatDialogue = ({
|
||||
const { t } = useTranslation(["chat"]);
|
||||
const { chatListRef } = useChatDialogue();
|
||||
|
||||
const shouldDisplayOnboarding = useFeatureIsOn("onboarding");
|
||||
const { shouldDisplayOnboardingAInstructions } = useOnboarding();
|
||||
|
||||
if (shouldDisplayOnboarding) {
|
||||
if (shouldDisplayOnboardingAInstructions) {
|
||||
return (
|
||||
<div className={chatDialogueContainerClassName} ref={chatListRef}>
|
||||
<Onboarding />
|
||||
|
@ -10,9 +10,15 @@ import { useChatsListItem } from "./hooks/useChatsListItem";
|
||||
|
||||
interface ChatsListItemProps {
|
||||
chat: ChatEntity;
|
||||
editable?: boolean;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export const ChatsListItem = ({ chat }: ChatsListItemProps): JSX.Element => {
|
||||
export const ChatsListItem = ({
|
||||
chat,
|
||||
editable = true,
|
||||
onDelete,
|
||||
}: ChatsListItemProps): JSX.Element => {
|
||||
const {
|
||||
setChatName,
|
||||
deleteChat,
|
||||
@ -47,17 +53,19 @@ export const ChatsListItem = ({ chat }: ChatsListItemProps): JSX.Element => {
|
||||
</div>
|
||||
</Link>
|
||||
<div className="opacity-0 group-hover:opacity-100 flex items-center justify-center bg-gradient-to-l from-white dark:from-black to-transparent z-10 transition-opacity">
|
||||
<button
|
||||
className="p-0 hover:text-blue-700"
|
||||
type="button"
|
||||
onClick={handleEditNameClick}
|
||||
>
|
||||
{editingName ? <FiSave /> : <FiEdit />}
|
||||
</button>
|
||||
{editable && (
|
||||
<button
|
||||
className="p-0 hover:text-blue-700"
|
||||
type="button"
|
||||
onClick={handleEditNameClick}
|
||||
>
|
||||
{editingName ? <FiSave /> : <FiEdit />}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="p-5 hover:text-red-700"
|
||||
type="button"
|
||||
onClick={() => void deleteChat()}
|
||||
onClick={onDelete ?? (() => void deleteChat())}
|
||||
data-testid="delete-chat-button"
|
||||
>
|
||||
<FiTrash2 />
|
||||
|
@ -0,0 +1,24 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -1,15 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Sidebar } from "@/lib/components/Sidebar/Sidebar";
|
||||
import { useOnboarding } from "@/lib/hooks/useOnboarding";
|
||||
|
||||
import { ChatHistory } from "./components/ChatHistory";
|
||||
import { NewChatButton } from "./components/NewChatButton";
|
||||
import { WelcomeChat } from "./components/WelcomeChat";
|
||||
import { useChatNotificationsSync } from "./hooks/useChatNotificationsSync";
|
||||
import { useChatsList } from "./hooks/useChatsList";
|
||||
|
||||
export const ChatsList = (): JSX.Element => {
|
||||
useChatsList();
|
||||
useChatNotificationsSync();
|
||||
const { shouldDisplayWelcomeChat } = useOnboarding();
|
||||
|
||||
return (
|
||||
<Sidebar showButtons={["myBrains", "user"]}>
|
||||
@ -17,6 +20,11 @@ export const ChatsList = (): JSX.Element => {
|
||||
<div className="pt-2">
|
||||
<NewChatButton />
|
||||
</div>
|
||||
{shouldDisplayWelcomeChat && (
|
||||
<div className="pt-2">
|
||||
<WelcomeChat />
|
||||
</div>
|
||||
)}
|
||||
<ChatHistory />
|
||||
</div>
|
||||
</Sidebar>
|
||||
|
51
frontend/lib/api/onboarding/__test__/useOnboarding.test.ts
Normal file
51
frontend/lib/api/onboarding/__test__/useOnboarding.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Onboarding } from "@/lib/types/Onboarding";
|
||||
|
||||
import { useOnboardingApi } from "../useOnboardingApi";
|
||||
|
||||
const axiosGetMock = vi.fn(() => ({}));
|
||||
const axiosPutMock = vi.fn(() => ({}));
|
||||
|
||||
vi.mock("@/lib/hooks", () => ({
|
||||
useAxios: () => ({
|
||||
axiosInstance: {
|
||||
get: axiosGetMock,
|
||||
put: axiosPutMock,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("useOnboarding", () => {
|
||||
it("should call getOnboarding with the correct parameters", async () => {
|
||||
axiosGetMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { getOnboarding },
|
||||
},
|
||||
} = renderHook(() => useOnboardingApi());
|
||||
|
||||
await getOnboarding();
|
||||
|
||||
expect(axiosGetMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosGetMock).toHaveBeenCalledWith("/onboarding");
|
||||
});
|
||||
it("should call updateOnboarding with the correct parameters", async () => {
|
||||
const onboarding: Partial<Onboarding> = {
|
||||
onboarding_a: true,
|
||||
onboarding_b1: false,
|
||||
};
|
||||
axiosPutMock.mockReturnValue({ data: {} });
|
||||
const {
|
||||
result: {
|
||||
current: { updateOnboarding },
|
||||
},
|
||||
} = renderHook(() => useOnboardingApi());
|
||||
|
||||
await updateOnboarding(onboarding);
|
||||
|
||||
expect(axiosPutMock).toHaveBeenCalledTimes(1);
|
||||
expect(axiosPutMock).toHaveBeenCalledWith("/onboarding", onboarding);
|
||||
});
|
||||
});
|
1
frontend/lib/api/onboarding/config.ts
Normal file
1
frontend/lib/api/onboarding/config.ts
Normal file
@ -0,0 +1 @@
|
||||
export const ONBOARDING_DATA_KEY = "onboarding";
|
16
frontend/lib/api/onboarding/onboarding.ts
Normal file
16
frontend/lib/api/onboarding/onboarding.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { AxiosInstance } from "axios";
|
||||
|
||||
import { Onboarding } from "@/lib/types/Onboarding";
|
||||
|
||||
export const getOnboarding = async (
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<Onboarding> => {
|
||||
return (await axiosInstance.get<Onboarding>("/onboarding")).data;
|
||||
};
|
||||
|
||||
export const updateOnboarding = async (
|
||||
onboarding: Partial<Onboarding>,
|
||||
axiosInstance: AxiosInstance
|
||||
): Promise<Onboarding> => {
|
||||
return (await axiosInstance.put<Onboarding>("/onboarding", onboarding)).data;
|
||||
};
|
19
frontend/lib/api/onboarding/useOnboardingApi.ts
Normal file
19
frontend/lib/api/onboarding/useOnboardingApi.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { useAxios } from "@/lib/hooks";
|
||||
import { Onboarding } from "@/lib/types/Onboarding";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useOnboardingApi = () => {
|
||||
const { axiosInstance } = useAxios();
|
||||
const getOnboarding = async () => {
|
||||
return (await axiosInstance.get<Onboarding>("/onboarding")).data;
|
||||
};
|
||||
const updateOnboarding = async (onboarding: Partial<Onboarding>) => {
|
||||
return (await axiosInstance.put<Onboarding>("/onboarding", onboarding))
|
||||
.data;
|
||||
};
|
||||
|
||||
return {
|
||||
getOnboarding,
|
||||
updateOnboarding,
|
||||
};
|
||||
};
|
46
frontend/lib/hooks/useOnboarding.ts
Normal file
46
frontend/lib/hooks/useOnboarding.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { useFeatureIsOn } from "@growthbook/growthbook-react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
import { ONBOARDING_DATA_KEY } from "@/lib/api/onboarding/config";
|
||||
import { useOnboardingApi } from "@/lib/api/onboarding/useOnboardingApi";
|
||||
|
||||
import { Onboarding } from "../types/Onboarding";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
export const useOnboarding = () => {
|
||||
const isOnboardingFeatureActivated = useFeatureIsOn("onboarding");
|
||||
const { getOnboarding } = useOnboardingApi();
|
||||
const params = useParams();
|
||||
const { updateOnboarding } = useOnboardingApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const chatId = params?.chatId as string | undefined;
|
||||
|
||||
const { data: onboarding } = useQuery({
|
||||
queryFn: getOnboarding,
|
||||
queryKey: [ONBOARDING_DATA_KEY],
|
||||
});
|
||||
|
||||
const updateOnboardingHandler = async (
|
||||
newOnboardingStatus: Partial<Onboarding>
|
||||
) => {
|
||||
await updateOnboarding(newOnboardingStatus);
|
||||
await queryClient.invalidateQueries({ queryKey: [ONBOARDING_DATA_KEY] });
|
||||
};
|
||||
|
||||
const shouldDisplayWelcomeChat =
|
||||
isOnboardingFeatureActivated && onboarding?.onboarding_a === true;
|
||||
|
||||
const shouldDisplayOnboardingAInstructions =
|
||||
isOnboardingFeatureActivated &&
|
||||
chatId === undefined &&
|
||||
shouldDisplayWelcomeChat;
|
||||
|
||||
return {
|
||||
onboarding,
|
||||
shouldDisplayOnboardingAInstructions,
|
||||
shouldDisplayWelcomeChat,
|
||||
updateOnboarding: updateOnboardingHandler,
|
||||
};
|
||||
};
|
6
frontend/lib/types/Onboarding.ts
Normal file
6
frontend/lib/types/Onboarding.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type Onboarding = {
|
||||
onboarding_a: boolean;
|
||||
onboarding_b1: boolean;
|
||||
onboarding_b2: boolean;
|
||||
onboarding_b3: boolean;
|
||||
};
|
@ -44,5 +44,6 @@
|
||||
"how_to_use_quivr": "How to use Quivr ?",
|
||||
"what_is_quivr": "What is Quivr ?",
|
||||
"what_is_brain": "What is a brain ?"
|
||||
}
|
||||
},
|
||||
"welcome":"Welcome"
|
||||
}
|
||||
|
@ -44,5 +44,6 @@
|
||||
"how_to_use_quivr": "¿Cómo usar Quivr?",
|
||||
"what_is_quivr": "¿Qué es Quivr?",
|
||||
"what_is_brain": "¿Qué es un cerebro?"
|
||||
}
|
||||
},
|
||||
"welcome": "Bienvenido"
|
||||
}
|
||||
|
@ -44,5 +44,6 @@
|
||||
"how_to_use_quivr": "Comment utiliser Quivr ?",
|
||||
"what_is_quivr": "Qu'est-ce que Quivr ?",
|
||||
"what_is_brain": "Qu'est-ce qu'un cerveau ?"
|
||||
}
|
||||
},
|
||||
"welcome": "Bienvenue"
|
||||
}
|
||||
|
@ -44,5 +44,6 @@
|
||||
"how_to_use_quivr": "Como usar o Quivr?",
|
||||
"what_is_quivr": "O que é o Quivr?",
|
||||
"what_is_brain": "O que é um cérebro?"
|
||||
}
|
||||
},
|
||||
"welcome": "Bem-vindo"
|
||||
}
|
||||
|
@ -44,5 +44,6 @@
|
||||
"how_to_use_quivr": "Как использовать Quivr?",
|
||||
"what_is_quivr": "Что такое Quivr?",
|
||||
"what_is_brain": "Что такое мозг?"
|
||||
}
|
||||
},
|
||||
"welcome": "Добро пожаловать"
|
||||
}
|
||||
|
@ -45,5 +45,6 @@
|
||||
"how_to_use_quivr": "如何使用Quivr?",
|
||||
"what_is_quivr": "什么是Quivr?",
|
||||
"what_is_brain": "什么是大脑?"
|
||||
}
|
||||
},
|
||||
"welcome": "欢迎来到"
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user