refactor(settings tab): extract components (#1335)

# Description

Extract the components from the settingsTab to increase readability of
file and allow refactoring of huge useBrainSettings hook

## 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:
Zineb El Bachiri 2023-10-27 12:52:39 +02:00 committed by GitHub
parent 1fe10d904d
commit f7ae27b9cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 417 additions and 162 deletions

View File

@ -4,9 +4,13 @@ import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
import {
BrainTabTrigger,
KnowledgeTab,
PeopleTab,
SettingsTab,
} from "./components";
import { DeleteOrUnsubscribeConfirmationModal } from "./components/Modals/DeleteOrUnsubscribeConfirmationModal";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
export const BrainManagementTabs = (): JSX.Element => {

View File

@ -0,0 +1,94 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
BrainContextMock,
BrainProviderMock,
} from "@/lib/context/BrainProvider/mocks/BrainProviderMock";
import {
SupabaseContextMock,
SupabaseProviderMock,
} from "@/lib/context/SupabaseProvider/mocks/SupabaseProviderMock";
import { SettingsTab } from "./SettingsTab";
const useTranslationMock = vi.fn(() => ({
t: (str: string): string => str,
}));
vi.mock("react-i18next", () => ({
useTranslation: () => useTranslationMock(),
}));
vi.mock("@/lib/context/SupabaseProvider/supabase-provider", () => ({
SupabaseContext: SupabaseContextMock,
}));
vi.mock("@/lib/context/BrainProvider/brain-provider", () => ({
BrainContext: BrainContextMock,
}));
vi.mock("@/lib/api/brain/useBrainApi", () => ({
useBrainApi: () => ({
setAsDefaultBrain: () => [],
updateBrain: () => [],
}),
}));
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>(
"@tanstack/react-query"
);
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: vi.fn() }),
}));
return {
...actual,
useQuery: () => ({
data: {},
}),
};
});
vi.mock("@/lib/hooks", async () => {
const actual = await vi.importActual<typeof import("@/lib/hooks")>(
"@/lib/hooks"
);
return {
...actual,
useAxios: () => ({
...actual.useAxios(),
axiosInstance: {
get: vi.fn(() => ({
data: [],
})),
},
}),
};
});
describe("Settings tab in brains-management", () => {
it("should render the seettings tab correctly", () => {
const brainId = "4adefe4e-eb08-4208-b237-";
render(
<SupabaseProviderMock>
<BrainProviderMock>
<SettingsTab brainId={brainId} />
</BrainProviderMock>
</SupabaseProviderMock>
);
expect(
screen.getByRole("button", { name: "setDefaultBrain" })
).toBeVisible();
expect(screen.getByText("brainName")).toBeVisible();
expect(screen.getByLabelText("brainDescription")).toBeVisible();
expect(screen.getByLabelText("promptName")).toBeVisible();
});
});
2;

View File

