diff --git a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/__tests__/ChatDialogue.test.tsx b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/__tests__/ChatDialogue.test.tsx index 4419e53fd..0859665d0 100644 --- a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/__tests__/ChatDialogue.test.tsx +++ b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/__tests__/ChatDialogue.test.tsx @@ -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(); + const { getAllByTestId } = render( + + + + ); 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(); + const { getByTestId } = render( + + + + ); expect(getByTestId("empty-history-message")).toBeDefined(); }); diff --git a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/index.tsx b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/index.tsx index acaf2e8c5..50d7105e2 100644 --- a/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/index.tsx +++ b/frontend/app/chat/[chatId]/components/ChatDialogueArea/components/ChatDialogue/index.tsx @@ -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 (
diff --git a/frontend/app/chat/components/ChatsList/components/ChatsListItem/ChatsListItem.tsx b/frontend/app/chat/components/ChatsList/components/ChatsListItem/ChatsListItem.tsx index f7205e113..6e940fe9a 100644 --- a/frontend/app/chat/components/ChatsList/components/ChatsListItem/ChatsListItem.tsx +++ b/frontend/app/chat/components/ChatsList/components/ChatsListItem/ChatsListItem.tsx @@ -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 => {
- + {editable && ( + + )}
diff --git a/frontend/lib/api/onboarding/__test__/useOnboarding.test.ts b/frontend/lib/api/onboarding/__test__/useOnboarding.test.ts new file mode 100644 index 000000000..da286276f --- /dev/null +++ b/frontend/lib/api/onboarding/__test__/useOnboarding.test.ts @@ -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_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); + }); +}); diff --git a/frontend/lib/api/onboarding/config.ts b/frontend/lib/api/onboarding/config.ts new file mode 100644 index 000000000..597f1fd26 --- /dev/null +++ b/frontend/lib/api/onboarding/config.ts @@ -0,0 +1 @@ +export const ONBOARDING_DATA_KEY = "onboarding"; diff --git a/frontend/lib/api/onboarding/onboarding.ts b/frontend/lib/api/onboarding/onboarding.ts new file mode 100644 index 000000000..b35613bb6 --- /dev/null +++ b/frontend/lib/api/onboarding/onboarding.ts @@ -0,0 +1,16 @@ +import { AxiosInstance } from "axios"; + +import { Onboarding } from "@/lib/types/Onboarding"; + +export const getOnboarding = async ( + axiosInstance: AxiosInstance +): Promise => { + return (await axiosInstance.get("/onboarding")).data; +}; + +export const updateOnboarding = async ( + onboarding: Partial, + axiosInstance: AxiosInstance +): Promise => { + return (await axiosInstance.put("/onboarding", onboarding)).data; +}; diff --git a/frontend/lib/api/onboarding/useOnboardingApi.ts b/frontend/lib/api/onboarding/useOnboardingApi.ts new file mode 100644 index 000000000..838cb8583 --- /dev/null +++ b/frontend/lib/api/onboarding/useOnboardingApi.ts @@ -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")).data; + }; + const updateOnboarding = async (onboarding: Partial) => { + return (await axiosInstance.put("/onboarding", onboarding)) + .data; + }; + + return { + getOnboarding, + updateOnboarding, + }; +}; diff --git a/frontend/lib/hooks/useOnboarding.ts b/frontend/lib/hooks/useOnboarding.ts new file mode 100644 index 000000000..dca1154ac --- /dev/null +++ b/frontend/lib/hooks/useOnboarding.ts @@ -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 + ) => { + 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, + }; +}; diff --git a/frontend/lib/types/Onboarding.ts b/frontend/lib/types/Onboarding.ts new file mode 100644 index 000000000..efcfa02ae --- /dev/null +++ b/frontend/lib/types/Onboarding.ts @@ -0,0 +1,6 @@ +export type Onboarding = { + onboarding_a: boolean; + onboarding_b1: boolean; + onboarding_b2: boolean; + onboarding_b3: boolean; +}; diff --git a/frontend/public/locales/en/chat.json b/frontend/public/locales/en/chat.json index 7f1cf5262..edf56d4bb 100644 --- a/frontend/public/locales/en/chat.json +++ b/frontend/public/locales/en/chat.json @@ -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" } diff --git a/frontend/public/locales/es/chat.json b/frontend/public/locales/es/chat.json index 8bc4a24a3..d11f6b761 100644 --- a/frontend/public/locales/es/chat.json +++ b/frontend/public/locales/es/chat.json @@ -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" } diff --git a/frontend/public/locales/fr/chat.json b/frontend/public/locales/fr/chat.json index 20bdb9d8a..2d7d4500d 100644 --- a/frontend/public/locales/fr/chat.json +++ b/frontend/public/locales/fr/chat.json @@ -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" } diff --git a/frontend/public/locales/pt-br/chat.json b/frontend/public/locales/pt-br/chat.json index 7e3419d8c..611663ece 100644 --- a/frontend/public/locales/pt-br/chat.json +++ b/frontend/public/locales/pt-br/chat.json @@ -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" } diff --git a/frontend/public/locales/ru/chat.json b/frontend/public/locales/ru/chat.json index 03346534a..5d1d001f9 100644 --- a/frontend/public/locales/ru/chat.json +++ b/frontend/public/locales/ru/chat.json @@ -44,5 +44,6 @@ "how_to_use_quivr": "Как использовать Quivr?", "what_is_quivr": "Что такое Quivr?", "what_is_brain": "Что такое мозг?" - } + }, + "welcome": "Добро пожаловать" } diff --git a/frontend/public/locales/zh-cn/chat.json b/frontend/public/locales/zh-cn/chat.json index c9fc6fbe8..503401779 100644 --- a/frontend/public/locales/zh-cn/chat.json +++ b/frontend/public/locales/zh-cn/chat.json @@ -45,5 +45,6 @@ "how_to_use_quivr": "如何使用Quivr?", "what_is_quivr": "什么是Quivr?", "what_is_brain": "什么是大脑?" - } + }, + "welcome": "欢迎来到" }