feat(frontend): first custom brain live (#2226)

# 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-20 15:11:03 -08:00 committed by GitHub
parent ec5679072f
commit b09878f332
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 485 additions and 228 deletions

View File

@ -205,7 +205,7 @@ def get_user_invitation(
detail="You have not been invited to this brain",
)
brain_details = brain_service.get_brain_details(brain_id)
brain_details = brain_service.get_brain_details(brain_id, current_user.id)
if brain_details is None:
raise HTTPException(

View File

@ -26,8 +26,9 @@ export const KnowledgeToFeed = ({
const brainsWithUploadRights = formatMinimalBrainsToSelectComponentInput(
useMemo(
() =>
allBrains.filter((brain) =>
requiredRolesForUpload.includes(brain.role)
allBrains.filter(
(brain) =>
requiredRolesForUpload.includes(brain.role) && !!brain.max_files
),
[allBrains]
)

View File

@ -14,6 +14,7 @@ export const QADisplay = ({ content }: QADisplayProps): JSX.Element => {
brain_name,
prompt_title,
metadata,
brain_id,
} = content;
return (
@ -31,6 +32,7 @@ export const QADisplay = ({ content }: QADisplayProps): JSX.Element => {
text={assistant}
brainName={brain_name}
promptName={prompt_title}
brainId={brain_id}
metadata={metadata} // eslint-disable-line @typescript-eslint/no-unsafe-assignment
/>
</>

View File

@ -19,11 +19,19 @@ type MessageRowProps = {
metadata?: {
sources?: Source[];
};
brainId?: string;
};
export const MessageRow = React.forwardRef(
(
{ speaker, text, brainName, promptName, children }: MessageRowProps,
{
speaker,
text,
brainName,
promptName,
children,
brainId,
}: MessageRowProps,
ref: React.Ref<HTMLDivElement>
) => {
const { handleCopy, isUserSpeaker } = useMessageRow({
@ -42,7 +50,7 @@ export const MessageRow = React.forwardRef(
>
{!isUserSpeaker ? (
<div className={styles.message_header}>
<QuestionBrain brainName={brainName} />
<QuestionBrain brainName={brainName} brainId={brainId} />
<QuestionPrompt promptName={promptName} />
</div>
) : (

View File

@ -1,22 +1,51 @@
import { Fragment } from "react";
import Image from "next/image";
import { Fragment, useEffect, useState } from "react";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import Icon from "@/lib/components/ui/Icon/Icon";
import styles from "./QuestionBrain.module.scss";
type QuestionBrainProps = {
brainName?: string | null;
brainId?: string;
};
export const QuestionBrain = ({
brainName,
brainId,
}: QuestionBrainProps): JSX.Element => {
const [brainLogoUrl, setBrainLogoUrl] = useState<string | undefined>(
undefined
);
const { getBrain } = useBrainApi();
const getBrainLogoUrl = async () => {
if (brainId) {
try {
const brain = await getBrain(brainId.toString());
setBrainLogoUrl(brain?.integration_description?.integration_logo_url);
} catch (error) {
console.error(error);
}
}
};
useEffect(() => {
void getBrainLogoUrl();
}, [brainId]);
if (brainName === undefined || brainName === null) {
return <Fragment />;
}
return (
<div data-testid="brain-tags" className={styles.brain_name_wrapper}>
{brainLogoUrl ? (
<Image src={brainLogoUrl} alt="brainLogo" width={18} height={18} />
) : (
<Icon name="brain" color="primary" size="normal" />
)}
<span>{brainName}</span>
</div>
);

View File

@ -49,6 +49,7 @@ const SelectedChatPage = (): JSX.Element => {
setShouldDisplayFeedCard(true);
},
iconName: "uploadFile",
hidden: !currentBrain?.max_files,
},
{
label: "Manage current brain",

View File

@ -25,7 +25,6 @@ export const BrainManagementTabs = (): JSX.Element => {
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
hasEditRights,
isPublicBrain,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
} = useBrainManagementTabs();
@ -57,14 +56,16 @@ export const BrainManagementTabs = (): JSX.Element => {
<StyledTabsTrigger value="settings">
{t("settings", { ns: "config" })}
</StyledTabsTrigger>
{(!isPublicBrain || hasEditRights) && (
{hasEditRights && (
<>
<StyledTabsTrigger value="people">
{t("people", { ns: "config" })}
</StyledTabsTrigger>
{brain?.brain_type === "doc" && (
<StyledTabsTrigger value="knowledgeOrSecrets">
{knowledgeOrSecretsTabLabel}
</StyledTabsTrigger>
)}
</>
)}
</TabsList>
@ -76,12 +77,14 @@ export const BrainManagementTabs = (): JSX.Element => {
<TabsContent value="people">
<PeopleTab brainId={brainId} hasEditRights={hasEditRights} />
</TabsContent>
{brain?.brain_type === "doc" && (
<TabsContent value="knowledgeOrSecrets">
<KnowledgeOrSecretsTab
brainId={brainId}
hasEditRights={hasEditRights}
/>
</TabsContent>
)}
</div>
<div className="flex justify-center">

View File

@ -15,6 +15,8 @@ import { usePermissionsController } from "./hooks/usePermissionsController";
import { UsePromptProps } from "./hooks/usePrompt";
import { useSettingsTab } from "./hooks/useSettingsTab";
import { useBrainFetcher } from "../../hooks/useBrainFetcher";
type SettingsTabProps = {
brainId: UUID;
};
@ -47,6 +49,10 @@ export const SettingsTabContent = ({
brainId,
});
const { brain } = useBrainFetcher({
brainId,
});
return (
<>
<form
@ -65,18 +71,23 @@ export const SettingsTabContent = ({
isSettingAsDefault={isSettingAsDefault}
setAsDefaultBrainHandler={setAsDefaultBrainHandler}
/>
{brain?.brain_type === "doc" && (
<>
<Divider
textClassName="font-semibold text-black w-full mx-1"
separatorClassName="w-full"
className="w-full my-10"
text={t("modelSection", { ns: "config" })}
/>
<ModelSelection
accessibleModels={accessibleModels}
hasEditRights={hasEditRights}
brainId={brainId}
handleSubmit={handleSubmit}
/>
</>
)}
<Divider text={t("customPromptSection", { ns: "config" })} />
<Prompt
usePromptProps={promptProps}

View File

@ -1,17 +1,12 @@
/* eslint max-lines:["error", 150] */
import { useTranslation } from "react-i18next";
import { LuStar } from "react-icons/lu";
import { ApiRequestDefinition } from "@/lib/components/ApiRequestDefinition";
import Button from "@/lib/components/ui/Button";
import { Chip } from "@/lib/components/ui/Chip";
import Field from "@/lib/components/ui/Field";
import { Radio } from "@/lib/components/ui/Radio";
import { TextArea } from "@/lib/components/ui/TextArea";
import { useGeneralInformation } from "./hooks/useGeneralInformation";
import { useBrainFormState } from "../../hooks/useBrainFormState";
type GeneralInformationProps = {
@ -27,18 +22,9 @@ export const GeneralInformation = (
props: GeneralInformationProps
): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
hasEditRights,
isPublicBrain,
isOwnedByCurrentUser,
isDefaultBrain,
isSettingAsDefault,
setAsDefaultBrainHandler,
} = props;
const { hasEditRights, isPublicBrain, isOwnedByCurrentUser } = props;
const { register } = useBrainFormState();
const { brainTypeOptions } = useGeneralInformation();
return (
<>
<div className="grid grid-cols-1 md:grid-cols-2 justify-between w-full items-end">
@ -61,21 +47,6 @@ export const GeneralInformation = (
{t("brain:public_brain_label")}
</Chip>
)}
<div>
{hasEditRights && (
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
className="bg-secondary text-primary border-none hover:bg-primary hover:text-white hover:disabled:text-primary disabled:bg-secondary disabled:text-primary disabled:cursor-not-allowed"
disabled={isSettingAsDefault || isDefaultBrain}
>
<LuStar size={18} />
{t("defaultBrain", { ns: "brain" })}
</Button>
)}
</div>
</div>
</div>
</div>
@ -89,15 +60,6 @@ export const GeneralInformation = (
{...register("description")}
/>
<div className="w-full mt-4">
<Radio
items={brainTypeOptions}
label={t("knowledge_source_label", { ns: "brain" })}
className="flex-1 justify-between w-[50%]"
disabled={true}
{...register("brain_type")}
/>
</div>
<ApiRequestDefinition />
</>
);

View File

@ -11,6 +11,7 @@ import {
import {
CreateBrainInput,
IntegrationBrains,
ListFilesProps,
SubscriptionUpdatableProperties,
UpdateBrainInput,
@ -155,3 +156,10 @@ export const getDocsFromQuestion = async (
)
).data.docs;
};
export const getIntegrationBrains = async (
axiosInstance: AxiosInstance
): Promise<IntegrationBrains[]> => {
return (await axiosInstance.get<IntegrationBrains[]>(`/brains/integrations`))
.data;
};

View File

@ -51,6 +51,11 @@ export type ApiBrainDefinition = {
jq_instructions: string;
};
export type IntegrationSettings = {
integration_id?: string;
settings?: { [x: string]: object | undefined };
};
export type CreateBrainInput = {
name: string;
description: string;
@ -63,6 +68,17 @@ export type CreateBrainInput = {
brain_definition?: Omit<ApiBrainDefinition, "brain_id">;
brain_secrets_values?: Record<string, string>;
connected_brains_ids?: UUID[];
integration?: IntegrationSettings;
};
export type IntegrationBrains = {
id: UUID;
integration_name: string;
integration_logo_url: string;
connections_settings: Record<string, unknown>;
integration_type: "custom" | "sync";
description: string;
max_files: number;
};
export type UpdateBrainInput = Partial<CreateBrainInput>;

View File

@ -9,6 +9,7 @@ import {
getBrainUsers,
getDefaultBrain,
getDocsFromQuestion,
getIntegrationBrains,
getPublicBrains,
setAsDefaultBrain,
Subscription,
@ -55,5 +56,6 @@ export const useBrainApi = () => {
brainId: string,
secrets: Record<string, string>
) => updateBrainSecrets(brainId, secrets, axiosInstance),
getIntegrationBrains: async () => getIntegrationBrains(axiosInstance),
};
};

View File

@ -12,4 +12,6 @@ export const mapBackendMinimalBrainToMinimalBrain = (
status: backendMinimalBrain.status,
brain_type: backendMinimalBrain.brain_type,
description: backendMinimalBrain.description,
integration_logo_url: backendMinimalBrain.integration_logo_url,
max_files: backendMinimalBrain.max_files,
});

View File

@ -1,3 +1,4 @@
import { useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@ -6,17 +7,20 @@ import { addBrainDefaultValues } from "@/lib/config/defaultBrainConfig";
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 { CreateBrainStep } from "./components/CreateBrainStep/CreateBrainStep";
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 {
isBrainCreationModalOpened,
setIsBrainCreationModalOpened,
setCurrentIntegrationBrain,
} = useBrainCreationContext();
const defaultValues: CreateBrainProps = {
...addBrainDefaultValues,
@ -28,6 +32,10 @@ export const AddBrainModal = (): JSX.Element => {
defaultValues,
});
useEffect(() => {
setCurrentIntegrationBrain(undefined);
}, [isBrainCreationModalOpened]);
return (
<FormProvider {...methods}>
<Modal
@ -45,7 +53,7 @@ export const AddBrainModal = (): JSX.Element => {
<div className={styles.content_wrapper}>
<BrainTypeSelectionStep />
<BrainMainInfosStep />
<BrainKnowledgeStep />
<CreateBrainStep />
</div>
</div>
</Modal>

View File

@ -1,10 +1,16 @@
import { createContext, useContext, useState } from "react";
import { IntegrationBrains } from "@/lib/api/brain/types";
interface BrainCreationContextProps {
isBrainCreationModalOpened: boolean;
setIsBrainCreationModalOpened: React.Dispatch<React.SetStateAction<boolean>>;
creating: boolean;
setCreating: React.Dispatch<React.SetStateAction<boolean>>;
currentIntegrationBrain: IntegrationBrains | undefined;
setCurrentIntegrationBrain: React.Dispatch<
React.SetStateAction<IntegrationBrains | undefined>
>;
}
export const BrainCreationContext = createContext<
@ -18,6 +24,8 @@ export const BrainCreationProvider = ({
}): JSX.Element => {
const [isBrainCreationModalOpened, setIsBrainCreationModalOpened] =
useState<boolean>(false);
const [currentIntegrationBrain, setCurrentIntegrationBrain] =
useState<IntegrationBrains>();
const [creating, setCreating] = useState<boolean>(false);
return (
@ -27,6 +35,8 @@ export const BrainCreationProvider = ({
setIsBrainCreationModalOpened,
creating,
setCreating,
currentIntegrationBrain,
setCurrentIntegrationBrain,
}}
>
{children}

View File

@ -1,35 +0,0 @@
import { Checkbox } from "@/lib/components/ui/Checkbox";
import { MinimalBrainForUser } from "@/lib/context/BrainProvider/types";
import { useConnectableBrain } from "./hooks/useConnectableBrain";
type ConnectableBrainProps = {
brain: MinimalBrainForUser;
};
export const ConnectableBrain = ({
brain,
}: ConnectableBrainProps): JSX.Element => {
const { onCheckedChange } = useConnectableBrain();
return (
<div className="flex flex-row items-center gap-2">
<Checkbox
className="text-white"
onCheckedChange={(checked) =>
onCheckedChange({
brainId: brain.id,
checked,
})
}
id={`connected_brains_ids-${brain.id}`}
/>
<label
htmlFor={`connected_brains_ids-${brain.id}`}
className="text-md font-medium leading-none cursor-pointer"
>
{brain.name}
</label>
</div>
);
};

View File

@ -1,35 +0,0 @@
import { CheckedState } from "@radix-ui/react-checkbox";
import { UUID } from "crypto";
import { useFormContext } from "react-hook-form";
import { CreateBrainProps } from "@/lib/components/AddBrainModal/types/types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useConnectableBrain = () => {
const { setValue, getValues } = useFormContext<CreateBrainProps>();
const onCheckedChange = ({
checked,
brainId,
}: {
checked: CheckedState;
brainId: UUID;
}) => {
if (checked === "indeterminate") {
return;
}
const connected_brains_ids = getValues("connected_brains_ids") ?? [];
if (checked) {
setValue("connected_brains_ids", [...connected_brains_ids, brainId]);
} else {
setValue(
"connected_brains_ids",
connected_brains_ids.filter((id) => id !== brainId)
);
}
};
return {
onCheckedChange,
};
};

View File

@ -1,24 +0,0 @@
import { useTranslation } from "react-i18next";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { ConnectableBrain } from "./Components/ConnectableBrain/ConnectableBrain";
export const CompositeBrainConnections = (): JSX.Element => {
const { allBrains } = useBrainContext();
const sortedBrains = allBrains.sort((a, b) => a.name.localeCompare(b.name));
const { t } = useTranslation("brain");
return (
<div className="px-10">
<p className="text-center mb-8 italic text-sm w-full">
{t("composite_brain_composition_invitation")}
</p>
<div className="w-full flex flex-col gap-2">
{sortedBrains.map((brain) => (
<ConnectableBrain key={brain.id} brain={brain} />
))}
</div>
</div>
);
};

View File

@ -10,7 +10,7 @@
padding: Spacings.$spacing05;
border-radius: Radius.$big;
cursor: pointer;
border: 2px solid transparent;
border: 1px solid Colors.$lightest-black;
&.disabled {
pointer-events: none;
@ -35,6 +35,7 @@
&:hover,
&.selected {
border-color: Colors.$primary;
background-color: Colors.$primary-lightest;
.name {
color: Colors.$primary;

View File

@ -10,6 +10,8 @@
padding-inline: Spacings.$spacing08;
height: 100%;
gap: Spacings.$spacing05;
overflow-y: hidden;
overflow-x: visible;
@media (max-width: ScreenSizes.$small) {
padding-inline: 0;
@ -19,13 +21,22 @@
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
padding-top: Spacings.$spacing03;
overflow-y: scroll;
overflow-x: visible;
.title {
@include Typography.H2;
}
}
.button {
.buttons_wrapper {
align-self: flex-end;
&.two_buttons {
display: flex;
justify-content: space-between;
align-self: normal;
}
}
}

View File

@ -1,16 +1,37 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import { IntegrationBrains } from "@/lib/api/brain/types";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
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 { CustomBrainList } from "./CustomBrainList/CustomBrainList";
import { useBrainCreationContext } from "../../brainCreation-provider";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const BrainTypeSelectionStep = (): JSX.Element => {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [customBrainsCatalogueOpened, setCustomBrainsCatalogueOpened] =
useState<boolean>(false);
const [customBrains, setCustomBrains] = useState<IntegrationBrains[]>([]);
const { goToNextStep, currentStepIndex } = useBrainCreationSteps();
const { getIntegrationBrains } = useBrainApi();
const { currentIntegrationBrain } = useBrainCreationContext();
useEffect(() => {
getIntegrationBrains()
.then((response) => {
setCustomBrains(
response.filter((brain) => brain.integration_type === "custom")
);
})
.catch((error) => {
console.error(error);
});
}, []);
const brainTypes: BrainType[] = [
{
@ -18,6 +39,15 @@ export const BrainTypeSelectionStep = (): JSX.Element => {
description: "Upload documents or website links to feed your brain.",
iconName: "feed",
},
{
name: "Custom Brain",
description:
"Explore your databases, converse with your APIs, and much more!",
iconName: "custom",
onClick: () => {
setCustomBrainsCatalogueOpened(true);
},
},
{
name: "Sync Brain - Coming soon!",
description:
@ -25,13 +55,6 @@ export const BrainTypeSelectionStep = (): JSX.Element => {
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 => {
@ -45,23 +68,53 @@ export const BrainTypeSelectionStep = (): JSX.Element => {
return (
<div className={styles.brain_types_wrapper}>
<div className={styles.main_wrapper}>
{customBrainsCatalogueOpened ? (
<CustomBrainList customBrainList={customBrains} />
) : (
<>
<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)}
onClick={() => {
setSelectedIndex(index);
if (brainType.onClick) {
brainType.onClick();
}
}}
/>
</div>
))}
</>
)}
</div>
<div className={styles.button}>
<div
className={`${styles.buttons_wrapper} ${
customBrainsCatalogueOpened ? styles.two_buttons : ""
}`}
>
{customBrainsCatalogueOpened && (
<QuivrButton
label="Type of brain"
iconName="chevronLeft"
color="primary"
onClick={() => {
setCustomBrainsCatalogueOpened(false);
setSelectedIndex(-1);
}}
/>
)}
<QuivrButton
label="Next Step"
iconName="chevronRight"
color="primary"
onClick={() => next()}
disabled={
selectedIndex === -1 ||
(!!customBrainsCatalogueOpened && !currentIntegrationBrain)
}
/>
</div>
</div>

View File

@ -0,0 +1,44 @@
@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";
.cards_wrapper {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: Spacings.$spacing05;
.title {
@include Typography.H2;
}
.brain_card_wrapper {
display: flex;
flex-direction: column;
align-items: center;
border-radius: Radius.$normal;
gap: Spacings.$spacing03;
padding: Spacings.$spacing04;
border: 1px solid Colors.$lightest-black;
width: fit-content;
cursor: pointer;
width: 100px;
.brain_title {
font-size: Typography.$small;
}
&:hover,
&.selected {
border-color: Colors.$primary;
background-color: Colors.$primary-lightest;
.brain_title {
color: Colors.$primary;
}
}
}
}

View File

@ -0,0 +1,53 @@
import Image from "next/image";
import { IntegrationBrains } from "@/lib/api/brain/types";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
import styles from "./CustomBrainList.module.scss";
import { useBrainCreationContext } from "../../../brainCreation-provider";
export const CustomBrainList = ({
customBrainList,
}: {
customBrainList: IntegrationBrains[];
}): JSX.Element => {
const { setCurrentIntegrationBrain, currentIntegrationBrain } =
useBrainCreationContext();
return (
<div className={styles.cards_wrapper}>
<MessageInfoBox content="More custom brains are coming!" type="info" />
<span className={styles.title}>Choose a custom brain</span>
<div>
{customBrainList.map((brain) => {
return (
<div
key={brain.id}
onClick={() => setCurrentIntegrationBrain(brain)}
>
<Tooltip tooltip={brain.description}>
<div
className={`${styles.brain_card_wrapper} ${
currentIntegrationBrain === brain ? styles.selected : ""
}`}
>
<Image
src={brain.integration_logo_url}
alt={brain.integration_name}
width={50}
height={50}
/>
<span className={styles.brain_title}>
{brain.integration_name}
</span>
</div>
</Tooltip>
</div>
);
})}
</div>
</div>
);
};

View File

@ -1,16 +1,18 @@
import { KnowledgeToFeed } from "@/app/chat/[chatId]/components/ActionsBar/components";
import { MessageInfoBox } from "@/lib/components/ui/MessageInfoBox/MessageInfoBox";
import QuivrButton from "@/lib/components/ui/QuivrButton/QuivrButton";
import styles from "./BrainKnowledgeStep.module.scss";
import styles from "./CreateBrainStep.module.scss";
import { useBrainCreationApi } from "./hooks/useBrainCreationApi";
import { useBrainCreationContext } from "../../brainCreation-provider";
import { useBrainCreationSteps } from "../../hooks/useBrainCreationSteps";
export const BrainKnowledgeStep = (): JSX.Element => {
export const CreateBrainStep = (): JSX.Element => {
const { currentStepIndex, goToPreviousStep } = useBrainCreationSteps();
const { createBrain } = useBrainCreationApi();
const { creating, setCreating } = useBrainCreationContext();
const { creating, setCreating, currentIntegrationBrain } =
useBrainCreationContext();
const previous = (): void => {
goToPreviousStep();
@ -27,10 +29,17 @@ export const BrainKnowledgeStep = (): JSX.Element => {
return (
<div className={styles.brain_knowledge_wrapper}>
{!currentIntegrationBrain ? (
<div>
<span className={styles.title}>Feed your brain</span>
<KnowledgeToFeed hideBrainSelector={true} />
</div>
) : (
<MessageInfoBox
content="Your custom brain is ready to be created"
type="success"
/>
)}
<div className={styles.buttons_wrapper}>
<QuivrButton
label="Previous step"

View File

@ -4,6 +4,7 @@ import { useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
import { IntegrationSettings } from "@/lib/api/brain/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";
@ -23,8 +24,11 @@ export const useBrainCreationApi = () => {
const { setKnowledgeToFeed } = useKnowledgeToFeedContext();
const { createBrain: createBrainApi, setCurrentBrainId } = useBrainContext();
const { crawlWebsiteHandler, uploadFileHandler } = useKnowledgeToFeedInput();
const { setIsBrainCreationModalOpened, setCreating } =
useBrainCreationContext();
const {
setIsBrainCreationModalOpened,
setCreating,
currentIntegrationBrain,
} = useBrainCreationContext();
const handleFeedBrain = async (brainId: UUID): Promise<void> => {
const uploadPromises = files.map((file) =>
@ -37,26 +41,21 @@ export const useBrainCreationApi = () => {
};
const createBrain = async (): Promise<void> => {
const {
name,
description,
brain_definition,
brain_secrets_values,
status,
brain_type,
connected_brains_ids,
} = getValues();
const { name, description } = getValues();
let integrationSettings: IntegrationSettings | undefined = undefined;
if (currentIntegrationBrain) {
integrationSettings = {
integration_id: currentIntegrationBrain.id,
settings: {},
};
}
const createdBrainId = await createBrainApi({
brain_type: currentIntegrationBrain ? "integration" : "doc",
name,
description,
status,
brain_type,
brain_definition: brain_type === "api" ? brain_definition : undefined,
brain_secrets_values:
brain_type === "api" ? brain_secrets_values : undefined,
connected_brains_ids:
brain_type === "composite" ? connected_brains_ids : undefined,
integration: integrationSettings,
});
if (createdBrainId === undefined) {
@ -67,9 +66,8 @@ export const useBrainCreationApi = () => {
return;
}
if (brain_type === "doc") {
void handleFeedBrain(createdBrainId);
}
setCurrentBrainId(createdBrainId);
setIsBrainCreationModalOpened(false);

View File

@ -1,3 +1,5 @@
import Image from "next/image";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import styles from "./CurrentBrain.module.scss";
@ -27,7 +29,16 @@ export const CurrentBrain = ({
<div className={styles.left}>
<span>Talking to</span>
<div className={styles.brain_name_wrapper}>
{currentBrain.integration_logo_url ? (
<Image
src={currentBrain.integration_logo_url}
alt="brain"
width={18}
height={18}
/>
) : (
<Icon size="small" name="brain" color="primary" />
)}
<span className={styles.brain_name}>{currentBrain.name}</span>
</div>
</div>

View File

@ -1,4 +1,4 @@
import Tooltip from "@/lib/components/ui/Tooltip";
import Tooltip from "@/lib/components/ui/Tooltip/Tooltip";
import { enhanceUrlDisplay } from "./utils/enhanceUrlDisplay";
import { removeFileExtension } from "./utils/removeFileExtension";

View File

@ -33,6 +33,7 @@ export const PageHeader = ({
onClick={button.onClick}
color={button.color}
iconName={button.iconName}
hidden={button.hidden}
/>
))}
</div>

View File

@ -4,7 +4,7 @@ import { HTMLAttributes } from "react";
import { cn } from "@/lib/utils";
import Tooltip from "./Tooltip";
import Tooltip from "./Tooltip/Tooltip";
interface EllipsisProps extends HTMLAttributes<HTMLDivElement> {
children: string;

View File

@ -69,6 +69,10 @@
}
}
.success {
color: Colors.$success;
}
.disabled {
color: Colors.$black;
pointer-events: none;

View File

@ -0,0 +1,18 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
.message_info_box_wrapper {
padding: Spacings.$spacing03;
display: flex;
align-items: center;
gap: Spacings.$spacing03;
width: 100%;
border: 1px solid Colors.$normal-grey;
color: Colors.$black;
border-radius: Radius.$normal;
&.success {
border-color: Colors.$success;
}
}

View File

@ -0,0 +1,41 @@
import { iconList } from "@/lib/helpers/iconList";
import { Color } from "@/lib/types/Colors";
import styles from "./MessageInfoBox.module.scss";
import { Icon } from "../Icon/Icon";
export type MessageInfoBoxProps = {
content: string;
type: "info" | "success" | "warning" | "error";
};
export const MessageInfoBox = ({
content,
type,
}: MessageInfoBoxProps): JSX.Element => {
const getIconProps = (): {
iconName: keyof typeof iconList;
iconColor: Color;
} => {
switch (type) {
case "info":
return { iconName: "info", iconColor: "grey" };
case "success":
return { iconName: "check", iconColor: "success" };
default:
return { iconName: "info", iconColor: "grey" };
}
};
return (
<div className={`${styles.message_info_box_wrapper} ${styles[type]} `}>
<Icon
name={getIconProps().iconName}
size="normal"
color={getIconProps().iconColor}
/>
<span>{content}</span>
</div>
);
};

View File

@ -15,6 +15,10 @@
display: flex;
width: fit-content;
&.hidden {
display: none;
}
&.primary {
border-color: Colors.$primary;
color: Colors.$primary;

View File

@ -14,6 +14,7 @@ export const QuivrButton = ({
isLoading,
iconName,
disabled,
hidden,
}: ButtonType): JSX.Element => {
const [hovered, setHovered] = useState<boolean>(false);
@ -23,6 +24,7 @@ export const QuivrButton = ({
${styles.button_wrapper}
${styles[color]}
${disabled ? styles.disabled : ""}
${hidden ? styles.hidden : ""}
`}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClick={() => onClick()}

View File

@ -0,0 +1,13 @@
@use "@/styles/Colors.module.scss";
@use "@/styles/Radius.module.scss";
@use "@/styles/Spacings.module.scss";
@use "@/styles/Typography.module.scss";
@use "@/styles/ZIndexes.module.scss";
.tooltip_content_wrapper {
z-index: ZIndexes.$tooltip;
background-color: Colors.$lightest-black;
padding: Spacings.$spacing03;
border-radius: Radius.$normal;
font-size: Typography.$small;
}

View File

@ -3,6 +3,8 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { AnimatePresence, motion } from "framer-motion";
import { ReactNode, useState } from "react";
import styles from "./Tooltip.module.scss";
interface TooltipProps {
children?: ReactNode;
tooltip?: ReactNode;
@ -31,11 +33,9 @@ const Tooltip = ({ children, tooltip }: TooltipProps): JSX.Element => {
opacity: 0,
transition: { ease: "easeIn", duration: 0.1 },
}}
// transition={{ duration: 0.2, ease: "circInOut" }}
className="select-none rounded-md border border-black/10 dark:border-white/25 bg-white dark:bg-gray-800 px-5 py-3 text-sm leading-none shadow-lg dark:shadow-primary/25"
className={styles.tooltip_content_wrapper}
>
{tooltip}
<TooltipPrimitive.Arrow className="fill-white dark:fill-black" />
</motion.div>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>

View File

@ -26,6 +26,7 @@ export const addBrainDefaultValues: CreateBrainInput = {
jq_instructions: "",
},
connected_brains_ids: [],
integration: undefined,
};
export const defaultModel: Model = "gpt-3.5-turbo";

View File

@ -10,6 +10,16 @@ import { BrainType, Model } from "../../types/BrainConfig";
export type BrainAccessStatus = "private" | "public";
export type IntegrationDescription = {
connection_settings?: object;
description: string;
id: UUID;
integration_logo_url: string;
integration_name: string;
integration_type: "custom" | "sync";
max_files: number;
};
export type Brain = {
id: UUID;
name: string;
@ -22,6 +32,7 @@ export type Brain = {
prompt_id?: string | null;
brain_type?: BrainType;
brain_definition?: ApiBrainDefinition;
integration_description?: IntegrationDescription;
};
export type MinimalBrainForUser = {
@ -31,6 +42,8 @@ export type MinimalBrainForUser = {
status: BrainAccessStatus;
brain_type: BrainType;
description: string;
integration_logo_url?: string;
max_files: number;
};
//TODO: rename rights to role in Backend and use MinimalBrainForUser instead of BackendMinimalBrainForUser
@ -44,7 +57,7 @@ export type PublicBrain = {
description?: string;
number_of_subscribers: number;
last_update: string;
brain_type: BrainType;
brain_type?: BrainType;
brain_definition?: ApiBrainDefinition;
};

View File

@ -10,8 +10,14 @@ import {
FaRegUserCircle,
FaUnlock,
} from "react-icons/fa";
import { FaInfo } from "react-icons/fa6";
import { FiUpload } from "react-icons/fi";
import { IoIosAdd, IoMdClose, IoMdLogOut } from "react-icons/io";
import {
IoIosAdd,
IoIosHelpCircleOutline,
IoMdClose,
IoMdLogOut,
} from "react-icons/io";
import {
IoArrowUpCircleOutline,
IoHomeOutline,
@ -66,8 +72,10 @@ export const iconList: { [name: string]: IconType } = {
followUp: IoArrowUpCircleOutline,
graph: VscGraph,
hastag: RiHashtag,
help: IoIosHelpCircleOutline,
history: MdHistory,
home: IoHomeOutline,
info: FaInfo,
key: FaKey,
loader: AiOutlineLoading3Quarters,
logout: IoMdLogOut,

View File

@ -4,12 +4,12 @@ import { ApiBrainDefinition } from "../api/brain/types";
export const brainStatuses = ["private", "public"] as const;
export type BrainStatus = (typeof brainStatuses)[number];
export const brainTypes = ["doc", "api", "composite"] as const;
export const brainTypes = ["doc", "api", "composite", "integration"] as const;
export type BrainType = (typeof brainTypes)[number];
export type BrainStatus = (typeof brainStatuses)[number];
export type Model = (typeof freeModels)[number];
// TODO: update this type to match the backend (antropic, openai and some other keys should be removed)

View File

@ -6,4 +6,5 @@ export type Color =
| "gold"
| "accent"
| "white"
| "dangerous";
| "dangerous"
| "success";

View File

@ -9,4 +9,5 @@ export interface ButtonType {
iconName: keyof typeof iconList;
onClick: () => void | Promise<void>;
disabled?: boolean;
hidden?: boolean;
}

View File

@ -1,3 +1,4 @@
$base: 1000;
$overlay: 1010;
$modal: 1020;
$tooltip: 1030;