feat(frontend): new brain creation modal (#2192)

# 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-14 16:37:33 -08:00 committed by GitHub
parent 08e015af6c
commit 6383918f7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 729 additions and 683 deletions

View File

@ -5,7 +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 { BrainCreationProvider } from "@/lib/components/AddBrainModal/brainCreation-provider";
import { Menu } from "@/lib/components/Menu/Menu";
import { useOutsideClickListener } from "@/lib/components/Menu/hooks/useOutsideClickListener";
import SearchModal from "@/lib/components/SearchModal/SearchModal";

View File

@ -2,7 +2,7 @@ import { SuggestionKeyDownProps } from "@tiptap/suggestion";
import { forwardRef } from "react";
import { FaAngleDoubleDown } from "react-icons/fa";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCreation-provider";
import TextButton from "@/lib/components/ui/TextButton/TextButton";
import { AddNewPromptButton } from "./components/AddNewPromptButton";

View File

@ -9,6 +9,7 @@
padding-block: Spacings.$spacing05;
width: 100%;
gap: Spacings.$spacing05;
overflow: hidden;
.single_selector_wrapper {
width: 30%;
@ -37,6 +38,7 @@
overflow: scroll;
flex-direction: column;
gap: Spacings.$spacing02;
flex-grow: 1;
overflow: scroll;
.uploaded_knowledge {
@ -46,6 +48,7 @@
justify-content: space-between;
width: 100%;
overflow: hidden;
font-size: Typography.$small;
.left {
display: flex;

View File

@ -13,7 +13,11 @@ import { FromDocuments } from "./components/FromDocuments/FromDocuments";
import { FromWebsites } from "./components/FromWebsites/FromWebsites";
import { formatMinimalBrainsToSelectComponentInput } from "./utils/formatMinimalBrainsToSelectComponentInput";
export const KnowledgeToFeed = (): JSX.Element => {
export const KnowledgeToFeed = ({
hideBrainSelector,
}: {
hideBrainSelector?: boolean;
}): JSX.Element => {
const { allBrains, setCurrentBrainId, currentBrain } = useBrainContext();
const [selectedTab, setSelectedTab] = useState("From documents");
const { knowledgeToFeed, removeKnowledgeToFeed } =
@ -46,6 +50,7 @@ export const KnowledgeToFeed = (): JSX.Element => {
return (
<div className={styles.knowledge_to_feed_wrapper}>
{!hideBrainSelector && (
<div className={styles.single_selector_wrapper}>
<SingleSelector
options={brainsWithUploadRights}
@ -58,6 +63,7 @@ export const KnowledgeToFeed = (): JSX.Element => {
placeholder="Select a brain"
/>
</div>
)}
<Tabs tabList={knowledgesTabs} />
<div className={styles.tabs_content_wrapper}>
{selectedTab === "From documents" && <FromDocuments />}

View File

@ -1,5 +1,6 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
.from_document_wrapper {
@ -24,6 +25,10 @@
display: flex;
gap: Spacings.$spacing02;
@media (max-width: ScreenSizes.$small) {
flex-direction: column;
}
.clickable {
cursor: pointer;
font-weight: bold;

View File

@ -4,7 +4,7 @@ import { UUID } from "crypto";
import { useEffect } from "react";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { useChatContext } from "@/lib/context";

View File

@ -4,7 +4,7 @@ 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 { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { SearchBar } from "@/lib/components/ui/SearchBar/SearchBar";

View File

@ -3,7 +3,7 @@
import { useState } from "react";
import { AddBrainModal } from "@/lib/components/AddBrainModal";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { useBrainCreationContext } from "@/lib/components/AddBrainModal/brainCreation-provider";
import PageHeader from "@/lib/components/PageHeader/PageHeader";
import { UploadDocumentModal } from "@/lib/components/UploadDocumentModal/UploadDocumentModal";
import { Tabs } from "@/lib/components/ui/Tabs/Tabs";

View File

@ -0,0 +1,21 @@
@use "@/styles/Spacings.module.scss";
.add_brain_modal_container {
display: flex;
padding-block: Spacings.$spacing05;
flex-direction: column;
width: 100%;
height: 100%;
overflow: hidden;
gap: Spacings.$spacing08;
.stepper_container {
width: 100%;
padding-inline: Spacings.$spacing08;
}
.content_wrapper {
flex-grow: 1;
overflow: scroll;
}
}

View File

@ -1,11 +1,23 @@
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { Modal } from "@/lib/components/ui/Modal/Modal";
import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
import { AddBrainSteps } from "./components/AddBrainSteps/AddBrainSteps";
import { CreateBrainProps } from "./types";
import styles from "./AddBrainModal.module.scss";
import { useBrainCreationContext } from "./brainCreation-provider";
import { BrainKnowledgeStep } from "./components/BrainKnowledgeStep/BrainKnowledgeStep";
import { BrainMainInfosStep } from "./components/BrainMainInfosStep/BrainMainInfosStep";
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
import { Stepper } from "./components/Stepper/Stepper";
import { CreateBrainProps } from "./types/types";
export const AddBrainModal = (): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const { isBrainCreationModalOpened, setIsBrainCreationModalOpened } =
useBrainCreationContext();
const defaultValues: CreateBrainProps = {
...addBrainDefaultValues,
setDefault: true,
@ -18,7 +30,25 @@ export const AddBrainModal = (): JSX.Element => {
return (
<FormProvider {...methods}>
<AddBrainSteps />
<Modal
title={t("newBrainTitle", { ns: "brain" })}
desc={t("newBrainSubtitle", { ns: "brain" })}
isOpen={isBrainCreationModalOpened}
setOpen={setIsBrainCreationModalOpened}
bigModal={true}
CloseTrigger={<div />}
>
<div className={styles.add_brain_modal_container}>
<div className={styles.stepper_container}>
<Stepper />
</div>
<div className={styles.content_wrapper}>
<BrainTypeSelectionStep />
<BrainMainInfosStep />
<BrainKnowledgeStep />
</div>
</div>
</Modal>
</FormProvider>
);
};

View File

@ -3,6 +3,8 @@ import { createContext, useContext, useState } from "react";
interface BrainCreationContextProps {
isBrainCreationModalOpened: boolean;
setIsBrainCreationModalOpened: React.Dispatch<React.SetStateAction<boolean>>;
creating: boolean;
setCreating: React.Dispatch<React.SetStateAction<boolean>>;
}
export const BrainCreationContext = createContext<
@ -15,11 +17,17 @@ export const BrainCreationProvider = ({
children: React.ReactNode;
}): JSX.Element => {
const [isBrainCreationModalOpened, setIsBrainCreationModalOpened] =
useState(false);
useState<boolean>(false);
const [creating, setCreating] = useState<boolean>(false);
return (
<BrainCreationContext.Provider
value={{ isBrainCreationModalOpened, setIsBrainCreationModalOpened }}
value={{
isBrainCreationModalOpened,
setIsBrainCreationModalOpened,
creating,
setCreating,
}}
>
{children}
</BrainCreationContext.Provider>

View File

@ -1,44 +0,0 @@
import { useTranslation } from "react-i18next";
import { Modal } from "@/lib/components/ui/Modal/Modal";
import { useBrainCreationContext } from "./brainCreation-provider";
import { BrainKnowledgeStep } from "./components/BrainKnowledgeStep/BrainKnowledgeStep";
import { BrainParamsStep } from "./components/BrainParamsStep/BrainParamsStep";
import { BrainTypeSelectionStep } from "./components/BrainTypeSelectionStep/BrainTypeSelectionStep";
import { Stepper } from "./components/Stepper/Stepper";
export const AddBrainSteps = (): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const { isBrainCreationModalOpened, setIsBrainCreationModalOpened } =
useBrainCreationContext();
return (
<Modal
title={t("newBrainTitle", { ns: "brain" })}
desc={t("newBrainSubtitle", { ns: "brain" })}
isOpen={isBrainCreationModalOpened}
setOpen={setIsBrainCreationModalOpened}
CloseTrigger={<div />}
>
<form
onSubmit={(e) => {
e.preventDefault();
}}
className="my-10 flex flex-col items-center gap-2 justify-center"
>
<Stepper />
<BrainTypeSelectionStep
onCancelBrainCreation={() => setIsBrainCreationModalOpened(false)}
/>
<BrainParamsStep
onCancelBrainCreation={() => setIsBrainCreationModalOpened(false)}
/>
<BrainKnowledgeStep
onCancelBrainCreation={() => setIsBrainCreationModalOpened(false)}
/>
</form>
</Modal>
);
};

View File

@ -1,80 +0,0 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { FaArrowLeft } from "react-icons/fa";
import { ApiRequestDefinition } from "@/lib/components/ApiRequestDefinition";
import Button from "@/lib/components/ui/Button";
import { BrainType } from "@/lib/types/BrainConfig";
import { CompositeBrainConnections } from "./components/CompositeBrainConnections/CompositeBrainConnections";
import { KnowledgeToFeedInput } from "./components/KnowledgeToFeedInput";
import { useBrainCreationHandler } from "./hooks/useBrainCreationHandler";
import { useBrainKnowledgeStep } from "./hooks/useBrainKnowledgeStep";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
type BrainKnowledgeStepProps = {
onCancelBrainCreation: () => void;
};
export const BrainKnowledgeStep = ({
onCancelBrainCreation,
}: BrainKnowledgeStepProps): JSX.Element => {
const { brainType, isSubmitButtonDisabled } = useBrainKnowledgeStep();
const { t } = useTranslation(["translation"]);
const { goToPreviousStep, currentStep } = useBrainCreationSteps();
const { handleCreateBrain, isBrainCreationPending } = useBrainCreationHandler(
{
closeBrainCreationModal: onCancelBrainCreation,
}
);
const brainTypeToKnowledgeComponent: Record<BrainType, JSX.Element> = {
doc: <KnowledgeToFeedInput />,
api: <ApiRequestDefinition />,
composite: <CompositeBrainConnections />,
};
if (currentStep !== "KNOWLEDGE" || brainType === undefined) {
return <Fragment />;
}
return (
<>
{brainTypeToKnowledgeComponent[brainType]}
<div className="flex flex-row justify-between items-center flex-1 mt-10 w-full">
<Button
type="button"
variant="tertiary"
onClick={onCancelBrainCreation}
className="text-primary"
disabled={isBrainCreationPending}
>
{t("cancel", { ns: "translation" })}
</Button>
<div className="flex gap-4">
<Button
type="button"
variant="secondary"
onClick={goToPreviousStep}
className="py-2 border-primary text-primary"
disabled={isBrainCreationPending}
>
<FaArrowLeft className="text-xl" size={16} />
{t("previous", { ns: "translation" })}
</Button>
<Button
className="bg-primary text-white py-2 border-none"
type="button"
onClick={() => void handleCreateBrain()}
disabled={isSubmitButtonDisabled || isBrainCreationPending}
isLoading={isBrainCreationPending}
>
{t("createButton", { ns: "translation" })}
</Button>
</div>
</div>
</>
);
};

View File

@ -1,26 +0,0 @@
import { useTranslation } from "react-i18next";
import {
Crawler,
FeedItems,
FileUploader,
} from "@/lib/components/KnowledgeToFeedInput/components";
export const KnowledgeToFeedInput = (): JSX.Element => {
const { t } = useTranslation(["translation", "upload"]);
return (
<div>
<div className="flex flex-row gap-10 justify-between items-center mt-5">
<FileUploader />
<span className="whitespace-nowrap ">
{`${t("and", { ns: "translation" })} / ${t("or", {
ns: "translation",
})}`}
</span>
<Crawler />
</div>
<FeedItems />
</div>
);
};

View File

@ -1,52 +0,0 @@
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { useToast } from "@/lib/hooks";
import { useBrainCreationApi } from "./useBrainCreationApi";
type UseBrainCreationHandler = {
closeBrainCreationModal: () => void;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainCreationHandler = ({
closeBrainCreationModal,
}: UseBrainCreationHandler) => {
const { getValues } = useFormContext<CreateBrainProps>();
const { publish } = useToast();
const { t } = useTranslation(["brain", "config"]);
const { isBrainCreationPending, createBrain } = useBrainCreationApi({
closeBrainCreationModal,
});
const handleCreateBrain = () => {
const { name, description } = getValues();
if (name.trim() === "" || isBrainCreationPending) {
publish({
variant: "danger",
text: t("nameRequired", { ns: "config" }),
});
return;
}
if (description.trim() === "") {
publish({
variant: "danger",
text: t("descriptionRequired", { ns: "config" }),
});
return;
}
createBrain();
};
return {
handleCreateBrain,
isBrainCreationPending,
};
};

View File

@ -1,24 +0,0 @@
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainKnowledgeStep = () => {
const { watch } = useFormContext<CreateBrainProps>();
const brainType = watch("brain_type");
const url = watch("brain_definition.url");
const compositeBrainConnections = watch("connected_brains_ids") ?? [];
const isApiBrain = brainType === "api";
const isCompositeBrain = brainType === "composite";
const isApiBrainDefinitionsFilled = url !== "";
const isSubmitButtonDisabled =
(isCompositeBrain && compositeBrainConnections.length === 0) ||
(isApiBrain && !isApiBrainDefinitionsFilled);
return {
brainType,
isSubmitButtonDisabled,
};
};

View File

@ -1,96 +0,0 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { FaArrowLeft, FaArrowRight } from "react-icons/fa";
import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field";
import { TextArea } from "@/lib/components/ui/TextArea";
import { PublicAccessConfirmationModal } from "./components/PublicAccessConfirmationModal";
import { useBrainParamsStep } from "./hooks/useBrainParamsStep";
import { usePublicAccessConfirmationModal } from "./hooks/usePublicAccessConfirmationModal";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
import { useBrainTypeSelectionStep } from "../BrainTypeSelectionStep/hooks/useBrainTypeSelectionStep";
type BrainParamsStepProps = {
onCancelBrainCreation: () => void;
};
export const BrainParamsStep = ({
onCancelBrainCreation,
}: BrainParamsStepProps): JSX.Element => {
const { goToNextStep, goToPreviousStep, currentStep } =
useBrainCreationSteps();
const { register } = useBrainTypeSelectionStep();
const { t } = useTranslation(["translation", "brain", "config"]);
const { isNextButtonDisabled } = useBrainParamsStep();
const {
isPublicAccessConfirmationModalOpened,
onCancelPublicAccess,
onConfirmPublicAccess,
} = usePublicAccessConfirmationModal();
if (currentStep !== "BRAIN_PARAMS") {
return <Fragment />;
}
return (
<>
<Field
label={t("brainName", { ns: "brain" })}
autoFocus
placeholder={t("brainNamePlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1"
required
{...register("name")}
/>
<TextArea
label={t("brainDescription", { ns: "brain" })}
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1 m-3"
required
{...register("description")}
/>
<div className="flex flex-row justify-between items-center flex-1 mt-10 w-full">
<Button
type="button"
variant="tertiary"
onClick={onCancelBrainCreation}
className="text-primary"
>
{t("cancel", { ns: "translation" })}
</Button>
<div className="flex gap-4">
<Button
type="button"
variant="secondary"
onClick={goToPreviousStep}
className="py-2 border-primary text-primary"
>
<FaArrowLeft className="text-xl" size={16} />
{t("previous", { ns: "translation" })}
</Button>
<Button
className="bg-primary text-white py-2 border-none"
type="button"
onClick={goToNextStep}
disabled={isNextButtonDisabled}
>
{t("next", { ns: "translation" })}
<FaArrowRight className="text-xl" size={16} />
</Button>
</div>
</div>
<PublicAccessConfirmationModal
opened={isPublicAccessConfirmationModalOpened}
onClose={onCancelPublicAccess}
onCancel={onCancelPublicAccess}
onConfirm={onConfirmPublicAccess}
/>
</>
);
};

View File

@ -1,42 +0,0 @@
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal/Modal";
type PublicAccessConfirmationModalProps = {
opened: boolean;
onClose: () => void;
onConfirm: () => void;
onCancel: () => void;
};
export const PublicAccessConfirmationModal = ({
opened,
onClose,
onConfirm,
onCancel,
}: PublicAccessConfirmationModalProps): JSX.Element => {
const { t } = useTranslation(["brain"]);
return (
<Modal isOpen={opened} setOpen={onClose} CloseTrigger={<div />}>
<div
dangerouslySetInnerHTML={{
__html: t("set_brain_status_to_public_modal_title"),
}}
/>
<div
dangerouslySetInnerHTML={{
__html: t("set_brain_status_to_public_modal_description"),
}}
/>
<div className="flex flex-row justify-between pt-10 px-10 items-center">
<Button type="button" onClick={onConfirm} variant="secondary">
{t("confirm_set_brain_status_to_public")}
</Button>
<Button type="button" onClick={onCancel}>
{t("cancel_set_brain_status_to_public")}
</Button>
</div>
</Modal>
);
};

View File

@ -1,16 +0,0 @@
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainParamsStep = () => {
const { watch } = useFormContext<CreateBrainProps>();
const brainName = watch("name");
const description = watch("description");
const isNextButtonDisabled = brainName === "" || description === "";
return {
isNextButtonDisabled,
};
};

View File

@ -1,25 +0,0 @@
import { useTranslation } from "react-i18next";
import { BrainStatus } from "@/lib/types/BrainConfig";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainStatusOptions = () => {
const { t } = useTranslation(["translation", "brain", "config"]);
const brainStatusOptions: {
label: string;
value: BrainStatus;
}[] = [
{
label: t("private_brain_label", { ns: "brain" }),
value: "private",
},
{
label: t("public_brain_label", { ns: "brain" }),
value: "public",
},
];
return {
brainStatusOptions,
};
};

View File

@ -1,43 +0,0 @@
import { useEffect, useState } from "react";
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const usePublicAccessConfirmationModal = () => {
const {
watch,
setValue,
formState: { dirtyFields },
} = useFormContext<CreateBrainProps>();
const [
isPublicAccessConfirmationModalOpened,
setIsPublicAccessConfirmationModalOpened,
] = useState(false);
const status = watch("status");
useEffect(() => {
if (status === "public" && dirtyFields.status === true) {
setIsPublicAccessConfirmationModalOpened(true);
}
}, [dirtyFields.status, status]);
const onConfirmPublicAccess = () => {
setIsPublicAccessConfirmationModalOpened(false);
};
const onCancelPublicAccess = () => {
setValue("status", "private", {
shouldDirty: true,
});
setIsPublicAccessConfirmationModalOpened(false);
};
return {
isPublicAccessConfirmationModalOpened,
onConfirmPublicAccess,
onCancelPublicAccess,
};
};

View File

@ -1,56 +0,0 @@
import { Fragment } from "react";
import { useTranslation } from "react-i18next";
import { FaArrowRight } from "react-icons/fa";
import Button from "@/lib/components/ui/Button";
import { Radio } from "@/lib/components/ui/Radio";
import { useBrainTypeSelectionStep } from "./hooks/useBrainTypeSelectionStep";
import { useKnowledgeSourceLabel } from "./hooks/useKnowledgeSourceLabel";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
type BrainTypeSelectionStepProps = {
onCancelBrainCreation: () => void;
};
export const BrainTypeSelectionStep = ({
onCancelBrainCreation,
}: BrainTypeSelectionStepProps): JSX.Element => {
const { knowledgeSourceOptions } = useKnowledgeSourceLabel();
const { register } = useBrainTypeSelectionStep();
const { goToNextStep, currentStep } = useBrainCreationSteps();
const { t } = useTranslation(["translation"]);
if (currentStep !== "BRAIN_TYPE") {
return <Fragment />;
}
return (
<>
<Radio
items={knowledgeSourceOptions}
className="flex-1 justify-between"
{...register("brain_type")}
/>
<div className="flex flex-row flex-1 justify-center w-full gap-48 mt-10">
<Button
type="button"
variant="tertiary"
onClick={onCancelBrainCreation}
>
{t("cancel")}
</Button>
<Button
className="bg-primary text-white py-2 border-none"
type="button"
data-testid="create-brain-submit-button"
onClick={goToNextStep}
>
{t("next")}
<FaArrowRight className="text-xl" size={16} />
</Button>
</div>
</>
);
};

View File

@ -1,21 +0,0 @@
import { useEffect } from "react";
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainTypeSelectionStep = () => {
const { register, watch, reset, setValue } =
useFormContext<CreateBrainProps>();
const brainType = watch("brain_type");
useEffect(() => {
const currentBrainType = brainType;
reset();
setValue("brain_type", currentBrainType);
}, [brainType, reset, setValue]);
return {
register,
};
};

View File

@ -1,35 +0,0 @@
import { useFeatureIsOn } from "@growthbook/growthbook-react";
import { useTranslation } from "react-i18next";
import { BrainType } from "@/lib/types/BrainConfig";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useKnowledgeSourceLabel = () => {
const { t } = useTranslation(["translation", "brain", "config"]);
const isCompositeBrainActivated = useFeatureIsOn("agent-brain");
const knowledgeSourceOptions: {
label: string;
value: BrainType;
}[] = [
{
label: t("knowledge_source_doc", { ns: "brain" }),
value: "doc",
},
{
label: t("knowledge_source_api", { ns: "brain" }),
value: "api",
},
];
if (isCompositeBrainActivated) {
knowledgeSourceOptions.push({
label: t("knowledge_source_composite_brain", { ns: "brain" }),
value: "composite",
});
}
return {
knowledgeSourceOptions,
};
};

View File

@ -1,31 +0,0 @@
import { Fragment } from "react";
import { cn } from "@/lib/utils";
import { Step } from "./components/Step";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const Stepper = (): JSX.Element => {
const { currentStep, steps } = useBrainCreationSteps();
return (
<div className="flex flex-row justify-between w-full px-12 mb-12">
{steps.map((step, index) => (
<Fragment key={step.value}>
<Step index={index} step={step} />
{index < steps.length - 1 && ( // Add horizontal line for all but the last step
<hr
className={cn(
"flex-grow border-t-2 border-primary m-4",
step.value === currentStep
? "border-primary"
: "border-gray-300"
)}
/>
)}
</Fragment>
))}
</div>
);
};

View File

@ -1,39 +0,0 @@
import { FaCheckCircle } from "react-icons/fa";
import { cn } from "@/lib/utils";
import { useBrainCreationSteps } from "../../../hooks/useBrainCreationSteps";
import { Step as StepType } from "../../../types";
type StepProps = {
index: number;
step: StepType;
};
export const Step = ({ index, step }: StepProps): JSX.Element => {
const { currentStep, currentStepIndex } = useBrainCreationSteps();
const isStepDone = index < currentStepIndex;
const stepContent = isStepDone ? <FaCheckCircle /> : index + 1;
return (
<div
key={step.label}
className="flex flex-row justify-center items-center flex-1"
>
<div className="flex flex-col justify-center items-center">
<div
className={cn(
"h-[40px] w-[40px] border-solid rounded-full flex flex-row items-center justify-center mb-2 border-primary border-2 text-primary",
isStepDone ? "bg-primary text-white" : "",
step.value === currentStep ? "bg-primary text-white" : ""
)}
>
{stepContent}
</div>
<span key={step.label} className="text-xs text-center">
{step.label}
</span>
</div>
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./AddBrainSteps";

View File

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

View File

@ -0,0 +1,19 @@
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_knowledge_wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
padding-inline: Spacings.$spacing08;
height: 100%;
.title {
@include Typography.H2;
}
.buttons_wrapper {
display: flex;
justify-content: space-between;
}
}

View File

@ -0,0 +1,51 @@
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import styles from "./BrainKnowledgeStep.module.scss";
import { useBrainCreationApi } from "./hooks/useBrainCreationApi";
import { useBrainCreationContext } from "../../brainCreation-provider";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const BrainKnowledgeStep = (): JSX.Element => {
const { currentStepIndex, goToPreviousStep } = useBrainCreationSteps();
const { createBrain } = useBrainCreationApi();
const { creating, setCreating } = useBrainCreationContext();
const previous = (): void => {
goToPreviousStep();
};
const feed = (): void => {
setCreating(true);
createBrain();
};
if (currentStepIndex !== 2) {
return <></>;
}
return (
<div className={styles.brain_knowledge_wrapper}>
<div>
<span className={styles.title}>Feed your brain</span>
<KnowledgeToFeed hideBrainSelector={true} />
</div>
<div className={styles.buttons_wrapper}>
<QuivrButton
label="Previous step"
color="primary"
iconName="chevronLeft"
onClick={previous}
/>
<QuivrButton
label="Create brain"
color="primary"
iconName="add"
onClick={feed}
isLoading={creating}
/>
</div>
</div>
);
};

View File

@ -2,7 +2,7 @@ import { CheckedState } from "@radix-ui/react-checkbox";
import { UUID } from "crypto";
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useConnectableBrain = () => {

View File

@ -4,22 +4,17 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
import { useKnowledgeToFeedInput } from "@/lib/components/KnowledgeToFeedInput/hooks/useKnowledgeToFeedInput.ts";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useKnowledgeToFeedContext } from "@/lib/context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { useToast } from "@/lib/hooks";
import { useKnowledgeToFeedFilesAndUrls } from "@/lib/hooks/useKnowledgeToFeed";
type UseBrainCreationHandler = {
closeBrainCreationModal: () => void;
};
import { useBrainCreationContext } from "../../../brainCreation-provider";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainCreationApi = ({
closeBrainCreationModal,
}: UseBrainCreationHandler) => {
export const useBrainCreationApi = () => {
const queryClient = useQueryClient();
const { publish } = useToast();
const { t } = useTranslation(["brain", "config"]);
@ -27,8 +22,9 @@ export const useBrainCreationApi = ({
const { getValues, reset } = useFormContext<CreateBrainProps>();
const { setKnowledgeToFeed } = useKnowledgeToFeedContext();
const { createBrain: createBrainApi, setCurrentBrainId } = useBrainContext();
const { setAsDefaultBrain } = useBrainApi();
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
const { setIsBrainCreationModalOpened, setCreating } =
useBrainCreationContext();
const handleFeedBrain = async (brainId: UUID): Promise<void> => {
const uploadPromises = files.map((file) =>
@ -44,7 +40,6 @@ export const useBrainCreationApi = ({
const {
name,
description,
setDefault,
brain_definition,
brain_secrets_values,
status,
@ -76,12 +71,11 @@ export const useBrainCreationApi = ({
void handleFeedBrain(createdBrainId);
}
if (setDefault) {
await setAsDefaultBrain(createdBrainId);
}
setCurrentBrainId(createdBrainId);
closeBrainCreationModal();
setIsBrainCreationModalOpened(false);
setCreating(false);
reset();
void queryClient.invalidateQueries({
queryKey: [PUBLIC_BRAINS_KEY],
});

View File

@ -0,0 +1,25 @@
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_main_infos_wrapper {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
padding-inline: Spacings.$spacing08;
.title {
@include Typography.H2;
}
.inputs_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
}
.buttons_wrapper {
display: flex;
justify-content: space-between;
}
}

View File

@ -0,0 +1,76 @@
import { Controller, useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { TextAreaInput } from "@/lib/components/ui/TextAreaInput/TextAreaInput";
import { TextInput } from "@/lib/components/ui/TextInput/TextInput";
import styles from "./BrainMainInfosStep.module.scss";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const BrainMainInfosStep = (): JSX.Element => {
const { currentStepIndex, goToNextStep, goToPreviousStep } =
useBrainCreationSteps();
const { watch } = useFormContext<CreateBrainProps>();
const name = watch("name");
const description = watch("description");
const isDisabled = !name || !description;
const next = (): void => {
goToNextStep();
};
const previous = (): void => {
goToPreviousStep();
};
if (currentStepIndex !== 1) {
return <></>;
}
return (
<div className={styles.brain_main_infos_wrapper}>
<div className={styles.inputs_wrapper}>
<span className={styles.title}>Define brain identity</span>
<Controller
name="name"
render={({ field }) => (
<TextInput
label="Name"
inputValue={field.value as string} // Explicitly specify the type as string
setInputValue={field.onChange}
/>
)}
/>
<Controller
name="description"
render={({ field }) => (
<TextAreaInput
label="Description"
inputValue={field.value as string}
setInputValue={field.onChange}
/>
)}
/>
</div>
<div className={styles.buttons_wrapper}>
<QuivrButton
color="primary"
label="Previous Step"
onClick={() => previous()}
iconName="chevronLeft"
/>
<QuivrButton
color="primary"
label="Next Step"
onClick={() => next()}
iconName="chevronRight"
disabled={isDisabled}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,47 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_type_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
padding: Spacings.$spacing05;
border-radius: Radius.$big;
cursor: pointer;
border: 2px solid transparent;
&.disabled {
pointer-events: none;
background-color: Colors.$lightest-grey;
opacity: 0.6;
}
.first_line_wrapper {
display: flex;
gap: Spacings.$spacing03;
align-items: center;
.name {
@include Typography.H3;
}
}
.description {
color: Colors.$dark-grey;
}
&:hover,
&.selected {
border-color: Colors.$primary;
.name {
color: Colors.$primary;
}
.description {
color: Colors.$black;
}
}
}

View File

@ -0,0 +1,41 @@
import { useState } from "react";
import { BrainType } from "@/lib/components/AddBrainModal/types/types";
import Icon from "@/lib/components/ui/Icon/Icon";
import styles from "./BrainTypeSelection.module.scss";
export const BrainTypeSelection = ({
brainType,
onClick,
selected,
}: {
brainType: BrainType;
onClick: () => void;
selected: boolean;
}): JSX.Element => {
const [isHovered, setIsHovered] = useState<boolean>(false);
return (
<div
className={`
${styles.brain_type_wrapper}
${brainType.disabled && styles.disabled}
${selected && styles.selected}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={onClick}
>
<div className={styles.first_line_wrapper}>
<Icon
name={brainType.iconName}
size="normal"
color={isHovered || selected ? "primary" : "black"}
/>
<span className={styles.name}>{brainType.name}</span>
</div>
<span className={styles.description}>{brainType.description}</span>
</div>
);
};

View File

@ -0,0 +1,31 @@
@use "@/styles/ScreenSizes.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.brain_types_wrapper {
display: flex;
justify-content: space-between;
flex-direction: column;
width: 100%;
padding-inline: Spacings.$spacing08;
height: 100%;
gap: Spacings.$spacing05;
@media (max-width: ScreenSizes.$small) {
padding-inline: 0;
}
.main_wrapper {
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
.title {
@include Typography.H2;
}
}
.button {
align-self: flex-end;
}
}

View File

@ -0,0 +1,69 @@
import { useState } from "react";
import { BrainType } from "@/lib/components/AddBrainModal/types/types";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import { BrainTypeSelection } from "./BrainTypeSelection/BrainTypeSelection";
import styles from "./BrainTypeSelectionStep.module.scss";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const BrainTypeSelectionStep = (): JSX.Element => {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const { goToNextStep, currentStepIndex } = useBrainCreationSteps();
const brainTypes: BrainType[] = [
{
name: "Core Brain",
description: "Upload documents or website links to feed your brain.",
iconName: "feed",
},
{
name: "Sync Brain - Coming soon!",
description:
"Connect to your tools and applications to interact with your data.",
iconName: "software",
disabled: true,
},
{
name: "Custom Brain - Coming soon!",
description:
"Explore your databases, converse with your APIs, and much more!",
iconName: "custom",
disabled: true,
},
];
const next = (): void => {
goToNextStep();
};
if (currentStepIndex !== 0) {
return <></>;
}
return (
<div className={styles.brain_types_wrapper}>
<div className={styles.main_wrapper}>
<span className={styles.title}>Choose a type of brain</span>
{brainTypes.map((brainType, index) => (
<div key={index}>
<BrainTypeSelection
brainType={brainType}
selected={index === selectedIndex}
onClick={() => setSelectedIndex(index)}
/>
</div>
))}
</div>
<div className={styles.button}>
<QuivrButton
label="Next Step"
iconName="chevronRight"
color="primary"
onClick={() => next()}
/>
</div>
</div>
);
};

View File

@ -0,0 +1,105 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
.stepper_wrapper {
display: flex;
width: 100%;
justify-content: space-between;
.step {
display: flex;
flex-direction: column;
border-radius: Radius.$circle;
position: relative;
.circle {
width: 2.5rem;
height: 2.5rem;
background-color: Colors.$primary;
border-radius: Radius.$circle;
display: flex;
justify-content: center;
align-items: center;
.inside_circle {
width: 100%;
height: 100%;
border-radius: Radius.$circle;
display: flex;
justify-content: center;
align-items: center;
}
}
&.done_step {
.circle {
background-color: Colors.$success;
}
.step_info {
.step_status {
color: Colors.$success;
}
}
}
&.current_step {
.circle {
background-color: Colors.$white;
border: 1px solid Colors.$primary;
}
.inside_circle {
background-color: Colors.$primary;
width: 70%;
height: 70%;
}
.step_info {
.step_status {
color: Colors.$primary;
}
}
}
&.pending_step {
.circle {
background-color: Colors.$primary-light;
}
.step_info {
.step_status {
color: Colors.$normal-grey;
}
}
}
.step_info {
margin-top: Spacings.$spacing03;
display: flex;
flex-direction: column;
font-size: Typography.$tiny;
width: 2.5rem;
.step_index {
white-space: nowrap;
color: Colors.$normal-grey;
}
}
}
.bar {
flex-grow: 1;
height: 4px;
border-radius: Radius.$big;
background-color: Colors.$primary-light;
margin: 0 8px;
margin-top: Spacings.$spacing05;
&.done {
background-color: Colors.$success;
}
}
}

View File

@ -0,0 +1,58 @@
import { Icon } from "@/lib/components/ui/Icon/Icon";
import styles from "./Stepper.module.scss";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const Stepper = (): JSX.Element => {
const { currentStep, steps } = useBrainCreationSteps();
const currentStepIndex = steps.findIndex(
(step) => step.value === currentStep
);
return (
<div className={styles.stepper_wrapper}>
{steps.map((step, index) => (
<>
<div
className={`${styles.step} ${
index === currentStepIndex
? styles.current_step
: index < currentStepIndex
? styles.done_step
: styles.pending_step
}`}
key={step.value}
>
<div className={styles.circle}>
<div className={styles.inside_circle}>
{index < currentStepIndex && (
<Icon name="check" size="normal" color="white" />
)}
</div>
</div>
<div className={styles.step_info}>
<span className={styles.step_index}>STEP {index + 1}</span>
<span className={styles.step_status}>
{index === currentStepIndex
? "Progress"
: index < currentStepIndex
? "Completed"
: "Pending"}
</span>
</div>
</div>
{index < steps.length - 1 && (
<div
className={`
${styles.bar}
${index < currentStepIndex ? styles.done : ""}
`}
></div>
)}
</>
))}
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./AddBrainSteps";

View File

@ -1,9 +1,10 @@
import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { Step } from "../types";
import {
CreateBrainProps,
Step,
} from "@/lib/components/AddBrainModal/types/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainCreationSteps = () => {

View File

@ -1,4 +1,5 @@
import { CreateBrainInput } from "@/lib/api/brain/types";
import { iconList } from "@/lib/helpers/iconList";
const brainCreationSteps = ["BRAIN_TYPE", "BRAIN_PARAMS", "KNOWLEDGE"] as const;
@ -8,3 +9,16 @@ export type CreateBrainProps = CreateBrainInput & {
setDefault: boolean;
brainCreationStep: BrainCreationStep;
};
export interface BrainType {
name: string;
description: string;
iconName: keyof typeof iconList;
disabled?: boolean;
onClick?: () => void;
}
export type Step = {
label: string;
value: BrainCreationStep;
};

View File

@ -1,6 +1,6 @@
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
import { defaultParamDefinitionRow } from "../config";
import { mapApiBrainDefinitionSchemaToParameterDefinition } from "../utils/mapApiBrainDefinitionSchemaToParameterDefinition";

View File

@ -1,6 +1,6 @@
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
import {
brainSecretsValueKeyInForm,

View File

@ -26,7 +26,7 @@
overflow: scroll;
&.big_modal {
width: 50vw;
width: 40vw;
height: 90vh;
}

View File

@ -0,0 +1,35 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
.text_area_input_container {
display: flex;
border: 1px solid Colors.$lighter-grey;
gap: Spacings.$spacing03;
padding-block: Spacings.$spacing02;
padding-inline: Spacings.$spacing03;
border-radius: Radius.$big;
align-items: center;
width: 100%;
&.simple {
border: none;
padding: 0;
.text_input {
background-color: transparent;
width: 10px;
}
}
.text_area_input {
caret-color: Colors.$accent;
border: none;
flex: 1;
resize: none;
&:focus {
box-shadow: none;
}
}
}

View File

@ -0,0 +1,32 @@
import styles from "./TextAreaInput.module.scss";
type TextAreaInputProps = {
label: string;
inputValue: string;
setInputValue: (value: string) => void;
onSubmit?: () => void;
};
export const TextAreaInput = ({
label,
inputValue,
setInputValue,
onSubmit,
}: TextAreaInputProps): JSX.Element => {
return (
<div className={styles.text_area_input_container}>
<textarea
className={styles.text_area_input}
value={inputValue}
rows={5}
onChange={(e) => setInputValue(e.target.value)}
placeholder={label}
onKeyDown={(e) => {
if (e.key === "Enter" && onSubmit) {
onSubmit();
}
}}
/>
</div>
);
};

View File

@ -1,5 +1,6 @@
import { AiOutlineLoading3Quarters } from "react-icons/ai";
import { BsArrowRightShort, BsChatLeftText } from "react-icons/bs";
import { CgSoftwareDownload } from "react-icons/cg";
import { CiFlag1 } from "react-icons/ci";
import {
FaCheck,
@ -21,6 +22,7 @@ import {
LuBrain,
LuBrainCircuit,
LuChevronDown,
LuChevronLeft,
LuChevronRight,
LuCopy,
LuFile,
@ -29,7 +31,9 @@ import {
} from "react-icons/lu";
import {
MdAlternateEmail,
MdDashboardCustomize,
MdDeleteOutline,
MdDynamicFeed,
MdHistory,
MdOutlineModeEditOutline,
MdUploadFile,
@ -47,12 +51,15 @@ export const iconList: { [name: string]: IconType } = {
check: FaCheck,
checkCircle: FaCheckCircle,
chevronDown: LuChevronDown,
chevronLeft: LuChevronLeft,
chevronRight: LuChevronRight,
close: IoMdClose,
copy: LuCopy,
custom: MdDashboardCustomize,
delete: MdDeleteOutline,
edit: MdOutlineModeEditOutline,
email: MdAlternateEmail,
feed: MdDynamicFeed,
file: LuFile,
flag: CiFlag1,
followUp: IoArrowUpCircleOutline,
@ -66,6 +73,7 @@ export const iconList: { [name: string]: IconType } = {
redirection: BsArrowRightShort,
search: LuSearch,
settings: IoSettingsSharp,
software: CgSoftwareDownload,
star: FaRegStar,
unlock: FaUnlock,
upload: FiUpload,

View File

@ -8,7 +8,7 @@ import { useOnboarding } from "./useOnboarding";
import { useOnboardingTracker } from "./useOnboardingTracker";
import { useToast } from "./useToast";
import { useBrainCreationContext } from "../components/AddBrainModal/components/AddBrainSteps/brainCreation-provider";
import { useBrainCreationContext } from "../components/AddBrainModal/brainCreation-provider";
import { useKnowledgeToFeedContext } from "../context/KnowledgeToFeedProvider/hooks/useKnowledgeToFeedContext";
import { acceptedFormats } from "../helpers/acceptedFormats";
import { cloneFileWithSanitizedName } from "../helpers/cloneFileWithSanitizedName";

View File

@ -30,3 +30,6 @@ $gold: #b8860b;
// ERROR
$dangerous-dark: #e30c17;
$dangerous: #9b373c;
// SUCCESS
$success: #47a455;

View File

@ -1,3 +1,4 @@
$circle: 50%;
$big: 12px;
$normal: 5px;
$small: 2px;

View File

@ -24,6 +24,7 @@
text-overflow: ellipsis;
}
$tiny: 12px;
$small: 14px;
$medium: 16px;
$large: 18px;