feat(frontend): Page Header + Begin of Studio (#2151)

# Description

Please include a summary of the changes and the related issue. Please
also include relevant motivation and context.

## Checklist before requesting a review

Please delete options that are not relevant.

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented hard-to-understand areas
- [ ] I have ideally added tests that prove my fix is effective or that
my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged

## Screenshots (if appropriate):
This commit is contained in:
Antoine Dewez 2024-02-06 16:05:07 -08:00 committed by GitHub
parent ccbe6a826b
commit a540a201e3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
132 changed files with 1303 additions and 1006 deletions

View File

@ -2,107 +2,102 @@
title: Generate Images title: Generate Images
--- ---
You can use Quivr to Generate Images using Dall-E 3 from OpenAI or any other API based image generation tool. You can use Quivr to Generate Images using Dall-E 3 from OpenAI or any other API based image generation tool.
This allows you to leverage image generation by creating a brain that is connected to Dall-E. This allows you to leverage image generation by creating a brain that is connected to Dall-E.
## OpenAI ## OpenAI
<Info> <Info>
In order to have image generation in Quivr we will create a brain that is connected to an API. You can find more info on this types of brains [here](/getting-started/api-based-brains) In order to have image generation in Quivr we will create a brain that is
connected to an API. You can find more info on this types of brains
[here](/getting-started/api-based-brains)
</Info> </Info>
<Steps> <Steps>
<Step title="Create OpenAI Account"> <Step title="Create OpenAI Account">
Create an account on [OpenAI](https://platform.openai.com/) Create an account on [OpenAI](https://platform.openai.com/)
</Step> </Step>
<Step title="Credit Card"> <Step title="Credit Card">
To get access to image generation, add your credit card in the `billing` section To get access to image generation, add your credit card in the `billing`
</Step> section
<Step title="Create API Key"> </Step>
Create on API Key and save the result <Step title="Create API Key">Create on API Key and save the result</Step>
</Step>
</Steps> </Steps>
<Warning> <Warning>Do not share your API Key</Warning>
Do not share your API Key
</Warning>
## Create a brain ## Create a brain
Now it is time to create an App brain. This type of brain allows you to interact with APIs. And that is exactly what we need to do now. Let's get started. Now it is time to create an App brain. This type of brain allows you to interact with APIs. And that is exactly what we need to do now. Let's get started.
<Warning> <Warning>
Generating images costs around 2cts per image. Be carefull or you'll have a surprised bill. Generating images costs around 2cts per image. Be carefull or you'll have a
surprised bill.
</Warning> </Warning>
<Steps> <Steps>
<Step title="Create a new Brain"> <Step title="Create a new Brain">
Go to the [Brains](https://www.quivr.app/brains-management) page and click on `Create Brain` Go to the [Brains](https://www.quivr.app/studio) page and click on `Create
</Step> Brain`
<Step title="Choose App Brain"> </Step>
<Step title="Choose App Brain">
Select `App (Through API)` and click on `Next` Select `App (Through API)` and click on `Next`
</Step> </Step>
<Step title="Name & Prompt"> <Step title="Name & Prompt">
1. Give your brain a name. 1. Give your brain a name. 2. Add this as your description. ``` You generate
2. Add this as your description. only one image and use markdown to display the url sent as a result and you
``` need to keep the entire url with all the parameters for the image to be
You generate only one image and use markdown to display the url sent as a result and you need to keep the entire url with all the parameters for the image to be displayed correctly. displayed correctly. Do not remove the signature and password in the url of
Do not remove the signature and password in the url of the image as it is a signed url. the image as it is a signed url. Also return the revised prompt below the
Also return the revised prompt below the image in italic. image in italic. If the user ask for a modification use the previous prompt
If the user ask for a modification use the previous prompt generated as base generated as base ``` 3. Click on `Next`
``` </Step>
3. Click on `Next` <Step title="URL">
</Step> 1. Select `POST` as the method 2. Add this as the url
<Step title="URL"> `https://api.openai.com/v1/images/generations`
1. Select `POST` as the method <img src="/images/openai-brain-url.png" />
2. Add this as the url `https://api.openai.com/v1/images/generations` </Step>
<img src="/images/openai-brain-url.png"/> <Step title="Parameters">
</Step>
<Step title="Parameters">
1. Add `prompt` as a parameter and click on `Required` 1. Add `prompt` as a parameter and click on `Required`
<ParamField path="prompt" type="string"> <ParamField path="prompt" type="string">
Description of the image to generate Description of the image to generate
</ParamField> </ParamField>
2. Add `size` as a parameter and click on `Required` 2. Add `size` as a parameter and click on `Required`
<ParamField path="size" type="string"> <ParamField path="size" type="string">
By default 1024x1024 . But images can have a size of 1024x1024 (square), 1024x1792 (vertical) or 1792x1024 (horizontal) pixels By default 1024x1024 . But images can have a size of 1024x1024 (square),
</ParamField> 1024x1792 (vertical) or 1792x1024 (horizontal) pixels
</ParamField>
3. Add `model` as a parameter and click on `Required` 3. Add `model` as a parameter and click on `Required`
<ParamField path="model" type="string"> <ParamField path="model" type="string">
Either dall-e-3 or dall-e-2. By default dall-e-3 Either dall-e-3 or dall-e-2. By default dall-e-3
</ParamField> </ParamField>
<img src="/images/openai-brain-params.png"/> <img src="/images/openai-brain-params.png" />
</Step> </Step>
<Step title="Secrets"> <Step title="Secrets">
1. Add `Authorization` as a secret 1. Add `Authorization` as a secret
<ParamField path="Authorization" type="string"> <ParamField path="Authorization" type="string">
Bearer YOUR_API_KEY Bearer YOUR_API_KEY
</ParamField> </ParamField>
**Add your API Key in the value** **Add your API Key in the value** 2. Add `Content-Type` as a secret and
2. Add `Content-Type` as a secret and click on `Required` click on `Required`
<ParamField path="Content-Type" type="string"> <ParamField path="Content-Type" type="string">
application/json application/json
</ParamField> </ParamField>
**Add `application/json` in the value** **Add `application/json` in the value**
<img src="/images/openai-brain-secrets.png"/> <img src="/images/openai-brain-secrets.png" />
</Step> </Step>
<Step title="Create"> <Step title="Create">Click on `Create`</Step>
Click on `Create` <Step title="Generate your image">
</Step>
<Step title="Generate your image">
Go to the chat and ask a question to your newly created brain. Go to the chat and ask a question to your newly created brain.
<Tip> <Tip>
You might have to type `@` and select your brain to be able to ask a question to it. You might have to type `@` and select your brain to be able to ask a
question to it.
</Tip> </Tip>
<img src="/images/openai-brain-chat.png"/> <img src="/images/openai-brain-chat.png" />
</Step> </Step>
</Steps> </Steps>
You can now use this brain to generate images. You can also use it to generate images in your own app throught the API. You can now use this brain to generate images. You can also use it to generate images in your own app throught the API.
Don't hesitate to share your creations on [Twitter](https://twitter.com/quivr_brain). Don't hesitate to share your creations on [Twitter](https://twitter.com/quivr_brain).
<Snippet file="commercial.mdx" /> <Snippet file="commercial.mdx" />

View File

@ -2,10 +2,7 @@
title: Brains title: Brains
--- ---
<Info> <Info>A few brains were harmed in the making of this documentation 🤯😏</Info>
A few brains were harmed in the making of this documentation 🤯😏
</Info>
# Introduction to Brains # Introduction to Brains
@ -22,7 +19,6 @@ A few brains were harmed in the making of this documentation 🤯😏
</Steps> </Steps>
# Two kinds of Brains # Two kinds of Brains
In Quivr, you can find two kinds of brains: **Document** and **App**. In Quivr, you can find two kinds of brains: **Document** and **App**.
@ -39,17 +35,19 @@ The brains are here to help you store information or interact with apps. Let's s
</CardGroup> </CardGroup>
<Info> <Info>
You can create as many brains as you want. You can also share them with your team. You can create as many brains as you want. You can also share them with your
team.
</Info> </Info>
We introduced the two kinds of brains in the previous section. Let's dive a little deeper into each of them. We introduced the two kinds of brains in the previous section. Let's dive a little deeper into each of them.
# Create a Brain # Create a Brain
Simply go to [Quivr](https://quivr.app/brains-management) and click on the "Add Brain" button. Simply go to [Quivr](https://quivr.app/studio) and click on the "Add Brain" button.
<Info> <Info>
By default, on the free version you can create up to 3 brains. If you need more, you can upgrade to a paid plan. By default, on the free version you can create up to 3 brains. If you need
more, you can upgrade to a paid plan.
</Info> </Info>
<Steps> <Steps>
@ -90,8 +88,7 @@ By default, on the free version you can create up to 3 brains. If you need more,
<img src="/images/add-brain-step6.png"/> <img src="/images/add-brain-step6.png"/>
</Frame> </Frame>
</Step> </Step>
</Steps> </Steps>
If you have any questions or issue with the documentation, feel free to edit or open an issue. If you have any questions or issue with the documentation, feel free to edit or open an issue.

View File

@ -5,6 +5,7 @@ import { posthog } from "posthog-js";
import { PostHogProvider } from "posthog-js/react"; import { PostHogProvider } from "posthog-js/react";
import { PropsWithChildren, useEffect } from "react"; import { PropsWithChildren, useEffect } from "react";
import { BrainCreationProvider } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { Menu } from "@/lib/components/Menu/Menu"; import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener"; import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import { NotificationBanner } from "@/lib/components/NotificationBanner"; import { NotificationBanner } from "@/lib/components/NotificationBanner";
@ -82,13 +83,15 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrainProvider> <BrainProvider>
<KnowledgeToFeedProvider> <KnowledgeToFeedProvider>
<MenuProvider> <BrainCreationProvider>
<ChatsProvider> <MenuProvider>
<ChatProvider> <ChatsProvider>
<App>{children}</App> <ChatProvider>
</ChatProvider> <App>{children}</App>
</ChatsProvider> </ChatProvider>
</MenuProvider> </ChatsProvider>
</MenuProvider>
</BrainCreationProvider>
</KnowledgeToFeedProvider> </KnowledgeToFeedProvider>
</BrainProvider> </BrainProvider>
</QueryClientProvider> </QueryClientProvider>

View File

@ -1,61 +0,0 @@
import { useTranslation } from "react-i18next";
import Spinner from "@/lib/components/ui/Spinner";
import { Tabs, TabsContent, TabsList } from "@/lib/components/ui/Tabs";
import { BrainSearchBar } from "./components/BrainSearchBar";
import { BrainsList } from "./components/BrainsList";
import { useBrainsTabs } from "./hooks/useBrainsTabs";
import { StyledTabsTrigger } from "../StyledTabsTrigger";
export const BrainsTabs = (): JSX.Element => {
const { t } = useTranslation(["brain", "translation"]);
const {
searchQuery,
isFetchingBrains,
setSearchQuery,
brains,
privateBrains,
publicBrains,
} = useBrainsTabs();
if (isFetchingBrains && brains.length === 0) {
return (
<div className="flex w-full h-full justify-center items-center">
<Spinner />
</div>
);
}
return (
<Tabs defaultValue="all" className="flex flex-col">
<TabsList className="flex flex-row justify-start gap-2 border-b-2 rounded-none pb-3">
<StyledTabsTrigger value="all">
{t("translation:all")}
</StyledTabsTrigger>
<StyledTabsTrigger value="private" className="capitalize">
{t("private_brain_label")}
</StyledTabsTrigger>
<StyledTabsTrigger value="public" className="capitalize">
{t("public_brain_label")}
</StyledTabsTrigger>
<div className="w-full flex justify-end">
<BrainSearchBar
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
</div>
</TabsList>
<TabsContent value="all">
<BrainsList brains={brains} />
</TabsContent>
<TabsContent value="private">
<BrainsList brains={privateBrains} />
</TabsContent>
<TabsContent value="public">
<BrainsList brains={publicBrains} />
</TabsContent>
</Tabs>
);
};

View File

@ -1,55 +0,0 @@
import Link from "next/link";
import { useTranslation } from "react-i18next";
import { CgFileDocument } from "react-icons/cg";
import { LuChevronRightCircle } from "react-icons/lu";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { getBrainIconFromBrainType } from "@/lib/helpers/getBrainIconFromBrainType";
type BrainItemProps = {
brain: MinimalBrainForUser;
};
export const BrainItem = ({ brain }: BrainItemProps): JSX.Element => {
const { t } = useTranslation("brain");
const isBrainDescriptionEmpty = brain.description === "";
const brainDescription = isBrainDescriptionEmpty
? t("empty_brain_description")
: brain.description;
return (
<div className="flex justify-center items-center flex-col flex-1 w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden dark:bg-black border border-black/10 dark:border-white/25 pb-2 bg-secondary">
<div className="w-full">
<div className="w-full py-2 flex gap-2 justify-center items-center bg-primary bg-opacity-40 px-2">
{getBrainIconFromBrainType(brain.brain_type, {
iconSize: 24,
DocBrainIcon: CgFileDocument,
iconClassName: "text-primary",
})}
<span className="line-clamp-1 mr-2 font-semibold text-md">
{brain.name}
</span>
</div>
</div>
<div className="flex-1 py-2">
<p
className={`line-clamp-2 text-center px-5 ${
isBrainDescriptionEmpty && "text-gray-400"
}`}
>
{brainDescription}
</p>
</div>
<div className="w-full px-2">
<Link
href={`/brains-management/${brain.id}`}
className="px-8 py-3 flex items-center justify-center gap-2 bg-white text-primary rounded-lg border-0 w-content mt-3 disabled:bg-secondary hover:bg-primary/50 disabled:hover:bg-primary/50 w-full text-md"
>
<span>{t("configure")}</span>
<LuChevronRightCircle className="text-md" />
</Link>
</div>
</div>
);
};

View File

@ -1,29 +0,0 @@
import { useTranslation } from "react-i18next";
import { LuSearch } from "react-icons/lu";
import Field from "@/lib/components/ui/Field";
type BrainSearchBarProps = {
searchQuery: string;
setSearchQuery: (searchQuery: string) => void;
};
export const BrainSearchBar = ({
searchQuery,
setSearchQuery,
}: BrainSearchBarProps): JSX.Element => {
const { t } = useTranslation(["brain"]);
return (
<Field
name="brainsearch"
placeholder={t("searchBrain")}
autoComplete="off"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-auto"
inputClassName="w-max w-[200px] rounded-3xl border-none"
icon={<LuSearch className="text-primary" size={20} />}
/>
);
};

View File

@ -1,21 +0,0 @@
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { BrainItem } from "./BrainItem";
type BrainsListProps = {
brains: MinimalBrainForUser[];
};
export const BrainsList = ({ brains }: BrainsListProps): JSX.Element => {
return (
<div className="flex flex-1 flex-col items-center justify-center">
<div className="w-full lg:grid-cols-4 md:grid-cols-3 grid mt-5 gap-3 items-stretch">
{brains.map((brain) => (
<div key={brain.id} className="h-[180px]">
<BrainItem brain={brain} />
</div>
))}
</div>
</div>
);
};

View File

@ -1,33 +0,0 @@
"use client";
import { useTranslation } from "react-i18next";
import { LuBrain } from "react-icons/lu";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { BrainsTabs } from "./components/BrainsTabs/BrainsTabs";
const BrainsManagement = (): JSX.Element => {
const { t } = useTranslation("chat");
return (
<div className="flex flex-col flex-1 bg-white">
<div className="w-full h-full p-6 flex flex-col flex-1 overflow-auto">
<div className="w-full mb-10">
<div className="flex flex-row justify-center items-center gap-2">
<LuBrain size={20} className="text-primary" />
<span className="capitalize text-2xl font-semibold">
{t("brains")}
</span>
</div>
</div>
<BrainsTabs />
</div>
<div className="w-full flex justify-center py-4">
<AddBrainModal />
</div>
</div>
);
};
export default BrainsManagement;

View File

@ -2,7 +2,8 @@ import { SuggestionKeyDownProps } from "@tiptap/suggestion";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { FaAngleDoubleDown } from "react-icons/fa"; import { FaAngleDoubleDown } from "react-icons/fa";
import { AddBrainModal } from "@/lib/components/AddBrainModal"; import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import TextButton from "@/lib/components/ui/TextButton/TextButton";
import { AddNewPromptButton } from "./components/AddNewPromptButton"; import { AddNewPromptButton } from "./components/AddNewPromptButton";
import { MentionItem } from "./components/MentionItem/MentionItem"; import { MentionItem } from "./components/MentionItem/MentionItem";
@ -23,6 +24,7 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
const { suggestionsRef, shouldShowScrollToBottomIcon, scrollToBottom } = const { suggestionsRef, shouldShowScrollToBottomIcon, scrollToBottom } =
useSuggestionsOverflowHandler(); useSuggestionsOverflowHandler();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => { const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault(); event.preventDefault();
@ -56,7 +58,14 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
onClick={scrollToBottom} onClick={scrollToBottom}
/> />
)} )}
{isBrain && <AddBrainModal />} {isBrain && (
<TextButton
label="Create Brain"
iconName="add"
color="black"
onClick={() => setIsBrainCreationModalOpened(true)}
/>
)}
{isPrompt && <AddNewPromptButton />} {isPrompt && <AddNewPromptButton />}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@ export const AddNewPromptButton = (): JSX.Element => {
return ( return (
<Button <Button
onClick={() => router.push("/brains-management")} onClick={() => router.push("/studio")}
variant={"tertiary"} variant={"tertiary"}
className={"border-0"} className={"border-0"}
data-testid="add-brain-button" data-testid="add-brain-button"

View File

@ -59,7 +59,7 @@ export const KnowledgeToFeed = ({
<KnowledgeToFeedInput feedBrain={() => void feedBrain()} /> <KnowledgeToFeedInput feedBrain={() => void feedBrain()} />
)} )}
{Boolean(currentBrainId) && ( {Boolean(currentBrainId) && (
<Link href={`/brains-management/${currentBrainId ?? ""}`}> <Link href={`/studio/${currentBrainId ?? ""}`}>
<Button variant={"tertiary"}> <Button variant={"tertiary"}>
{t("manage_brain", { ns: "brain" })} {t("manage_brain", { ns: "brain" })}
</Button> </Button>

View File

@ -1,21 +1,24 @@
@use "@/styles/Colors.module.scss"; @use "@/styles/Colors.module.scss";
@use "@/styles/Spacings.module.scss"; @use "@/styles/Spacings.module.scss";
.chat_page_container { .main_container {
display: flex; display: flex;
flex: 1 1 0%; flex-direction: column;
background-color: Colors.$white; width: 100%;
padding-block: Spacings.$spacing06;
padding-inline: Spacings.$spacing09;
display: flex;
gap: Spacings.$spacing09;
&.feeding { .chat_page_container {
background-color: Colors.$chat-bg-gray; display: flex;
} flex: 1 1 0%;
background-color: Colors.$white;
padding-block: Spacings.$spacing06;
padding-inline: Spacings.$spacing09;
display: flex;
gap: Spacings.$spacing09;
overflow: hidden;
.data_panel_wrapper { .data_panel_wrapper {
height: 100%; height: 100%;
width: 30%; width: 30%;
}
} }
} }

View File

@ -1,8 +1,13 @@
"use client"; "use client";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext"; import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useDevice } from "@/lib/hooks/useDevice"; import { useDevice } from "@/lib/hooks/useDevice";
import { useCustomDropzone } from "@/lib/hooks/useDropzone"; import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { ButtonType } from "@/lib/types/QuivrButton";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { ActionsBar } from "./components/ActionsBar"; import { ActionsBar } from "./components/ActionsBar";
@ -13,40 +18,63 @@ import styles from "./page.module.scss";
const SelectedChatPage = (): JSX.Element => { const SelectedChatPage = (): JSX.Element => {
const { getRootProps } = useCustomDropzone(); const { getRootProps } = useCustomDropzone();
const { shouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { isMobile } = useDevice(); const { isMobile } = useDevice();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
useChatNotificationsSync(); useChatNotificationsSync();
const buttons: ButtonType[] = [
{
label: "Create brain",
color: "primary",
onClick: () => {
setIsBrainCreationModalOpened(true);
},
},
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
},
];
return ( return (
<div <div className={styles.main_container}>
className={` <div className={styles.page_header}>
${styles.chat_page_container ?? ""} <PageHeader iconName="chat" label="Chat" buttons={buttons} />
${shouldDisplayFeedCard ? styles.feeding ?? "" : ""} </div>
`}
data-testid="chat-page"
{...getRootProps()}
>
<div <div
className={cn( className={styles.chat_page_container}
"flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden", data-testid="chat-page"
"dark:bg-black transition-colors ease-out duration-500" {...getRootProps()}
)}
> >
<div <div
className={`flex flex-col flex-1 w-full max-w-4xl h-full dark:shadow-primary/25 overflow-hidden`} className={cn(
"flex flex-col flex-1 items-center justify-stretch w-full h-full overflow-hidden",
"dark:bg-black transition-colors ease-out duration-500"
)}
> >
<div className="flex flex-1 flex-col overflow-y-auto"> <div
<ChatDialogueArea /> className={`flex flex-col flex-1 w-full max-w-4xl h-full dark:shadow-primary/25 overflow-hidden`}
>
<div className="flex flex-1 flex-col overflow-y-auto">
<ChatDialogueArea />
</div>
<ActionsBar />
</div> </div>
<ActionsBar />
</div> </div>
{!isMobile && (
<div className={styles.data_panel_wrapper}>
<DataPanel />
</div>
)}
<UploadDocumentModal />
<AddBrainModal />
</div> </div>
{!isMobile && (
<div className={styles.data_panel_wrapper}>
<DataPanel />
</div>
)}
</div> </div>
); );
}; };

View File

@ -6,59 +6,69 @@
@use "@/styles/Typography.module.scss"; @use "@/styles/Typography.module.scss";
@use "@/styles/Variables.module.scss"; @use "@/styles/Variables.module.scss";
.search_page_container { .main_container {
background-color: Colors.$white; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.main_container { .page_header {
position: absolute;
width: 100%;
}
.search_page_container {
background-color: Colors.$white;
width: 100%;
height: 100%;
display: flex; display: flex;
align-items: center;
justify-content: center;
flex-direction: column; flex-direction: column;
row-gap: Spacings.$spacing05;
position: relative;
width: 50%;
margin-inline: auto;
transform: translateY(-#{Variables.$searchBarHeight});
@media (max-width: ScreenSizes.$small) { .main_wrapper {
width: 100%;
padding-inline: Spacings.$spacing07;
}
.quivr_logo_wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; row-gap: Spacings.$spacing05;
align-items: center; width: 50%;
margin-inline: auto;
transform: translateY(-#{Variables.$searchBarHeight});
.quivr_text { @media (max-width: ScreenSizes.$small) {
@include Typography.Big; width: 100%;
padding-inline: Spacings.$spacing07;
}
.quivr_text_primary { .quivr_logo_wrapper {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.quivr_text {
@include Typography.Big;
.quivr_text_primary {
color: Colors.$primary;
}
}
}
}
.shortcuts_card_wrapper {
background-color: Colors.$lightest-grey;
padding: Spacings.$spacing05;
gap: Spacings.$spacing03;
border-radius: Radius.$big;
.shortcut_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
.shortcut {
color: Colors.$primary; color: Colors.$primary;
} }
} }
} }
} }
.shortcuts_card_wrapper {
background-color: Colors.$lightest-grey;
padding: Spacings.$spacing05;
gap: Spacings.$spacing03;
border-radius: Radius.$big;
.shortcut_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
.shortcut {
color: Colors.$primary;
}
}
}
} }

View File

@ -3,15 +3,23 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react"; import { useEffect } from "react";
import { QuivrLogo } from "@/lib/assets/QuivrLogo"; import { QuivrLogo } from "@/lib/assets/QuivrLogo";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar"; import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { ButtonType } from "@/lib/types/QuivrButton";
import styles from "./page.module.scss"; import styles from "./page.module.scss";
const Search = (): JSX.Element => { const Search = (): JSX.Element => {
const pathname = usePathname(); const pathname = usePathname();
const { session } = useSupabase(); const { session } = useSupabase();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
useEffect(() => { useEffect(() => {
if (session === null) { if (session === null) {
@ -19,30 +27,54 @@ const Search = (): JSX.Element => {
} }
}, [pathname, session]); }, [pathname, session]);
const buttons: ButtonType[] = [
{
label: "Create brain",
color: "primary",
onClick: () => {
setIsBrainCreationModalOpened(true);
},
},
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
},
];
return ( return (
<div className={styles.search_page_container}> <div className={styles.main_container}>
<div className={styles.main_container}> <div className={styles.page_header}>
<div className={styles.quivr_logo_wrapper}> <PageHeader iconName="home" label="Home" buttons={buttons} />
<QuivrLogo size={80} color="black" /> </div>
<div className={styles.quivr_text}> <div className={styles.search_page_container}>
<span>Talk to </span> <div className={styles.main_wrapper}>
<span className={styles.quivr_text_primary}>Quivr</span> <div className={styles.quivr_logo_wrapper}>
<QuivrLogo size={80} color="black" />
<div className={styles.quivr_text}>
<span>Talk to </span>
<span className={styles.quivr_text_primary}>Quivr</span>
</div>
</div>
<div className={styles.search_bar_wrapper}>
<SearchBar />
</div> </div>
</div> </div>
<div className={styles.search_bar_wrapper}> <div className={styles.shortcuts_card_wrapper}>
<SearchBar /> <div className={styles.shortcut_wrapper}>
</div> <span className={styles.shortcut}>@</span>
</div> <span>Select a brain</span>
<div className={styles.shortcuts_card_wrapper}> </div>
<div className={styles.shortcut_wrapper}> <div className={styles.shortcut_wrapper}>
<span className={styles.shortcut}>@</span> <span className={styles.shortcut}>#</span>
<span>Select a brain</span> <span>Select a prompt</span>
</div> </div>
<div className={styles.shortcut_wrapper}>
<span className={styles.shortcut}>#</span>
<span>Select a prompt</span>
</div> </div>
</div> </div>
<UploadDocumentModal />
<AddBrainModal />
</div> </div>
); );
}; };

View File

@ -1,7 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { StyledTabsTrigger } from "@/app/brains-management/components/StyledTabsTrigger"; import { StyledTabsTrigger } from "@/app/studio/components/StyledTabsTrigger";
import Button from "@/lib/components/ui/Button"; import Button from "@/lib/components/ui/Button";
import Spinner from "@/lib/components/ui/Spinner"; import Spinner from "@/lib/components/ui/Spinner";
import { Tabs, TabsContent, TabsList } from "@/lib/components/ui/Tabs"; import { Tabs, TabsContent, TabsList } from "@/lib/components/ui/Tabs";

View File

@ -23,7 +23,7 @@ export const useBrainFetcher = ({ brainId }: UseBrainFetcherProps) => {
return await getBrain(brainId); return await getBrain(brainId);
} catch (error) { } catch (error) {
router.push("/brains-management"); router.push("/studio");
} }
}; };

View File

@ -1,5 +1,5 @@
import { UUID } from "crypto"; import { UUID } from "crypto";
import { useParams, useRouter } from "next/navigation"; import { useParams, usePathname, useRouter } from "next/navigation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -13,7 +13,7 @@ import { getBrainPermissions } from "../utils/getBrainPermissions";
import { getTargetedTab } from "../utils/getTargetedTab"; import { getTargetedTab } from "../utils/getTargetedTab";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainManagementTabs = () => { export const useBrainManagementTabs = (customBrainId?: UUID) => {
const [selectedTab, setSelectedTab] = const [selectedTab, setSelectedTab] =
useState<BrainManagementTab>("settings"); useState<BrainManagementTab>("settings");
const { allBrains } = useBrainContext(); const { allBrains } = useBrainContext();
@ -41,8 +41,9 @@ export const useBrainManagementTabs = () => {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const pathname = usePathname();
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]); const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
const brainId = params?.brainId as UUID | undefined; const brainId = customBrainId ?? (params?.brainId as UUID | undefined);
const { hasEditRights, isOwnedByCurrentUser, isPublicBrain } = const { hasEditRights, isOwnedByCurrentUser, isPublicBrain } =
getBrainPermissions({ getBrainPermissions({
@ -67,6 +68,7 @@ export const useBrainManagementTabs = () => {
if (brainId === undefined) { if (brainId === undefined) {
return; return;
} }
setIsDeleteOrUnsubscribeRequestPending(true); setIsDeleteOrUnsubscribeRequestPending(true);
try { try {
if (!isOwnedByCurrentUser) { if (!isOwnedByCurrentUser) {
@ -80,7 +82,7 @@ export const useBrainManagementTabs = () => {
} catch (error) { } catch (error) {
console.error("Error deleting brain: ", error); console.error("Error deleting brain: ", error);
} finally { } finally {
router.push("/brains-management"); pathname === "/studio" ? void fetchAllBrains() : router.push("/studio");
setIsDeleteOrUnsubscribeRequestPending(false); setIsDeleteOrUnsubscribeRequestPending(false);
} }
}; };

View File

@ -16,7 +16,7 @@ const BrainsManagement = (): JSX.Element => {
return ( return (
<div className="flex flex-col w-full p-5 lg:p-20 bg-highlight"> <div className="flex flex-col w-full p-5 lg:p-20 bg-highlight">
<div> <div>
<Link href="/brains-management"> <Link href="/studio">
<Button variant="tertiary" className="p-0"> <Button variant="tertiary" className="p-0">
<LuChevronLeftCircle className="text-primary" /> <LuChevronLeftCircle className="text-primary" />
{t("previous")} {t("previous")}

View File

@ -0,0 +1,49 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_item_wrapper {
padding-inline: Spacings.$spacing05;
border-top: 1px solid Colors.$primary-lightest;
overflow: hidden;
display: flex;
gap: Spacings.$spacing02;
justify-content: space-between;
align-items: center;
cursor: pointer;
&:hover {
background-color: Colors.$lightest-black;
}
.brain_info_wrapper {
padding-block: Spacings.$spacing03;
display: flex;
overflow: hidden;
gap: Spacings.$spacing05;
overflow: hidden;
flex: 1;
.name {
@include Typography.EllipsisOverflow;
width: 200px;
}
.description {
@include Typography.EllipsisOverflow;
flex: 1;
color: Colors.$dark-grey;
}
@media (max-width: ScreenSizes.$small) {
.name {
width: auto;
}
.description {
display: none;
}
}
}
}

View File

@ -0,0 +1,72 @@
import Link from "next/link";
import { useState } from "react";
import { DeleteOrUnsubscribeConfirmationModal } from "@/app/studio/[brainId]/components/BrainManagementTabs/components/Modals/DeleteOrUnsubscribeConfirmationModal";
import { useBrainManagementTabs } from "@/app/studio/[brainId]/components/BrainManagementTabs/hooks/useBrainManagementTabs";
import { getBrainPermissions } from "@/app/studio/[brainId]/components/BrainManagementTabs/utils/getBrainPermissions";
import Icon from "@/lib/components/ui/Icon/Icon";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import styles from "./BrainItem.module.scss";
type BrainItemProps = {
brain: MinimalBrainForUser;
even: boolean;
};
export const BrainItem = ({ brain, even }: BrainItemProps): JSX.Element => {
const {
handleUnsubscribeOrDeleteBrain,
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
isDeleteOrUnsubscribeRequestPending,
} = useBrainManagementTabs(brain.id);
const [isHovered, setIsHovered] = useState<boolean>(false);
const { allBrains } = useBrainContext();
const { isOwnedByCurrentUser } = getBrainPermissions({
brainId: brain.id,
userAccessibleBrains: allBrains,
});
return (
<div
className={`
${even ? styles.even : styles.odd}
${styles.brain_item_wrapper}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Link className={styles.brain_info_wrapper} href={`/studio/${brain.id}`}>
<span className={styles.name}>{brain.name}</span>
<span className={styles.description}>{brain.description}</span>
</Link>
<Icon
name="edit"
size="normal"
color="black"
hovered={isHovered}
onClick={() => (window.location.href = `/studio/${brain.id}`)}
/>
<Icon
name="delete"
size="normal"
color="dangerous"
handleHover={true}
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
/>
<DeleteOrUnsubscribeConfirmationModal
isOpen={isDeleteOrUnsubscribeModalOpened}
setOpen={setIsDeleteOrUnsubscribeModalOpened}
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
isOwnedByCurrentUser={isOwnedByCurrentUser}
isDeleteOrUnsubscribeRequestPending={
isDeleteOrUnsubscribeRequestPending
}
/>
</div>
);
};

View File

@ -0,0 +1,24 @@
import { useTranslation } from "react-i18next";
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
type BrainSearchBarProps = {
searchQuery: string;
setSearchQuery: (searchQuery: string) => void;
};
export const BrainSearchBar = ({
searchQuery,
setSearchQuery,
}: BrainSearchBarProps): JSX.Element => {
const { t } = useTranslation(["brain"]);
return (
<TextInput
iconName="search"
label={t("searchBrain")}
inputValue={searchQuery}
setInputValue={setSearchQuery}
/>
);
};

View File

@ -0,0 +1,37 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brains_wrapper {
display: flex;
flex-direction: column;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
border-radius: Radius.$big;
overflow: hidden;
width: 100%;
overflow-y: scroll;
.columns {
@include Typography.H3;
display: flex;
padding: Spacings.$spacing05;
gap: Spacings.$spacing05;
background-color: Colors.$lightest-grey;
.name {
width: 200px;
}
.description {
color: Colors.$dark-grey;
}
@media (max-width: ScreenSizes.$small) {
.description {
display: none;
}
}
}
}

View File

@ -0,0 +1,25 @@
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import styles from "./BrainsList.module.scss";
import { BrainItem } from "../BrainItem/BrainItem";
type BrainsListProps = {
brains: MinimalBrainForUser[];
};
export const BrainsList = ({ brains }: BrainsListProps): JSX.Element => {
return (
<div className={styles.brains_wrapper}>
<div className={styles.columns}>
<span className={styles.name}>Name</span>
<span className={styles.description}>Description</span>
</div>
{brains.map((brain, index) => (
<div key={brain.id}>
<BrainItem brain={brain} even={!(index % 2)} />
</div>
))}
</div>
);
};

View File

@ -0,0 +1,13 @@
@use "@/styles/Spacings.module.scss";
.manage_brains_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
padding-block: Spacings.$spacing05;
height: 100%;
.search_brain {
width: 250px;
}
}

View File

@ -0,0 +1,33 @@
import Spinner from "@/lib/components/ui/Spinner";
import styles from "./ManageBrains.module.scss";
import { useBrainsTabs } from "../../hooks/useBrainsTabs";
import { BrainSearchBar } from "../BrainSearchBar";
import { BrainsList } from "../BrainsList/BrainsList";
export const ManageBrains = (): JSX.Element => {
const { searchQuery, isFetchingBrains, setSearchQuery, brains } =
useBrainsTabs();
if (isFetchingBrains && brains.length === 0) {
return (
<div className="flex w-full h-full justify-center items-center">
<Spinner />
</div>
);
}
return (
<div className={styles.manage_brains_wrapper}>
<div className={styles.search_brain}>
<BrainSearchBar
searchQuery={searchQuery}
setSearchQuery={setSearchQuery}
/>
</div>
<BrainsList brains={brains} />
</div>
);
};

View File

@ -0,0 +1,16 @@
@use "@/styles/Spacings.module.scss";
.page_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
width: 100%;
height: 100vh;
overflow: hidden;
.content_wrapper {
padding-inline: Spacings.$spacing09;
padding-block: Spacings.$spacing05;
overflow-y: scroll;
}
}

View File

@ -0,0 +1,68 @@
"use client";
import { useState } from "react";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { ButtonType } from "@/lib/types/QuivrButton";
import { Tab } from "@/lib/types/Tab";
import { ManageBrains } from "./components/BrainsTabs/components/ManageBrains/ManageBrains";
import styles from "./page.module.scss";
const Studio = (): JSX.Element => {
const [selectedTab, setSelectedTab] = useState("Manage my brains");
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
const studioTabs: Tab[] = [
{
label: "Manage my brains",
isSelected: selectedTab === "Manage my brains",
onClick: () => setSelectedTab("Manage my brains"),
},
{
label: "Analytics - Coming soon",
isSelected: selectedTab === "Analytics",
onClick: () => setSelectedTab("Analytics"),
disabled: true,
},
];
const buttons: ButtonType[] = [
{
label: "Create brain",
color: "primary",
onClick: () => {
setIsBrainCreationModalOpened(true);
},
},
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
},
];
return (
<div className={styles.page_wrapper}>
<div className={styles.page_header}>
<PageHeader iconName="brainCircuit" label="Studio" buttons={buttons} />
</div>
<div className={styles.content_wrapper}>
<Tabs tabList={studioTabs} />
{selectedTab === "Manage my brains" && <ManageBrains />}
</div>
<UploadDocumentModal />
<AddBrainModal />
</div>
);
};
export default Studio;

View File

@ -1,56 +0,0 @@
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
import TextButton from "@/lib/components/ui/TextButton/TextButton";
import { useLogoutModal } from "./hooks/useLogoutModal";
export const LogoutModal = (): JSX.Element => {
const { t } = useTranslation(["translation", "logout"]);
const {
handleLogout,
isLoggingOut,
isLogoutModalOpened,
setIsLogoutModalOpened,
} = useLogoutModal();
return (
<Modal
Trigger={
<div onClick={() => void 0}>
<TextButton
iconName="logout"
color="dangerous"
label={t("logoutButton")}
/>
</div>
}
isOpen={isLogoutModalOpened}
setOpen={setIsLogoutModalOpened}
CloseTrigger={<div />}
>
<div className="text-center flex flex-col items-center gap-5">
<h2 className="text-lg font-medium mb-5">
{t("areYouSure", { ns: "logout" })}
</h2>
<div className="flex gap-5 items-center justify-center">
<Button
onClick={() => setIsLogoutModalOpened(false)}
variant={"primary"}
>
{t("cancel", { ns: "logout" })}
</Button>
<Button
isLoading={isLoggingOut}
variant={"danger"}
onClick={() => void handleLogout()}
data-testid="logout-button"
>
{t("logoutButton")}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -1,40 +0,0 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useLogoutModal } from "../useLogoutModal";
const mockSignOut = vi.fn(() => ({ error: null }));
const mockUseSupabase = () => ({
supabase: {
auth: {
signOut: mockSignOut,
},
},
});
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: vi.fn() }),
}));
vi.mock("@/lib/context/SupabaseProvider", () => ({
useSupabase: () => mockUseSupabase(),
}));
const clearLocalStorageMock = vi.fn();
Object.defineProperty(window, "localStorage", {
value: {
clear: clearLocalStorageMock,
},
});
describe("useLogoutModal", () => {
it("should call signOut", async () => {
const { result } = renderHook(() => useLogoutModal());
await act(() => result.current.handleLogout());
expect(mockSignOut).toHaveBeenCalledTimes(1);
expect(clearLocalStorageMock).toHaveBeenCalledTimes(1);
});
});

View File

@ -6,7 +6,4 @@
flex-direction: column; flex-direction: column;
gap: Spacings.$spacing07; gap: Spacings.$spacing07;
width: auto; width: auto;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
border-radius: Radius.$big;
padding: Spacings.$spacing05;
} }

View File

@ -4,7 +4,6 @@ import styles from "./Settings.module.scss";
import { ApiKeyConfig } from "../ApiKeyConfig"; import { ApiKeyConfig } from "../ApiKeyConfig";
import LanguageSelect from "../LanguageSelect/LanguageSelect"; import LanguageSelect from "../LanguageSelect/LanguageSelect";
import { LogoutModal } from "../LogoutModal/LogoutModal";
type InfoDisplayerProps = { type InfoDisplayerProps = {
email: string; email: string;
@ -18,7 +17,6 @@ export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
</InfoDisplayer> </InfoDisplayer>
<LanguageSelect /> <LanguageSelect />
<ApiKeyConfig /> <ApiKeyConfig />
<LogoutModal />
</div> </div>
); );
}; };

View File

@ -1,45 +0,0 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Typography.module.scss";
.menu_card_container {
padding: Spacings.$spacing05;
border-radius: Radius.$big;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.25);
cursor: pointer;
display: flex;
flex-direction: column;
gap: Spacings.$spacing03;
width: 20%;
@media (max-width: ScreenSizes.$small) {
width: auto;
.title,
.subtitle {
display: none;
}
}
&:hover,
&.selected {
background-color: Colors.$primary-lightest;
}
.first_line_wrapper {
display: flex;
align-items: center;
justify-content: space-between;
.title {
@include Typography.H2;
}
}
.subtitle {
font-size: Typography.$small;
color: Colors.$normal-grey;
}
}

View File

@ -1,39 +0,0 @@
import { useState } from "react";
import { Icon } from "@/lib/components/ui/Icon/Icon";
import styles from "./UserMenuCard.module.scss";
import { UserMenuCardProps } from "../types/types";
export const UserMenuCard = ({
title,
subtitle,
iconName,
selected,
onClick,
}: UserMenuCardProps): JSX.Element => {
const [isHovered, setIsHovered] = useState(false);
return (
<div
className={`
${styles.menu_card_container}
${selected ? styles.selected : ""}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
>
<div className={styles.first_line_wrapper}>
<span className={styles.title}>{title}</span>
<Icon
name={iconName}
size="normal"
color={isHovered ? "primary" : "black"}
/>
</div>
<span className={styles.subtitle}>{subtitle}</span>
</div>
);
};

View File

@ -1,13 +1,9 @@
@use "@/styles/Spacings.module.scss"; @use "@/styles/Spacings.module.scss";
.user_page_container { .user_page_container {
padding: Spacings.$spacing09; padding-inline: Spacings.$spacing09;
padding-block: Spacings.$spacing07;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: Spacings.$spacing09; gap: Spacings.$spacing05;
.left_menu_wrapper {
display: flex;
gap: Spacings.$spacing05;
}
} }

View File

@ -1,51 +1,61 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { Modal } from "@/lib/components/ui/Modal";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";
import { useSupabase } from "@/lib/context/SupabaseProvider"; import { useSupabase } from "@/lib/context/SupabaseProvider";
import { useUserData } from "@/lib/hooks/useUserData"; import { useUserData } from "@/lib/hooks/useUserData";
import { redirectToLogin } from "@/lib/router/redirectToLogin"; import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { ButtonType } from "@/lib/types/QuivrButton";
import { Tab } from "@/lib/types/Tab";
import { BrainsUsage } from "./components/BrainsUsage/BrainsUsage"; import { BrainsUsage } from "./components/BrainsUsage/BrainsUsage";
import { Plan } from "./components/Plan/Plan"; import { Plan } from "./components/Plan/Plan";
import { Settings } from "./components/Settings/Settings"; import { Settings } from "./components/Settings/Settings";
import { UserMenuCard } from "./components/UserMenuCard/UserMenuCard";
import { UserMenuCardProps } from "./components/types/types";
import styles from "./page.module.scss"; import styles from "./page.module.scss";
import { useLogoutModal } from "../../lib/hooks/useLogoutModal";
const UserPage = (): JSX.Element => { const UserPage = (): JSX.Element => {
const [selectedTab, setSelectedTab] = useState("Settings");
const { session } = useSupabase(); const { session } = useSupabase();
const { userData } = useUserData(); const { userData } = useUserData();
const { t } = useTranslation(["translation", "logout"]);
const {
handleLogout,
isLoggingOut,
isLogoutModalOpened,
setIsLogoutModalOpened,
} = useLogoutModal();
const [userMenuCards, setUserMenuCards] = useState<UserMenuCardProps[]>([ const button: ButtonType = {
{ label: "Logout",
title: "Settings", color: "dangerous",
subtitle: "Change your settings", onClick: () => {
iconName: "settings", setIsLogoutModalOpened(true);
selected: true,
}, },
{
title: "Brain Usage",
subtitle: "View your brain usage",
iconName: "graph",
selected: false,
},
{
title: "Plan",
subtitle: "Manage your plan",
iconName: "unlock",
selected: false,
},
]);
const handleCardClick = (index: number) => {
setUserMenuCards(
userMenuCards.map((card, i) => ({
...card,
selected: i === index,
}))
);
}; };
const userTabs: Tab[] = [
{
label: "Settings",
isSelected: selectedTab === "Settings",
onClick: () => setSelectedTab("Settings"),
},
{
label: "Brains Usage",
isSelected: selectedTab === "Brains Usage",
onClick: () => setSelectedTab("Brains Usage"),
},
{
label: "Plan",
isSelected: selectedTab === "Plan",
onClick: () => setSelectedTab("Plan"),
},
];
if (!session || !userData) { if (!session || !userData) {
redirectToLogin(); redirectToLogin();
@ -53,25 +63,41 @@ const UserPage = (): JSX.Element => {
return ( return (
<> <>
<main className={styles.user_page_container}> <div className={styles.page_header}>
<div className={styles.left_menu_wrapper}> <PageHeader iconName="user" label="Profile" buttons={[button]} />
{userMenuCards.map((card, index) => ( </div>
<UserMenuCard <div className={styles.user_page_container}>
key={index} <Tabs tabList={userTabs} />
title={card.title}
subtitle={card.subtitle}
iconName={card.iconName}
selected={card.selected}
onClick={() => handleCardClick(index)}
/>
))}
</div>
<div className={styles.content_wrapper}> <div className={styles.content_wrapper}>
{userMenuCards[0].selected && <Settings email={userData.email} />} {userTabs[0].isSelected && <Settings email={userData.email} />}
{userMenuCards[1].selected && <BrainsUsage />} {userTabs[1].isSelected && <BrainsUsage />}
{userMenuCards[2].selected && <Plan />} {userTabs[2].isSelected && <Plan />}
</div> </div>
</main> </div>
<Modal
isOpen={isLogoutModalOpened}
setOpen={setIsLogoutModalOpened}
CloseTrigger={<div />}
>
<div className="text-center flex flex-col items-center gap-5">
<h2 className="text-lg font-medium mb-5">
{t("areYouSure", { ns: "logout" })}
</h2>
<div className="flex gap-5 items-center justify-center">
<QuivrButton
onClick={() => setIsLogoutModalOpened(false)}
color="primary"
label={t("cancel", { ns: "logout" })}
></QuivrButton>
<QuivrButton
isLoading={isLoggingOut}
color="dangerous"
onClick={() => void handleLogout()}
label={t("logoutButton")}
></QuivrButton>
</div>
</div>
</Modal>
</> </>
); );
}; };

View File

@ -146,8 +146,12 @@ export const getDocsFromQuestion = async (
question: string, question: string,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<ListFilesProps["files"]> => { ): Promise<ListFilesProps["files"]> => {
return (await axiosInstance.post<Record<"docs",ListFilesProps["files"]>>(`/brains/${brainId}/documents`, { return (
question, await axiosInstance.post<Record<"docs", ListFilesProps["files"]>>(
})).data.docs; `/brains/${brainId}/documents`,
} {
question,
}
)
).data.docs;
};

Some files were not shown because too many files have changed in this diff Show More