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,15 +2,15 @@
title: Generate Images
---
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.
## OpenAI
<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>
<Steps>
@ -18,46 +18,43 @@ In order to have image generation in Quivr we will create a brain that is connec
Create an account on [OpenAI](https://platform.openai.com/)
</Step>
<Step title="Credit Card">
To get access to image generation, add your credit card in the `billing` section
</Step>
<Step title="Create API Key">
Create on API Key and save the result
To get access to image generation, add your credit card in the `billing`
section
</Step>
<Step title="Create API Key">Create on API Key and save the result</Step>
</Steps>
<Warning>
Do not share your API Key
</Warning>
<Warning>Do not share your API Key</Warning>
## 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.
<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>
<Steps>
<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
Brain`
</Step>
<Step title="Choose App Brain">
Select `App (Through API)` and click on `Next`
</Step>
<Step title="Name & Prompt">
1. Give your brain a name.
2. Add this as your description.
```
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.
Do not remove the signature and password in the url of the image as it is a signed url.
Also return the revised prompt below the image in italic.
If the user ask for a modification use the previous prompt generated as base
```
3. Click on `Next`
1. Give your brain a name. 2. Add this as your description. ``` 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. Do not remove the signature and password in the url of
the image as it is a signed url. Also return the revised prompt below the
image in italic. If the user ask for a modification use the previous prompt
generated as base ``` 3. Click on `Next`
</Step>
<Step title="URL">
1. Select `POST` as the method
2. Add this as the url `https://api.openai.com/v1/images/generations`
1. Select `POST` as the method 2. Add this as the url
`https://api.openai.com/v1/images/generations`
<img src="/images/openai-brain-url.png" />
</Step>
<Step title="Parameters">
@ -67,7 +64,8 @@ Generating images costs around 2cts per image. Be carefull or you'll have a surp
</ParamField>
2. Add `size` as a parameter and click on `Required`
<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),
1024x1792 (vertical) or 1792x1024 (horizontal) pixels
</ParamField>
3. Add `model` as a parameter and click on `Required`
<ParamField path="model" type="string">
@ -80,29 +78,26 @@ Generating images costs around 2cts per image. Be carefull or you'll have a surp
<ParamField path="Authorization" type="string">
Bearer YOUR_API_KEY
</ParamField>
**Add your API Key in the value**
2. Add `Content-Type` as a secret and click on `Required`
**Add your API Key in the value** 2. Add `Content-Type` as a secret and
click on `Required`
<ParamField path="Content-Type" type="string">
application/json
</ParamField>
**Add `application/json` in the value**
<img src="/images/openai-brain-secrets.png" />
</Step>
<Step title="Create">
Click on `Create`
</Step>
<Step title="Create">Click on `Create`</Step>
<Step title="Generate your image">
Go to the chat and ask a question to your newly created brain.
<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>
<img src="/images/openai-brain-chat.png" />
</Step>
</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.
Don't hesitate to share your creations on [Twitter](https://twitter.com/quivr_brain).
<Snippet file="commercial.mdx" />

View File

@ -2,10 +2,7 @@
title: Brains
---
<Info>
A few brains were harmed in the making of this documentation 🤯😏
</Info>
<Info>A few brains were harmed in the making of this documentation 🤯😏</Info>
# Introduction to Brains
@ -22,7 +19,6 @@ A few brains were harmed in the making of this documentation 🤯😏
</Steps>
# Two kinds of Brains
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>
<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>
We introduced the two kinds of brains in the previous section. Let's dive a little deeper into each of them.
# 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>
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>
<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"/>
</Frame>
</Step>
</Steps>
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 { PropsWithChildren, useEffect } from "react";
import { BrainCreationProvider } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import { NotificationBanner } from "@/lib/components/NotificationBanner";
@ -82,6 +83,7 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
<QueryClientProvider client={queryClient}>
<BrainProvider>
<KnowledgeToFeedProvider>
<BrainCreationProvider>
<MenuProvider>
<ChatsProvider>
<ChatProvider>
@ -89,6 +91,7 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
</ChatProvider>
</ChatsProvider>
</MenuProvider>
</BrainCreationProvider>
</KnowledgeToFeedProvider>
</BrainProvider>
</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 { 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 { MentionItem } from "./components/MentionItem/MentionItem";
@ -23,6 +24,7 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
const { suggestionsRef, shouldShowScrollToBottomIcon, scrollToBottom } =
useSuggestionsOverflowHandler();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault();
@ -56,7 +58,14 @@ export const MentionList = forwardRef<MentionListRef, MentionListProps>(
onClick={scrollToBottom}
/>
)}
{isBrain && <AddBrainModal />}
{isBrain && (
<TextButton
label="Create Brain"
iconName="add"
color="black"
onClick={() => setIsBrainCreationModalOpened(true)}
/>
)}
{isPrompt && <AddNewPromptButton />}
</div>
</div>