@ -1,22 +1,15 @@
/* eslint-disable max-lines */
/* eslint max-lines:["error", 125] */
import { UUID } from "crypto";
import { useTranslation } from "react-i18next";
import { FaSpinner } from "react-icons/fa";
import Button from "@/lib/components/ui/Button";
import { Chip } from "@/lib/components/ui/Chip";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";
import { Radio } from "@/lib/components/ui/Radio";
import { TextArea } from "@/lib/components/ui/TextArea";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { SaveButton } from "@/shared/SaveButton";
import { GeneralInformation, ModelSelection, Prompt } from "./components";
import { AccessConfirmationModal } from "./components/PrivateAccessConfirmationModal/AccessConfirmationModal";
import { useAccessConfirmationModal } from "./components/PrivateAccessConfirmationModal/hooks/useAccessConfirmationModal";
import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
import { useSettingsTab } from "./hooks/useSettingsTab";
import { getBrainPermissions } from "../../utils/getBrainPermissions";
@ -38,15 +31,15 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
isUpdating,
isDefaultBrain,
formRef,
promptId,
pickPublicPrompt,
removeBrainPrompt,
accessibleModels,
brainStatusOptions,
status,
setValue,
dirtyFields,
resetField,
pickPublicPrompt,
promptId,
removeBrainPrompt,
} = useSettingsTab({ brainId });
const { onCancel, isAccessModalOpened, closeModal } =
@ -75,158 +68,38 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
className="my-10 mb-0 flex flex-col items-center gap-2"
ref={formRef}
>
<div className="flex flex-row flex-1 justify-between w-full items-end">
<div>
<Field
label={t("brainName", { ns: "brain" })}
placeholder={t("brainNamePlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1"
required
disabled={!hasEditRights}
{...register("name")}
/>
</div>
<div className="mt-4">
<div className="flex flex-1 items-center flex-col">
{isPublicBrain && !isOwnedByCurrentUser && (
<Chip className="mb-3 bg-primary text-white w-full">
{t("brain:public_brain_label")}
</Chip>
)}
{isDefaultBrain ? (
<div className="border rounded-lg border-dashed border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white dark dark focus:text-white dark:focus:text-black transition-colors py-2 px-4 shadow-none">
{t("defaultBrain", { ns: "brain" })}
</div>
) : (
hasEditRights && (
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
)
)}
</div>
</div>
</div>
{isOwnedByCurrentUser && (
<div className="w-full mt-4">
<Radio
items={brainStatusOptions}
label={t("brain_status_label", { ns: "brain" })}
value={status}
className="flex-1 justify-between w-[50%]"
{...register("status")}
/>
</div>
)}
<TextArea
label={t("brainDescription", { ns: "brain" })}
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1 m-3"
disabled={!hasEditRights}
{...register("description")}
<GeneralInformation
brainStatusOptions={brainStatusOptions}
hasEditRights={hasEditRights}
isDefaultBrain={isDefaultBrain}
isOwnedByCurrentUser={isOwnedByCurrentUser}
isPublicBrain={isPublicBrain}
isSettingAsDefault={isSettingAsDefault}
register={register}
setAsDefaultBrainHandler={setAsDefaultBrainHandler}
/>
<Divider text={t("modelSection", { ns: "config" })} />
<Field
label={t("openAiKeyLabel", { ns: "config" })}
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("openAiKey")}
<ModelSelection
accessibleModels={accessibleModels}
model={model}
maxTokens={maxTokens}
temperature={temperature}
register={register}
hasEditRights={hasEditRights}
brainId={brainId}
handleSubmit={handleSubmit}
/>
<fieldset className="w-full flex flex-col mt-2">
<label className="flex-1 text-sm" htmlFor="model">
{t("modelLabel", { ns: "config" })}
</label>
<select
id="model"
disabled={!hasEditRights}
{...register("model")}
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
onChange={() => {
void handleSubmit(false); // Trigger form submission
}}
>
{accessibleModels.map((availableModel) => (
<option value={availableModel} key={availableModel}>
{availableModel}
</option>
))}
</select>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="temp">
{t("temperature", { ns: "config" })}: {temperature}
</label>
<input
id="temp"
type="range"
min="0"
max="1"
step="0.01"
value={temperature}
disabled={!hasEditRights}
{...register("temperature")}
/>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="tokens">
{t("maxTokens", { ns: "config" })}: {maxTokens}
</label>
<input
type="range"
min="10"
max={defineMaxTokens(model)}
value={maxTokens}
disabled={!hasEditRights}
{...register("maxTokens")}
/>
</fieldset>
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
<Divider text={t("customPromptSection", { ns: "config" })} />
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
<Field
label={t("promptName", { ns: "config" })}
placeholder={t("promptNamePlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("prompt.title")}
<Prompt
brainId={brainId}
handleSubmit={handleSubmit}
isUpdating={isUpdating}
pickPublicPrompt={pickPublicPrompt}
removeBrainPrompt={removeBrainPrompt}
promptId={promptId}
register={register}
hasEditRights={hasEditRights}
/>
<TextArea
label={t("promptContent", { ns: "config" })}
placeholder={t("promptContentPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("prompt.content")}
/>
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
{hasEditRights && promptId !== "" && (
<Button
disabled={isUpdating}
onClick={() => void removeBrainPrompt()}
>
{t("removePrompt", { ns: "config" })}
</Button>
)}
<div className="flex flex-row justify-end flex-1 w-full mt-8">
{isUpdating && <FaSpinner className="animate-spin" />}
{isUpdating && (

View File

@ -0,0 +1,104 @@
/* eslint max-lines:["error", 110] */
import { UseFormRegister } from "react-hook-form";
import { useTranslation } from "react-i18next";
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 { BrainConfig } from "@/lib/types/brainConfig";
type GeneralInformationProps = {
register: UseFormRegister<BrainConfig>;
hasEditRights: boolean;
isPublicBrain: boolean;
isOwnedByCurrentUser: boolean;
isDefaultBrain: boolean;
isSettingAsDefault: boolean;
setAsDefaultBrainHandler: () => Promise<void>;
brainStatusOptions: {
label: string;
value: "private" | "public";
}[];
};
export const GeneralInformation = (
props: GeneralInformationProps
): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
register,
hasEditRights,
isPublicBrain,
isOwnedByCurrentUser,
isDefaultBrain,
isSettingAsDefault,
setAsDefaultBrainHandler,
brainStatusOptions,
} = props;
return (
<>
<div className="flex flex-row flex-1 justify-between w-full items-end">
<div>
<Field
label={t("brainName", { ns: "brain" })}
placeholder={t("brainNamePlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1"
required
disabled={!hasEditRights}
{...register("name")}
/>
</div>
<div className="mt-4">
<div className="flex flex-1 items-center flex-col">
{isPublicBrain && !isOwnedByCurrentUser && (
<Chip className="mb-3 bg-primary text-white w-full">
{t("brain:public_brain_label")}
</Chip>
)}
{isDefaultBrain ? (
<div className="border rounded-lg border-dashed border-black dark:border-white bg-white dark:bg-black text-black dark:text-white focus:bg-black dark:focus:bg-white dark dark focus:text-white dark:focus:text-black transition-colors py-2 px-4 shadow-none">
{t("defaultBrain", { ns: "brain" })}
</div>
) : (
hasEditRights && (
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
type="button"
>
{t("setDefaultBrain", { ns: "brain" })}
</Button>
)
)}
</div>
</div>
</div>
{isOwnedByCurrentUser && (
<div className="w-full mt-4">
<Radio
items={brainStatusOptions}
label={t("brain_status_label", { ns: "brain" })}
value={status}
className="flex-1 justify-between w-[50%]"
{...register("status")}
/>
</div>
)}
<TextArea
label={t("brainDescription", { ns: "brain" })}
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
autoComplete="off"
className="flex-1 m-3"
disabled={!hasEditRights}
{...register("description")}
/>
</>
);
};

View File

@ -0,0 +1,98 @@
import { UUID } from "crypto";
import { UseFormRegister } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Field from "@/lib/components/ui/Field";
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { BrainConfig } from "@/lib/types/brainConfig";
import { SaveButton } from "@/shared/SaveButton";
type ModelSelectionProps = {
brainId: UUID;
temperature: number;
maxTokens: number;
model: "gpt-3.5-turbo" | "gpt-3.5-turbo-16k";
handleSubmit: (checkDirty: boolean) => Promise<void>;
register: UseFormRegister<BrainConfig>;
hasEditRights: boolean;
accessibleModels: string[];
};
export const ModelSelection = (props: ModelSelectionProps): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
handleSubmit,
register,
temperature,
maxTokens,
model,
hasEditRights,
accessibleModels,
} = props;
return (
<>
<Field
label={t("openAiKeyLabel", { ns: "config" })}
placeholder={t("openAiKeyPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("openAiKey")}
/>
<fieldset className="w-full flex flex-col mt-2">
<label className="flex-1 text-sm" htmlFor="model">
{t("modelLabel", { ns: "config" })}
</label>
<select
id="model"
disabled={!hasEditRights}
{...register("model")}
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
onChange={() => {
void handleSubmit(false); // Trigger form submission
}}
>
{accessibleModels.map((availableModel) => (
<option value={availableModel} key={availableModel}>
{availableModel}
</option>
))}
</select>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="temp">
{t("temperature", { ns: "config" })}: {temperature}
</label>
<input
id="temp"
type="range"
min="0"
max="1"
step="0.01"
value={temperature}
disabled={!hasEditRights}
{...register("temperature")}
/>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="tokens">
{t("maxTokens", { ns: "config" })}: {maxTokens}
</label>
<input
type="range"
min="10"
max={defineMaxTokens(model)}
value={maxTokens}
disabled={!hasEditRights}
{...register("maxTokens")}
/>
</fieldset>
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
</>
);
};

View File

@ -0,0 +1,73 @@
import { UUID } from "crypto";
import { UseFormRegister } from "react-hook-form";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import Field from "@/lib/components/ui/Field";
import { TextArea } from "@/lib/components/ui/TextArea";
import { BrainConfig } from "@/lib/types/brainConfig";
import { SaveButton } from "@/shared/SaveButton";
import { PublicPrompts } from "../PublicPrompts";
type PromptProps = {
brainId: UUID;
pickPublicPrompt: ({
title,
content,
}: {
title: string;
content: string;
}) => void;
removeBrainPrompt: () => Promise<void>;
isUpdating: boolean;
handleSubmit: (checkDirty: boolean) => Promise<void>;
register: UseFormRegister<BrainConfig>;
promptId?: string;
hasEditRights: boolean;
};
export const Prompt = (props: PromptProps): JSX.Element => {
const { t } = useTranslation(["translation", "brain", "config"]);
const {
pickPublicPrompt,
removeBrainPrompt,
isUpdating,
handleSubmit,
register,
hasEditRights,
promptId,
} = props;
return (
<>
{hasEditRights && <PublicPrompts onSelect={pickPublicPrompt} />}
<Field
label={t("promptName", { ns: "config" })}
placeholder={t("promptNamePlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("prompt.title")}
/>
<TextArea
label={t("promptContent", { ns: "config" })}
placeholder={t("promptContentPlaceholder", { ns: "config" })}
autoComplete="off"
className="flex-1"
disabled={!hasEditRights}
{...register("prompt.content")}
/>
{hasEditRights && (
<div className="flex w-full justify-end py-4">
<SaveButton handleSubmit={handleSubmit} />
</div>
)}
{hasEditRights && promptId !== "" && (
<Button disabled={isUpdating} onClick={() => void removeBrainPrompt()}>
{t("removePrompt", { ns: "config" })}
</Button>
)}
</>
);
};

View File

@ -1 +1,4 @@
export * from "./GeneralInformation";
export * from "./ModelSelection";
export * from "./Prompt";
export * from "./PublicPrompts";

View File

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

View File

@ -1,3 +1,4 @@
export * from "./BrainTabTrigger";
export * from "./KnowledgeTab";
export * from "./PeopleTab";
export * from "./SettingsTab";

View File

@ -38,6 +38,7 @@
"@supabase/auth-ui-shared": "^0.1.6",
"@supabase/supabase-js": "^2.22.0",
"@tanstack/react-query": "^4.33.0",
"@testing-library/user-event": "^14.5.1",
"@types/dom-speech-recognition": "^0.0.1",
"@types/draft-js": "^0.11.12",
"@types/lodash": "^4.14.197",
@ -96,4 +97,4 @@
"react-icons": "^4.8.0",
"vitest": "^0.32.2"
}
}
}