feat(frontend): Quivr Assistants (#2448)

# 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):


<!--
ELLIPSIS_HIDDEN
-->
----

| 🚀 This description was created by
[Ellipsis](https://www.ellipsis.dev) for commit
3e95f1a5aa |
|--------|

### Summary:
This PR introduces the 'Quivr Assistants' feature with new frontend
components and pages, backend routes and DTOs changes, and a minor
update in the `processAssistant` function.

**Key points**:
- Introduced the 'Quivr Assistants' feature with new frontend components
and pages, backend routes and DTOs changes.
- Added new `AssistantModal` component in
`/frontend/app/assistants/AssistantModal/AssistantModal.tsx`.
- Added new `InputsStep` and `OutputsStep` components for handling
assistant inputs and outputs.
- Added new `AssistantModal` page in
`/frontend/app/assistants/page.tsx`.
- Added new API endpoints and types for assistants in
`/frontend/lib/api/assistants/assistants.ts` and
`/frontend/lib/api/assistants/types.ts`.
- Updated backend assistant routes and DTOs in
`backend/modules/assistant/controller/assistant_routes.py`,
`backend/modules/assistant/dto/inputs.py`,
`backend/modules/assistant/ito/difference.py`, and
`backend/modules/assistant/ito/summary.py`.
- Made a minor update in the `processAssistant` function in the
`/frontend/lib/api/assistants/assistants.ts` file.


----
Generated with ❤️ by [ellipsis.dev](https://www.ellipsis.dev)

<!--
ELLIPSIS_HIDDEN
-->
This commit is contained in:
Antoine Dewez 2024-04-19 10:36:36 +02:00 committed by GitHub
parent 5876bcfcdc
commit 803f304390
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 890 additions and 127 deletions

View File

@ -5,7 +5,7 @@ from logger import get_logger
from middlewares.auth import AuthBearer, get_current_user
from modules.assistant.dto.inputs import InputAssistant
from modules.assistant.dto.outputs import AssistantOutput
from modules.assistant.ito.difference import DifferenceAssistant, difference_inputs
from modules.assistant.ito.difference import DifferenceAssistant
from modules.assistant.ito.summary import SummaryAssistant, summary_inputs
from modules.assistant.service.assistant import Assistant
from modules.user.entity.user_identity import UserIdentity
@ -43,7 +43,7 @@ async def process_assistant(
files: List[UploadFile] = None,
current_user: UserIdentity = Depends(get_current_user),
):
if input.name == "summary":
if input.name.lower() == "summary":
summary_assistant = SummaryAssistant(
input=input, files=files, current_user=current_user
)
@ -52,7 +52,7 @@ async def process_assistant(
return await summary_assistant.process_assistant()
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
elif input.name == "difference":
elif input.name.lower() == "difference":
difference_assistant = DifferenceAssistant(
input=input, files=files, current_user=current_user
)

View File

@ -142,7 +142,7 @@ def difference_inputs():
tags=["new"],
input_description="Two documents to compare",
output_description="The difference between the two documents",
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/assistant_summary_434446a2aa.png",
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/report_94bea8b918.png",
inputs=Inputs(
files=[
InputFile(

View File

@ -169,7 +169,7 @@ def summary_inputs():
tags=["new"],
input_description="One document to summarize",
output_description="A summary of the document",
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/assistant_summary_434446a2aa.png",
icon_url="https://quivr-cms.s3.eu-west-3.amazonaws.com/report_94bea8b918.png",
inputs=Inputs(
files=[
InputFile(

View File

@ -0,0 +1,29 @@
@use "@/styles/Spacings.module.scss";
.modal_content_container {
padding: Spacings.$spacing05;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
.modal_content_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
.message_wrapper {
display: flex;
flex-direction: column;
}
.title {
font-weight: 600;
}
}
.button {
display: flex;
align-self: flex-end;
}
}

View File

@ -0,0 +1,151 @@
import { useState } from "react";
import { Assistant } from "@/lib/api/assistants/types";
import { useAssistants } from "@/lib/api/assistants/useAssistants";
import { Stepper } from "@/lib/components/AddBrainModal/components/Stepper/Stepper";
import { StepValue } from "@/lib/components/AddBrainModal/types/types";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import { Modal } from "@/lib/components/ui/Modal/Modal";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { Step } from "@/lib/types/Modal";
import styles from "./AssistantModal.module.scss";
import { InputsStep } from "./InputsStep/InputsStep";
import { OutputsStep } from "./OutputsStep/OutputsStep";
interface AssistantModalProps {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
assistant: Assistant;
}
export const AssistantModal = ({
isOpen,
setIsOpen,
assistant,
}: AssistantModalProps): JSX.Element => {
const steps: Step[] = [
{
label: "Inputs",
value: "FIRST_STEP",
},
{
label: "Outputs",
value: "SECOND_STEP",
},
];
const [currentStep, setCurrentStep] = useState<StepValue>("FIRST_STEP");
const [emailOutput, setEmailOutput] = useState<boolean>(true);
const [brainOutput, setBrainOutput] = useState<string>("");
const [files, setFiles] = useState<{ key: string; file: File | null }[]>(
assistant.inputs.files.map((fileInput) => ({
key: fileInput.key,
file: null,
}))
);
const { processAssistant } = useAssistants();
const handleFileChange = (file: File, inputKey: string) => {
setFiles((prevFiles) =>
prevFiles.map((fileObj) =>
fileObj.key === inputKey ? { ...fileObj, file } : fileObj
)
);
};
const handleSetIsOpen = (value: boolean) => {
if (!value) {
setCurrentStep("FIRST_STEP");
}
setIsOpen(value);
};
const handleProcessAssistant = async () => {
handleSetIsOpen(false);
await processAssistant(
{
name: assistant.name,
inputs: {
files: files.map((file) => ({
key: file.key,
value: (file.file as File).name,
})),
urls: [],
texts: [],
},
outputs: {
email: {
activated: emailOutput,
},
brain: {
activated: brainOutput !== "",
value: brainOutput,
},
},
},
files.map((file) => file.file as File)
);
};
return (
<Modal
title={assistant.name}
desc={assistant.description}
isOpen={isOpen}
setOpen={handleSetIsOpen}
size="big"
CloseTrigger={<div />}
>
<div className={styles.modal_content_container}>
<div className={styles.modal_content_wrapper}>
<Stepper steps={steps} currentStep={currentStep} />
{currentStep === "FIRST_STEP" ? (
<MessageInfoBox type="tutorial">
<div className={styles.message_wrapper}>
<span className={styles.title}>Expected Input</span>
{assistant.input_description}
</div>
</MessageInfoBox>
) : (
<MessageInfoBox type="tutorial">
<div className={styles.message_wrapper}>
<span className={styles.title}>Output</span>
{assistant.output_description}
</div>
</MessageInfoBox>
)}
{currentStep === "FIRST_STEP" ? (
<InputsStep
inputs={assistant.inputs}
onFileChange={handleFileChange}
/>
) : (
<OutputsStep
setEmailOutput={setEmailOutput}
setBrainOutput={setBrainOutput}
/>
)}
</div>
<div className={styles.button}>
{currentStep === "FIRST_STEP" ? (
<QuivrButton
label="Next"
color="primary"
iconName="chevronRight"
onClick={() => setCurrentStep("SECOND_STEP")}
disabled={!!files.find((file) => !file.file)}
/>
) : (
<QuivrButton
label="Process"
color="primary"
iconName="chevronRight"
onClick={() => handleProcessAssistant()}
disabled={!emailOutput && brainOutput === ""}
/>
)}
</div>
</div>
</Modal>
);
};

View File

@ -0,0 +1,28 @@
import { capitalCase } from "change-case";
import { AssistantInputs } from "@/lib/api/assistants/types";
import { FileInput } from "@/lib/components/ui/FileInput/FileInput";
interface InputsStepProps {
inputs: AssistantInputs;
onFileChange: (file: File, inputKey: string) => void; //
}
export const InputsStep = ({
inputs,
onFileChange,
}: InputsStepProps): JSX.Element => {
return (
<div>
{inputs.files.map((fileInput) => (
<FileInput
key={fileInput.key}
label={capitalCase(fileInput.key)}
icon="file"
acceptedFileTypes={fileInput.allowed_extensions}
onFileChange={(file) => onFileChange(file, fileInput.key)}
/>
))}
</div>
);
};

View File

@ -0,0 +1,16 @@
@use "@/styles/Spacings.module.scss";
.outputs_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing03;
.message_wrapper {
width: 100%;
}
.brain_selector {
padding-block: Spacings.$spacing02;
max-width: 250px;
}
}

View File

@ -0,0 +1,83 @@
import { useMemo, useState } from "react";
import { formatMinimalBrainsToSelectComponentInput } from "@/app/chat/[chatId]/components/ActionsBar/components/KnowledgeToFeed/utils/formatMinimalBrainsToSelectComponentInput";
import { Checkbox } from "@/lib/components/ui/Checkbox/Checkbox";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import { SingleSelector } from "@/lib/components/ui/SingleSelector/SingleSelector";
import { requiredRolesForUpload } from "@/lib/config/upload";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import styles from "./OutputsStep.module.scss";
interface OutputsStepProps {
setEmailOutput: (value: boolean) => void;
setBrainOutput: (value: string) => void;
}
export const OutputsStep = ({
setEmailOutput,
setBrainOutput,
}: OutputsStepProps): JSX.Element => {
const [existingBrainChecked, setExistingBrainChecked] =
useState<boolean>(false);
const [selectedBrainId, setSelectedBrainId] = useState<string>("");
const { allBrains } = useBrainContext();
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
useMemo(
() =>
allBrains.filter(
(brain) =>
requiredRolesForUpload.includes(brain.role) && !!brain.max_files
),
[allBrains]
)
);
return (
<div className={styles.outputs_wrapper}>
<MessageInfoBox type="info">
It can take a few minutes to process.
</MessageInfoBox>
<Checkbox
label="Receive the results by Email"
checked={true}
setChecked={setEmailOutput}
/>
<Checkbox
label="Upload the results on an existing Brain"
checked={existingBrainChecked}
setChecked={() => {
if (existingBrainChecked) {
setBrainOutput("");
setSelectedBrainId("");
}
setExistingBrainChecked(!existingBrainChecked);
}}
/>
{existingBrainChecked && (
<div className={styles.brain_selector}>
<SingleSelector
options={brainsWithUploadRights}
onChange={(brain) => {
setBrainOutput(brain);
setSelectedBrainId(brain);
}}
selectedOption={
selectedBrainId
? {
value: selectedBrainId,
label: allBrains.find(
(brain) => brain.id === selectedBrainId
)?.name as string,
}
: undefined
}
placeholder="Select a brain"
iconName="brain"
/>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,14 @@
@use "@/styles/Spacings.module.scss";
.content_wrapper {
padding: Spacings.$spacing06;
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
.assistants_grid {
display: flex;
gap: Spacings.$spacing03;
flex-wrap: wrap;
}
}

View File

@ -0,0 +1,94 @@
"use client";
import { usePathname } from "next/navigation";
import { useEffect, useState } from "react";
import { Assistant } from "@/lib/api/assistants/types";
import { useAssistants } from "@/lib/api/assistants/useAssistants";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { BrainCard } from "@/lib/components/ui/BrainCard/BrainCard";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import { useSupabase } from "@/lib/context/SupabaseProvider";
import { redirectToLogin } from "@/lib/router/redirectToLogin";
import { AssistantModal } from "./AssistantModal/AssistantModal";
import styles from "./page.module.scss";
const Search = (): JSX.Element => {
const pathname = usePathname();
const { session } = useSupabase();
const [assistants, setAssistants] = useState<Assistant[]>([]);
const [assistantModalOpened, setAssistantModalOpened] =
useState<boolean>(false);
const [currentAssistant, setCurrentAssistant] = useState<Assistant | null>(
null
);
const { getAssistants } = useAssistants();
useEffect(() => {
if (session === null) {
redirectToLogin();
}
void (async () => {
try {
const res = await getAssistants();
if (res) {
setAssistants(res);
}
} catch (error) {
console.error(error);
}
})();
}, [pathname, session]);
return (
<>
<div className={styles.page_header}>
<PageHeader
iconName="assistant"
label="Quivr Assistants"
buttons={[]}
/>
<div className={styles.content_wrapper}>
<div className={styles.message_wrapper}>
<MessageInfoBox type="info">
<span>
Quivr assistants are AI-driven agents that apply specific
processes to an input in order to generate a usable output.{" "}
<br></br>This output can be directly uploaded to a digital brain
or sent via email.{" "}
</span>
</MessageInfoBox>
</div>
<div className={styles.assistants_grid}>
{assistants.map((assistant) => {
return (
<BrainCard
tooltip={assistant.description}
brainName={assistant.name}
tags={assistant.tags}
imageUrl={assistant.icon_url}
callback={() => {
setAssistantModalOpened(true);
setCurrentAssistant(assistant);
}}
key={assistant.name}
/>
);
})}
</div>
</div>
</div>
{currentAssistant && (
<AssistantModal
isOpen={assistantModalOpened}
setIsOpen={setAssistantModalOpened}
assistant={currentAssistant}
/>
)}
</>
);
};
export default Search;

View File

@ -0,0 +1,38 @@
import { AxiosInstance } from "axios";
import { Assistant, ProcessAssistantRequest } from "./types";
export const getAssistants = async (
axiosInstance: AxiosInstance
): Promise<Assistant[] | undefined> => {
return (await axiosInstance.get<Assistant[] | undefined>("/assistants")).data;
};
export const processAssistant = async (
axiosInstance: AxiosInstance,
input: ProcessAssistantRequest,
files: File[]
): Promise<string | undefined> => {
const formData = new FormData();
formData.append(
"input",
JSON.stringify({
name: input.name,
inputs: {
files: input.inputs.files,
urls: input.inputs.urls,
texts: input.inputs.texts,
},
outputs: input.outputs,
})
);
files.forEach((file) => {
formData.append("files", file);
});
return (
await axiosInstance.post<string | undefined>("/assistant/process", formData)
).data;
};

View File

@ -0,0 +1,64 @@
interface AssistantInput {
key: string;
required: boolean;
description: string;
}
interface FilesInputAssistant extends AssistantInput {
allowed_extensions: string[];
}
export interface AssistantInputs {
files: FilesInputAssistant[];
urls: AssistantInput[];
texts: AssistantInput[];
}
interface AssistantOutput {
required: boolean;
description: string;
type: string;
}
interface AssistantOutputs {
email: AssistantOutput;
brain: AssistantOutput;
}
export interface Assistant {
name: string;
input_description: string;
output_description: string;
inputs: AssistantInputs;
outputs: AssistantOutputs;
tags: string[];
icon_url: string;
description: string;
}
export interface ProcessAssistantRequest {
name: string;
inputs: {
files: {
key: string;
value: string;
}[];
urls: {
key: string;
value: string;
}[];
texts: {
key: string;
value: string;
}[];
};
outputs: {
email: {
activated: boolean;
};
brain: {
activated: boolean;
value: string;
};
};
}

View File

@ -0,0 +1,15 @@
import { useAxios } from "@/lib/hooks";
import { getAssistants, processAssistant } from "./assistants";
import { ProcessAssistantRequest } from "./types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useAssistants = () => {
const { axiosInstance } = useAxios();
return {
getAssistants: async () => getAssistants(axiosInstance),
processAssistant: async (input: ProcessAssistantRequest, files: File[]) =>
processAssistant(axiosInstance, input, files),
};
};

View File

@ -12,11 +12,13 @@ import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInf
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
import { CreateBrainStep } from "./components/CreateBrainStep/CreateBrainStep";
import { Stepper } from "./components/Stepper/Stepper";
import { useBrainCreationSteps } from "./hooks/useBrainCreationSteps";
import { CreateBrainProps } from "./types/types";
export const AddBrainModal = (): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const { userIdentityData } = useUserData();
const { currentStep, steps } = useBrainCreationSteps();
const {
isBrainCreationModalOpened,
@ -27,7 +29,7 @@ export const AddBrainModal = (): JSX.Element => {
const defaultValues: CreateBrainProps = {
...addBrainDefaultValues,
setDefault: true,
brainCreationStep: "BRAIN_TYPE",
brainCreationStep: "FIRST_STEP",
};
const methods = useForm<CreateBrainProps>({
@ -51,7 +53,7 @@ export const AddBrainModal = (): JSX.Element => {
>
<div className={styles.add_brain_modal_container}>
<div className={styles.stepper_container}>
<Stepper />
<Stepper currentStep={currentStep} steps={steps} />
</div>
<div className={styles.content_wrapper}>
<BrainTypeSelectionStep />

View File

@ -2,6 +2,8 @@ import { createContext, useContext, useState } from "react";
import { IntegrationBrains } from "@/lib/api/brain/types";
import { StepValue } from "./types/types";
interface BrainCreationContextProps {
isBrainCreationModalOpened: boolean;
setIsBrainCreationModalOpened: React.Dispatch<React.SetStateAction<boolean>>;
@ -11,6 +13,8 @@ interface BrainCreationContextProps {
setCurrentSelectedBrain: React.Dispatch<
React.SetStateAction<IntegrationBrains | undefined>
>;
currentStep: StepValue;
setCurrentStep: React.Dispatch<React.SetStateAction<StepValue>>;
}
export const BrainCreationContext = createContext<
@ -27,6 +31,7 @@ export const BrainCreationProvider = ({
const [currentSelectedBrain, setCurrentSelectedBrain] =
useState<IntegrationBrains>();
const [creating, setCreating] = useState<boolean>(false);
const [currentStep, setCurrentStep] = useState<StepValue>("FIRST_STEP");
return (
<BrainCreationContext.Provider
@ -37,6 +42,8 @@ export const BrainCreationProvider = ({
setCreating,
currentSelectedBrain,
setCurrentSelectedBrain,
currentStep,
setCurrentStep,
}}
>
{children}

View File

@ -1,5 +1,3 @@
@use "@/styles/Radius.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
@ -18,55 +16,5 @@
display: flex;
gap: Spacings.$spacing03;
flex-wrap: wrap;
.brain_card_container {
display: flex;
flex-direction: column;
gap: Spacings.$spacing02;
&.disabled {
pointer-events: none;
opacity: 0.1;
}
.tag_wrapper {
height: 2rem;
}
.brain_card_wrapper {
display: flex;
flex-direction: column;
align-items: center;
border-radius: Radius.$normal;
gap: Spacings.$spacing03;
padding: Spacings.$spacing04;
width: fit-content;
cursor: pointer;
width: 120px;
.dark_image {
filter: invert(100%);
}
.brain_title {
@include Typography.EllipsisOverflow;
font-size: Typography.$small;
font-weight: 500;
width: 100%;
display: flex;
justify-content: center;
}
&:hover,
&.selected {
border-color: var(--primary-0);
background-color: var(--background-special-0);
.brain_title {
color: var(--primary-0);
}
}
}
}
}
}

View File

@ -1,11 +1,6 @@
import { capitalCase } from "change-case";
import Image from "next/image";
import { IntegrationBrains } from "@/lib/api/brain/types";
import { BrainCard } from "@/lib/components/ui/BrainCard/BrainCard";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import { Tag } from "@/lib/components/ui/Tag/Tag";
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import { useUserData } from "@/lib/hooks/useUserData";
import styles from "./BrainCatalogue.module.scss";
@ -21,7 +16,6 @@ export const BrainCatalogue = ({
}): JSX.Element => {
const { setCurrentSelectedBrain, currentSelectedBrain } =
useBrainCreationContext();
const { isDarkMode } = useUserSettingsContext();
const { userIdentityData } = useUserData();
return (
@ -45,42 +39,19 @@ export const BrainCatalogue = ({
<div className={styles.brains_grid}>
{brains.map((brain) => {
return (
<div
key={brain.id}
className={`${styles.brain_card_container} ${
!userIdentityData?.onboarded && !brain.onboarding_brain
? styles.disabled
: ""
}`}
onClick={() => {
<BrainCard
tooltip={brain.description}
brainName={brain.integration_display_name}
tags={brain.tags}
selected={currentSelectedBrain?.id === brain.id}
imageUrl={brain.integration_logo_url}
callback={() => {
next();
setCurrentSelectedBrain(brain);
}}
>
<Tooltip tooltip={brain.description}>
<div
className={`${styles.brain_card_wrapper} ${
currentSelectedBrain === brain ? styles.selected : ""
}`}
>
<Image
className={isDarkMode ? styles.dark_image : ""}
src={brain.integration_logo_url}
alt={brain.integration_name}
width={50}
height={50}
/>
<span className={styles.brain_title}>
{brain.integration_display_name}
</span>
<div className={styles.tag_wrapper}>
{brain.tags[0] && (
<Tag color="primary" name={capitalCase(brain.tags[0])} />
)}
</div>
</div>
</Tooltip>
</div>
key={brain.id}
disabled={!userIdentityData?.onboarded && !brain.onboarding_brain}
/>
);
})}
</div>

View File

@ -2,11 +2,14 @@ import { Icon } from "@/lib/components/ui/Icon/Icon";
import styles from "./Stepper.module.scss";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
import { StepValue } from "../../types/types";
export const Stepper = (): JSX.Element => {
const { currentStep, steps } = useBrainCreationSteps();
interface StepperProps {
currentStep: StepValue;
steps: { value: string; label: string }[];
}
export const Stepper = ({ currentStep, steps }: StepperProps): JSX.Element => {
const currentStepIndex = steps.findIndex(
(step) => step.value === currentStep
);

View File

@ -1,35 +1,31 @@
import { useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import {
CreateBrainProps,
Step,
} from "@/lib/components/AddBrainModal/types/types";
import { Step } from "@/lib/types/Modal";
import { useBrainCreationContext } from "../brainCreation-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainCreationSteps = () => {
const { t } = useTranslation("brain");
const { isBrainCreationModalOpened } = useBrainCreationContext();
const { isBrainCreationModalOpened, currentStep, setCurrentStep } =
useBrainCreationContext();
const steps: Step[] = [
{
label: t("brain_type"),
value: "BRAIN_TYPE",
value: "FIRST_STEP",
},
{
label: t("brain_params"),
value: "BRAIN_PARAMS",
value: "SECOND_STEP",
},
{
label: t("resources"),
value: "KNOWLEDGE",
value: "THIRD_STEP",
},
];
const { watch, setValue } = useFormContext<CreateBrainProps>();
const currentStep = watch("brainCreationStep");
const currentStepIndex = steps.findIndex(
(step) => step.value === currentStep
);
@ -44,7 +40,7 @@ export const useBrainCreationSteps = () => {
}
const nextStep = steps[currentStepIndex + 1];
return setValue("brainCreationStep", nextStep.value);
return setCurrentStep(nextStep.value);
};
const goToPreviousStep = () => {
@ -53,11 +49,11 @@ export const useBrainCreationSteps = () => {
}
const previousStep = steps[currentStepIndex - 1];
return setValue("brainCreationStep", previousStep.value);
return setCurrentStep(previousStep.value);
};
const goToFirstStep = () => {
return setValue("brainCreationStep", steps[0].value);
return setCurrentStep(steps[0].value);
};
return {

View File

@ -1,13 +1,13 @@
import { CreateBrainInput } from "@/lib/api/brain/types";
import { iconList } from "@/lib/helpers/iconList";
const brainCreationSteps = ["BRAIN_TYPE", "BRAIN_PARAMS", "KNOWLEDGE"] as const;
const steps = ["FIRST_STEP", "SECOND_STEP", "THIRD_STEP"] as const;
export type BrainCreationStep = (typeof brainCreationSteps)[number];
export type StepValue = (typeof steps)[number];
export type CreateBrainProps = CreateBrainInput & {
setDefault: boolean;
brainCreationStep: BrainCreationStep;
brainCreationStep: StepValue;
};
export interface BrainType {
@ -17,8 +17,3 @@ export interface BrainType {
disabled?: boolean;
onClick?: () => void;
}
export type Step = {
label: string;
value: BrainCreationStep;
};

View File

@ -11,6 +11,7 @@ import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks
import styles from "./Menu.module.scss";
import { AnimatedDiv } from "./components/AnimationDiv";
import { AssistantsButton } from "./components/AssistantsButton/AssistantsButton";
import { DiscussionButton } from "./components/DiscussionButton/DiscussionButton";
import { HomeButton } from "./components/HomeButton/HomeButton";
import { ProfileButton } from "./components/ProfileButton/ProfileButton";
@ -32,7 +33,14 @@ export const Menu = (): JSX.Element => {
return <></>;
}
const displayedOnPages = ["/chat", "/library", "/studio", "/search", "/user"];
const displayedOnPages = [
"/assistants",
"/chat",
"/library",
"/search",
"studio",
"/user",
];
const isMenuDisplayed = displayedOnPages.some((page) =>
pathname.includes(page)
@ -66,6 +74,7 @@ export const Menu = (): JSX.Element => {
<DiscussionButton />
<HomeButton />
<StudioButton />
<AssistantsButton />
<ThreadsButton />
</div>
<div className={styles.block}>

View File

@ -0,0 +1,21 @@
import Link from "next/link";
import { usePathname } from "next/navigation";
import { MenuButton } from "@/lib/components/Menu/components/MenuButton/MenuButton";
export const AssistantsButton = (): JSX.Element => {
const pathname = usePathname() ?? "";
const isSelected = pathname.includes("/assistants");
return (
<Link href={`/assistants`}>
<MenuButton
label="Quivr Assistants"
isSelected={isSelected}
iconName="assistant"
type="open"
color="primary"
/>
</Link>
);
};

View File

@ -9,6 +9,7 @@
padding: Spacings.$spacing04;
padding-left: Spacings.$spacing09;
border-bottom: 1px solid var(--border-1);
height: 3rem;
.left {
@include Typography.H2;
@ -30,7 +31,6 @@
.buttons_wrapper {
display: flex;
gap: Spacings.$spacing03;
align-self: flex-end;
align-items: center;
}
}

View File

@ -0,0 +1,50 @@
@use "@/styles/Radius.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_card_container {
display: flex;
flex-direction: column;
gap: Spacings.$spacing02;
&.disabled {
pointer-events: none;
opacity: 0.1;
}
.brain_card_wrapper {
display: flex;
flex-direction: column;
align-items: center;
border-radius: Radius.$normal;
gap: Spacings.$spacing03;
padding: Spacings.$spacing04;
width: fit-content;
cursor: pointer;
width: 120px;
.dark_image {
filter: invert(100%);
}
.brain_title {
@include Typography.EllipsisOverflow;
font-size: Typography.$small;
font-weight: 500;
width: 100%;
display: flex;
justify-content: center;
}
&:hover,
&.selected {
border-color: var(--primary-0);
background-color: var(--background-special-0);
.brain_title {
color: var(--primary-0);
}
}
}
}

View File

@ -0,0 +1,65 @@
import { capitalCase } from "change-case";
import Image from "next/image";
import { useUserSettingsContext } from "@/lib/context/UserSettingsProvider/hooks/useUserSettingsContext";
import styles from "./BrainCard.module.scss";
import { Tag } from "../Tag/Tag";
import Tooltip from "../Tooltip/Tooltip";
interface BrainCardProps {
tooltip: string;
selected?: boolean;
imageUrl: string;
brainName: string;
tags: string[];
callback: () => void;
key: string;
disabled?: boolean;
}
export const BrainCard = ({
tooltip,
selected,
imageUrl,
brainName,
tags,
callback,
key,
disabled,
}: BrainCardProps): JSX.Element => {
const { isDarkMode } = useUserSettingsContext();
return (
<div
key={key}
className={`${styles.brain_card_container} ${
disabled ? styles.disabled : ""
}`}
onClick={() => {
callback();
}}
>
<Tooltip tooltip={tooltip}>
<div
className={`${styles.brain_card_wrapper} ${
selected ? styles.selected : ""
}`}
>
<Image
className={isDarkMode ? styles.dark_image : ""}
src={imageUrl}
alt={brainName}
width={50}
height={50}
/>
<span className={styles.brain_title}>{brainName}</span>
<div className={styles.tag_wrapper}>
{tags[0] && <Tag color="primary" name={capitalCase(tags[0])} />}
</div>
</div>
</Tooltip>
</div>
);
};

View File

@ -0,0 +1,20 @@
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
.checkbox_wrapper {
display: flex;
align-items: center;
gap: Spacings.$spacing03;
cursor: pointer;
.checkbox {
width: 16px;
height: 16px;
border: 1px solid var(--border-2);
border-radius: Radius.$small;
&.filled {
background-color: var(--primary-0);
}
}
}

View File

@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import styles from "./Checkbox.module.scss";
interface CheckboxProps {
label: string;
checked: boolean;
setChecked: (value: boolean) => void;
}
export const Checkbox = ({
label,
checked,
setChecked,
}: CheckboxProps): JSX.Element => {
const [currentChecked, setCurrentChecked] = useState<boolean>(checked);
useEffect(() => {
setCurrentChecked(checked);
}, [checked]);
return (
<div
className={styles.checkbox_wrapper}
onClick={() => {
setChecked(!currentChecked);
setCurrentChecked(!currentChecked);
}}
>
<div
className={`${styles.checkbox} ${currentChecked ? styles.filled : ""}`}
></div>
<span>{label}</span>
</div>
);
};

View File

@ -0,0 +1,32 @@
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.header_wrapper {
display: flex;
justify-content: space-between;
align-items: center;
border: 1px solid var(--border-2);
border-radius: Radius.$big;
padding: Spacings.$spacing03;
cursor: pointer;
&:hover {
background-color: var(--background-2);
border-color: var(--accent);
}
.placeholder {
color: var(--text-2);
font-size: Typography.$small;
}
}
.filename {
font-size: Typography.$tiny;
}
.error_message {
font-size: Typography.$tiny;
color: var(--dangerous);
}

View File

@ -0,0 +1,69 @@
import { useRef, useState } from "react";
import { iconList } from "@/lib/helpers/iconList";
import styles from "./FileInput.module.scss";
import { FieldHeader } from "../FieldHeader/FieldHeader";
import { Icon } from "../Icon/Icon";
interface FileInputProps {
label: string;
icon: keyof typeof iconList;
onFileChange: (file: File) => void;
acceptedFileTypes?: string[];
}
export const FileInput = (props: FileInputProps): JSX.Element => {
const [currentFile, setcurrentFile] = useState<File | null>(null);
const [errorMessage, setErrorMessage] = useState<string>("");
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
const fileExtension = file.name.split(".").pop();
if (props.acceptedFileTypes?.includes(fileExtension || "")) {
props.onFileChange(file);
setcurrentFile(file);
setErrorMessage("");
} else {
setErrorMessage("Wrong extension");
}
}
};
const handleClick = () => {
fileInputRef.current?.click();
};
return (
<div>
<FieldHeader label={props.label} iconName={props.icon.toString()} />
<div className={styles.file_input_wrapper} onClick={handleClick}>
<div className={styles.header_wrapper}>
<span className={styles.placeholder}>
Click here to {currentFile ? "change your" : "upload a"} file
</span>
<Icon name="upload" size="normal" color="black" />
</div>
<input
ref={fileInputRef}
type="file"
className={styles.file_input}
onChange={handleFileChange}
accept={props.acceptedFileTypes
?.map((type) => `application/${type}`)
.join(",")}
style={{ display: "none" }}
/>
</div>
{currentFile && (
<span className={styles.filename}>{currentFile.name}</span>
)}
{errorMessage !== "" && (
<span className={styles.error_message}>{errorMessage}</span>
)}
</div>
);
};

View File

@ -71,12 +71,13 @@ import {
import { PiOfficeChairFill } from "react-icons/pi";
import { RiDeleteBackLine, RiHashtag } from "react-icons/ri";
import { SlOptions } from "react-icons/sl";
import { TbNetwork } from "react-icons/tb";
import { TbNetwork, TbRobot } from "react-icons/tb";
import { VscGraph } from "react-icons/vsc";
export const iconList: { [name: string]: IconType } = {
add: LuPlusCircle,
addWithoutCircle: IoIosAdd,
assistant: TbRobot,
back: RiDeleteBackLine,
brain: LuBrain,
brainCircuit: LuBrainCircuit,

View File

@ -0,0 +1,6 @@
import { StepValue } from "../components/AddBrainModal/types/types";
export type Step = {
label: string;
value: StepValue;
};