View File

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

View File

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

View File

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

View File

@ -1,8 +1,13 @@
"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 { useDevice } from "@/lib/hooks/useDevice";
import { useCustomDropzone } from "@/lib/hooks/useDropzone";
import { ButtonType } from "@/lib/types/QuivrButton";
import { cn } from "@/lib/utils";
import { ActionsBar } from "./components/ActionsBar";
@ -13,17 +18,37 @@ import styles from "./page.module.scss";
const SelectedChatPage = (): JSX.Element => {
const { getRootProps } = useCustomDropzone();
const { shouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { isMobile } = useDevice();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
useChatNotificationsSync();
const buttons: ButtonType[] = [
{
label: "Create brain",
color: "primary",
onClick: () => {
setIsBrainCreationModalOpened(true);
},
},
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
},
];
return (
<div className={styles.main_container}>
<div className={styles.page_header}>
<PageHeader iconName="chat" label="Chat" buttons={buttons} />
</div>
<div
className={`
${styles.chat_page_container ?? ""}
${shouldDisplayFeedCard ? styles.feeding ?? "" : ""}
`}
className={styles.chat_page_container}
data-testid="chat-page"
{...getRootProps()}
>
@ -47,6 +72,9 @@ const SelectedChatPage = (): JSX.Element => {
<DataPanel />
</div>
)}
<UploadDocumentModal />
<AddBrainModal />
</div>
</div>
);
};

View File

