mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-13 11:12:23 +03:00
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:
parent
ccbe6a826b
commit
a540a201e3
@ -2,107 +2,102 @@
|
||||
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.
|
||||
|
||||
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>
|
||||
<Step title="Create OpenAI Account">
|
||||
<Step title="Create OpenAI Account">
|
||||
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
|
||||
</Step>
|
||||
</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</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`
|
||||
</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`
|
||||
</Step>
|
||||
<Step title="URL">
|
||||
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">
|
||||
<Step title="Create a new 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`
|
||||
</Step>
|
||||
<Step title="URL">
|
||||
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">
|
||||
1. Add `prompt` as a parameter and click on `Required`
|
||||
<ParamField path="prompt" type="string">
|
||||
Description of the image to generate
|
||||
</ParamField>
|
||||
<ParamField path="prompt" type="string">
|
||||
Description of the image to generate
|
||||
</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
|
||||
</ParamField>
|
||||
<ParamField path="size" type="string">
|
||||
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">
|
||||
Either dall-e-3 or dall-e-2. By default dall-e-3
|
||||
</ParamField>
|
||||
<img src="/images/openai-brain-params.png"/>
|
||||
</Step>
|
||||
<Step title="Secrets">
|
||||
<ParamField path="model" type="string">
|
||||
Either dall-e-3 or dall-e-2. By default dall-e-3
|
||||
</ParamField>
|
||||
<img src="/images/openai-brain-params.png" />
|
||||
</Step>
|
||||
<Step title="Secrets">
|
||||
1. Add `Authorization` as a secret
|
||||
<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`
|
||||
<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="Generate your image">
|
||||
<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`
|
||||
<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="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>
|
||||
<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" />
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
||||
If you have any questions or issue with the documentation, feel free to edit or open an issue.
|
||||
|
@ -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,13 +83,15 @@ const AppWithQueryClient = ({ children }: PropsWithChildren): JSX.Element => {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrainProvider>
|
||||
<KnowledgeToFeedProvider>
|
||||
<MenuProvider>
|
||||
<ChatsProvider>
|
||||
<ChatProvider>
|
||||
<App>{children}</App>
|
||||
</ChatProvider>
|
||||
</ChatsProvider>
|
||||
</MenuProvider>
|
||||
<BrainCreationProvider>
|
||||
<MenuProvider>
|
||||
<ChatsProvider>
|
||||
<ChatProvider>
|
||||
<App>{children}</App>
|
||||
</ChatProvider>
|
||||
</ChatsProvider>
|
||||
</MenuProvider>
|
||||
</BrainCreationProvider>
|
||||
</KnowledgeToFeedProvider>
|
||||
</BrainProvider>
|
||||
</QueryClientProvider>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -1,21 +1,24 @@
|
||||
@use "@/styles/Colors.module.scss";
|
||||
@use "@/styles/Spacings.module.scss";
|
||||
|
||||
.chat_page_container {
|
||||
.main_container {
|
||||
display: flex;
|
||||
flex: 1 1 0%;
|
||||
background-color: Colors.$white;
|
||||
padding-block: Spacings.$spacing06;
|
||||
padding-inline: Spacings.$spacing09;
|
||||
display: flex;
|
||||
gap: Spacings.$spacing09;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
&.feeding {
|
||||
background-color: Colors.$chat-bg-gray;
|
||||
}
|
||||
.chat_page_container {
|
||||
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 {
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
.data_panel_wrapper {
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,40 +18,63 @@ 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.chat_page_container ?? ""}
|
||||
${shouldDisplayFeedCard ? styles.feeding ?? "" : ""}
|
||||
`}
|
||||
data-testid="chat-page"
|
||||
{...getRootProps()}
|
||||
>
|
||||
<div className={styles.main_container}>
|
||||
<div className={styles.page_header}>
|
||||
<PageHeader iconName="chat" label="Chat" buttons={buttons} />
|
||||
</div>
|
||||
<div
|
||||
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"
|
||||
)}
|
||||
className={styles.chat_page_container}
|
||||
data-testid="chat-page"
|
||||
{...getRootProps()}
|
||||
>
|
||||
<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">
|
||||
<ChatDialogueArea />
|
||||
<div
|
||||
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>
|
||||
<ActionsBar />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className={styles.data_panel_wrapper}>
|
||||
<DataPanel />
|
||||
</div>
|
||||
)}
|
||||
<UploadDocumentModal />
|
||||
<AddBrainModal />
|
||||
</div>
|
||||
{!isMobile && (
|
||||
<div className={styles.data_panel_wrapper}>
|
||||
<DataPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -6,59 +6,69 @@
|
||||
@use "@/styles/Typography.module.scss";
|
||||
@use "@/styles/Variables.module.scss";
|
||||
|
||||
.search_page_container {
|
||||
background-color: Colors.$white;
|
||||
.main_container {
|
||||
position: relative;
|
||||
width: 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;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
row-gap: Spacings.$spacing05;
|
||||
position: relative;
|
||||
width: 50%;
|
||||
margin-inline: auto;
|
||||
transform: translateY(-#{Variables.$searchBarHeight});
|
||||
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
width: 100%;
|
||||
padding-inline: Spacings.$spacing07;
|
||||
}
|
||||
|
||||
.quivr_logo_wrapper {
|
||||
.main_wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
row-gap: Spacings.$spacing05;
|
||||
width: 50%;
|
||||
margin-inline: auto;
|
||||
transform: translateY(-#{Variables.$searchBarHeight});
|
||||
|
||||
.quivr_text {
|
||||
@include Typography.Big;
|
||||
@media (max-width: ScreenSizes.$small) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,30 +27,54 @@ 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.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 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}>
|
||||
<span>Talk to </span>
|
||||
<span className={styles.quivr_text_primary}>Quivr</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.search_bar_wrapper}>
|
||||
<SearchBar />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.search_bar_wrapper}>
|
||||
<SearchBar />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.shortcuts_card_wrapper}>
|
||||
<div className={styles.shortcut_wrapper}>
|
||||
<span className={styles.shortcut}>@</span>
|
||||
<span>Select a brain</span>
|
||||
</div>
|
||||
<div className={styles.shortcut_wrapper}>
|
||||
<span className={styles.shortcut}>#</span>
|
||||
<span>Select a prompt</span>
|
||||
<div className={styles.shortcuts_card_wrapper}>
|
||||
<div className={styles.shortcut_wrapper}>
|
||||
<span className={styles.shortcut}>@</span>
|
||||
<span>Select a brain</span>
|
||||
</div>
|
||||
<div className={styles.shortcut_wrapper}>
|
||||
<span className={styles.shortcut}>#</span>
|
||||
<span>Select a prompt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UploadDocumentModal />
|
||||
<AddBrainModal />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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";
|
@ -23,7 +23,7 @@ export const useBrainFetcher = ({ brainId }: UseBrainFetcherProps) => {
|
||||
|
||||
return await getBrain(brainId);
|
||||
} catch (error) {
|
||||
router.push("/brains-management");
|
||||
router.push("/studio");
|
||||
}
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
@ -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")}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
16
frontend/app/studio/page.module.scss
Normal file
16
frontend/app/studio/page.module.scss
Normal 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;
|
||||
}
|
||||
}
|
68
frontend/app/studio/page.tsx
Normal file
68
frontend/app/studio/page.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -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;
|
||||
}
|
||||
gap: Spacings.$spacing05;
|
||||
}
|
||||
|
@ -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>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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`, {
|
||||
question,
|
||||
})).data.docs;
|
||||
}
|
||||
|
||||
return (
|
||||
await axiosInstance.post<Record<"docs", ListFilesProps["files"]>>(
|
||||
`/brains/${brainId}/documents`,
|
||||
{
|
||||
question,
|
||||
}
|
||||
)
|
||||
).data.docs;
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user