@ -6,20 +6,29 @@
@use "@/styles/Typography.module.scss";
@use "@/styles/Variables.module.scss";
.main_container {
position: relative;
width: 100%;
height: 100%;
.page_header {
position: absolute;
width: 100%;
}
.search_page_container {
background-color: Colors.$white;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
flex-direction: column;
.main_container {
.main_wrapper {
display: flex;
flex-direction: column;
row-gap: Spacings.$spacing05;
position: relative;
width: 50%;
margin-inline: auto;
transform: translateY(-#{Variables.$searchBarHeight});
@ -62,3 +71,4 @@
}
}
}
}

View File

@ -3,15 +3,23 @@ import { usePathname } from "next/navigation";
import { useEffect } from "react";
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 { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { ButtonType } from "@/lib/types/QuivrButton";
import styles from "./page.module.scss";
const Search = (): JSX.Element => {
const pathname = usePathname();
const { session } = useSupabase();
const { setShouldDisplayFeedCard } = useKnowledgeToFeedContext();
const { setIsBrainCreationModalOpened } = useBrainCreationContext();
useEffect(() => {
if (session === null) {
@ -19,9 +27,30 @@ const Search = (): JSX.Element => {
}
}, [pathname, session]);
const buttons: ButtonType[] = [
{
label: "Create brain",
color: "primary",
onClick: () => {
setIsBrainCreationModalOpened(true);
},
},
{
label: "Add knowledge",
color: "primary",
onClick: () => {
setShouldDisplayFeedCard(true);
},
},
];
return (
<div className={styles.search_page_container}>
<div className={styles.main_container}>
<div className={styles.page_header}>
<PageHeader iconName="home" label="Home" buttons={buttons} />
</div>
<div className={styles.search_page_container}>
<div className={styles.main_wrapper}>
<div className={styles.quivr_logo_wrapper}>
<QuivrLogo size={80} color="black" />
<div className={styles.quivr_text}>
@ -44,6 +73,9 @@ const Search = (): JSX.Element => {
</div>
</div>
</div>
<UploadDocumentModal />
<AddBrainModal />
</div>
);
};

View File

@ -1,7 +1,7 @@
/* eslint-disable max-lines */
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 Spinner from "@/lib/components/ui/Spinner";
import { Tabs, TabsContent, TabsList } from "@/lib/components/ui/Tabs";

View File

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

View File

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

View File

@ -16,7 +16,7 @@ const BrainsManagement = (): JSX.Element => {
return (
<div className="flex flex-col w-full p-5 lg:p-20 bg-highlight">
<div>
<Link href="/brains-management">
<Link href="/studio">
<Button variant="tertiary" className="p-0">
<LuChevronLeftCircle className="text-primary" />
{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;
gap: Spacings.$spacing07;
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 LanguageSelect from "../LanguageSelect/LanguageSelect";
import { LogoutModal } from "../LogoutModal/LogoutModal";
type InfoDisplayerProps = {
email: string;
@ -18,7 +17,6 @@ export const Settings = ({ email }: InfoDisplayerProps): JSX.Element => {
</InfoDisplayer>
<LanguageSelect />
<ApiKeyConfig />
<LogoutModal />
</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";
.user_page_container {
padding: Spacings.$spacing09;
padding-inline: Spacings.$spacing09;
padding-block: Spacings.$spacing07;
display: flex;
flex-direction: column;
gap: Spacings.$spacing09;
.left_menu_wrapper {
display: flex;
gap: Spacings.$spacing05;
}
}

View File

@ -1,51 +1,61 @@
"use client";
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 { useUserData } from "@/lib/hooks/useUserData";
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 { Plan } from "./components/Plan/Plan";
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 { useLogoutModal } from "../../lib/hooks/useLogoutModal";
const UserPage = (): JSX.Element => {
const [selectedTab, setSelectedTab] = useState("Settings");
const { session } = useSupabase();
const { userData } = useUserData();
const { t } = useTranslation(["translation", "logout"]);
const {
handleLogout,
isLoggingOut,
isLogoutModalOpened,
setIsLogoutModalOpened,
} = useLogoutModal();
const [userMenuCards, setUserMenuCards] = useState<UserMenuCardProps[]>([
{
title: "Settings",
subtitle: "Change your settings",
iconName: "settings",
selected: true,
const button: ButtonType = {
label: "Logout",
color: "dangerous",
onClick: () => {
setIsLogoutModalOpened(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) {
redirectToLogin();
@ -53,25 +63,41 @@ const UserPage = (): JSX.Element => {
return (
<>
<main className={styles.user_page_container}>
<div className={styles.left_menu_wrapper}>
{userMenuCards.map((card, index) => (
<UserMenuCard
key={index}
title={card.title}
subtitle={card.subtitle}
iconName={card.iconName}
selected={card.selected}
onClick={() => handleCardClick(index)}
/>
))}
<div className={styles.page_header}>
<PageHeader iconName="user" label="Profile" buttons={[button]} />
</div>
<div className={styles.user_page_container}>
<Tabs tabList={userTabs} />
<div className={styles.content_wrapper}>
{userMenuCards[0].selected && <Settings email={userData.email} />}
{userMenuCards[1].selected && <BrainsUsage />}
{userMenuCards[2].selected && <Plan />}
{userTabs[0].isSelected && <Settings email={userData.email} />}
{userTabs[1].isSelected && <BrainsUsage />}
{userTabs[2].isSelected && <Plan />}
</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,
axiosInstance: AxiosInstance
): Promise<ListFilesProps["files"]> => {
return (await axiosInstance.post<Record<"docs",ListFilesProps["files"]>>(`/brains/${brainId}/documents`, {
return (
await axiosInstance.post<Record<"docs", ListFilesProps["files"]>>(
`/brains/${brainId}/documents`,
{
question,
})).data.docs;
}
)
).data.docs;
};

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