add brain settings tab

This commit is contained in:
mamadoudicko 2023-07-25 16:49:04 +02:00
parent abffb65f5f
commit 77b3d6bedb
10 changed files with 305 additions and 20 deletions

View File

@ -10,6 +10,7 @@ from models.brains import (
) )
from models.settings import BrainRateLimiting from models.settings import BrainRateLimiting
from models.users import User from models.users import User
from routes.authorizations.brain_authorization import RoleEnum, has_brain_authorization from routes.authorizations.brain_authorization import RoleEnum, has_brain_authorization
logger = get_logger(__name__) logger = get_logger(__name__)
@ -74,10 +75,7 @@ async def get_brain_endpoint(
brain = Brain(id=brain_id) brain = Brain(id=brain_id)
brains = brain.get_brain_details() brains = brain.get_brain_details()
if len(brains) > 0: if len(brains) > 0:
return { return brains[0]
"id": brain_id,
"name": brains[0]["name"],
}
else: else:
return HTTPException( return HTTPException(
status_code=404, status_code=404,

View File

@ -1,6 +1,7 @@
import { Content, List, Root } from "@radix-ui/react-tabs"; import { Content, List, Root } from "@radix-ui/react-tabs";
import { BrainTabTrigger, PeopleTab } from "./components"; import { BrainTabTrigger, PeopleTab } from "./components";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs"; import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
export const BrainManagementTabs = (): JSX.Element => { export const BrainManagementTabs = (): JSX.Element => {
@ -38,11 +39,14 @@ export const BrainManagementTabs = (): JSX.Element => {
<div className="p-20 pt-5"> <div className="p-20 pt-5">
<Content value="settings"> <Content value="settings">
<p>coming soon</p> <SettingsTab brainId={brainId} />
</Content> </Content>
<Content value="people"> <Content value="people">
<PeopleTab brainId={brainId} /> <PeopleTab brainId={brainId} />
</Content> </Content>
<Content value="knowledge">
<p>Coming soon</p>
</Content>
</div> </div>
</Root> </Root>
); );

View File

@ -0,0 +1,121 @@
/* eslint-disable max-lines */
import { UUID } from "crypto";
import Button from "@/lib/components/ui/Button";
import { Divider } from "@/lib/components/ui/Divider";
import Field from "@/lib/components/ui/Field";
import { TextArea } from "@/lib/components/ui/TextField";
import { models, paidModels } from "@/lib/context/BrainConfigProvider/types";
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
import { ApiKeyConfig } from "./components";
import { useSettingsTab } from "./hooks/useSettingsTab";
type SettingsTabProps = {
brainId: UUID;
};
export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
const {
handleSubmit,
register,
hasChanges,
openAiKey,
temperature,
maxTokens,
model,
setAsDefaultBrainHandler,
isSettingAsDefault,
isUpdating,
} = useSettingsTab({ brainId });
return (
<form
onSubmit={(e) => void handleSubmit(e)}
className="my-10 mb-0 flex flex-col items-center gap-2"
>
<Field
label="Name"
placeholder="E.g. History notes"
autoComplete="off"
className="flex-1"
{...register("name")}
/>
<TextArea
label="Description"
placeholder="My new brain is about..."
autoComplete="off"
className="flex-1 m-3"
{...register("description")}
/>
<Divider text="Model config" />
<Field
label="OpenAI API Key"
placeholder="sk-xxx"
autoComplete="off"
className="flex-1"
{...register("openAiKey")}
/>
<fieldset className="w-full flex flex-col mt-2">
<label className="flex-1 text-sm" htmlFor="model">
Model
</label>
<select
id="model"
{...register("model")}
className="px-5 py-2 dark:bg-gray-700 bg-gray-200 rounded-md"
>
{(openAiKey !== undefined ? paidModels : models).map(
(availableModel) => (
<option value={availableModel} key={availableModel}>
{availableModel}
</option>
)
)}
</select>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="temp">
Temperature: {temperature}
</label>
<input
id="temp"
type="range"
min="0"
max="1"
step="0.01"
value={temperature}
{...register("temperature")}
/>
</fieldset>
<fieldset className="w-full flex mt-4">
<label className="flex-1" htmlFor="tokens">
Max tokens: {maxTokens}
</label>
<input
type="range"
min="10"
max={defineMaxTokens(model)}
value={maxTokens}
{...register("maxTokens")}
/>
</fieldset>
<div className="flex flex-row justify-end flex-1 w-full mt-8">
<Button isLoading={isUpdating} disabled={!hasChanges}>
Save changes
</Button>
</div>
<Divider text="Default brain" className="mt-4" />
<Button
variant={"secondary"}
isLoading={isSettingAsDefault}
onClick={() => void setAsDefaultBrainHandler()}
>
Set as default brain
</Button>
<ApiKeyConfig />
</form>
);
};

View File

@ -0,0 +1,154 @@
/* eslint-disable complexity */
/* eslint-disable max-lines */
import axios from "axios";
import { UUID } from "crypto";
import { FormEvent, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useBrainConfig } from "@/lib/context/BrainConfigProvider";
import { defineMaxTokens } from "@/lib/helpers/defineMexTokens";
import { useToast } from "@/lib/hooks";
type UseSettingsTabProps = {
brainId: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
const [isUpdating, setIsUpdating] = useState(false);
const [isSettingAsDefault, setIsSettingHasDefault] = useState(false);
const { publish } = useToast();
const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi();
const { config } = useBrainConfig();
const defaultValues = {
...config,
name: "",
description: "",
setDefault: false,
};
const {
register,
getValues,
reset,
watch,
setValue,
formState: { dirtyFields },
} = useForm({
defaultValues,
});
useEffect(() => {
const fetchBrain = async () => {
const brain = await getBrain(brainId);
if (brain === undefined) {
return;
}
reset({
...brain,
maxTokens: brain.max_tokens,
});
};
void fetchBrain();
}, []);
const openAiKey = watch("openAiKey");
const model = watch("model");
const temperature = watch("temperature");
const maxTokens = watch("maxTokens");
useEffect(() => {
setValue("maxTokens", Math.min(maxTokens, defineMaxTokens(model)));
}, [maxTokens, model, setValue]);
const setAsDefaultBrainHandler = async () => {
try {
setIsSettingHasDefault(true);
await setAsDefaultBrain(brainId);
publish({
variant: "success",
text: "Brain set as default successfully",
});
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 429) {
publish({
variant: "danger",
text: `${JSON.stringify(
(
err.response as {
data: { detail: string };
}
).data.detail
)}`,
});
return;
}
} finally {
setIsSettingHasDefault(false);
}
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const { name: isNameDirty } = dirtyFields;
const { name } = getValues();
if (isNameDirty !== undefined && isNameDirty && name.trim() === "") {
publish({
variant: "danger",
text: "Name is required",
});
return;
}
try {
setIsUpdating(true);
await updateBrain(brainId, getValues());
reset(defaultValues);
publish({
variant: "success",
text: "Brain created successfully",
});
} catch (err) {
if (axios.isAxiosError(err) && err.response?.status === 429) {
publish({
variant: "danger",
text: `${JSON.stringify(
(
err.response as {
data: { detail: string };
}
).data.detail
)}`,
});
} else {
publish({
variant: "danger",
text: `${JSON.stringify(err)}`,
});
}
} finally {
setIsUpdating(false);
}
};
return {
handleSubmit,
register,
openAiKey,
model,
temperature,
maxTokens,
isUpdating,
hasChanges: Object.keys(dirtyFields).length > 0,
setAsDefaultBrainHandler,
isSettingAsDefault,
};
};

View File

@ -4,8 +4,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { Subscription } from "../brain"; import { Subscription } from "../brain";
import { import {
CreateOrUpdateBrainInput, CreateBrainInput,
SubscriptionUpdatableProperties, SubscriptionUpdatableProperties,
UpdateBrainInput,
} from "../types"; } from "../types";
import { useBrainApi } from "../useBrainApi"; import { useBrainApi } from "../useBrainApi";
@ -61,7 +62,7 @@ describe("useBrainApi", () => {
}, },
} = renderHook(() => useBrainApi()); } = renderHook(() => useBrainApi());
const brain: CreateOrUpdateBrainInput = { const brain: CreateBrainInput = {
name: "Test Brain", name: "Test Brain",
description: "This is a description", description: "This is a description",
status: "public", status: "public",
@ -212,7 +213,7 @@ describe("useBrainApi", () => {
}, },
} = renderHook(() => useBrainApi()); } = renderHook(() => useBrainApi());
const brainId = "123"; const brainId = "123";
const brain: CreateOrUpdateBrainInput = { const brain: UpdateBrainInput = {
name: "Test Brain", name: "Test Brain",
description: "This is a description", description: "This is a description",
status: "public", status: "public",

View File

@ -10,8 +10,9 @@ import {
import { Document } from "@/lib/types/Document"; import { Document } from "@/lib/types/Document";
import { import {
CreateOrUpdateBrainInput, CreateBrainInput,
SubscriptionUpdatableProperties, SubscriptionUpdatableProperties,
UpdateBrainInput,
} from "./types"; } from "./types";
import { mapBackendMinimalBrainToMinimalBrain } from "./utils/mapBackendMinimalBrainToMinimalBrain"; import { mapBackendMinimalBrainToMinimalBrain } from "./utils/mapBackendMinimalBrainToMinimalBrain";
import { import {
@ -32,7 +33,7 @@ export const getBrainDocuments = async (
}; };
export const createBrain = async ( export const createBrain = async (
brain: CreateOrUpdateBrainInput, brain: CreateBrainInput,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<MinimalBrainForUser> => { ): Promise<MinimalBrainForUser> => {
return mapBackendMinimalBrainToMinimalBrain( return mapBackendMinimalBrainToMinimalBrain(
@ -130,7 +131,7 @@ export const setAsDefaultBrain = async (
export const updateBrain = async ( export const updateBrain = async (
brainId: string, brainId: string,
brain: CreateOrUpdateBrainInput, brain: UpdateBrainInput,
axiosInstance: AxiosInstance axiosInstance: AxiosInstance
): Promise<void> => { ): Promise<void> => {
await axiosInstance.put(`/brains/${brainId}/`, brain); await axiosInstance.put(`/brains/${brainId}/`, brain);

View File

@ -4,7 +4,7 @@ export type SubscriptionUpdatableProperties = {
role: BrainRoleType | null; role: BrainRoleType | null;
}; };
export type CreateOrUpdateBrainInput = { export type CreateBrainInput = {
name: string; name: string;
description?: string; description?: string;
status?: string; status?: string;
@ -13,3 +13,5 @@ export type CreateOrUpdateBrainInput = {
max_tokens?: number; max_tokens?: number;
openai_api_key?: string; openai_api_key?: string;
}; };
export type UpdateBrainInput = Partial<CreateBrainInput>;

View File

@ -15,8 +15,9 @@ import {
updateBrainAccess, updateBrainAccess,
} from "./brain"; } from "./brain";
import { import {
CreateOrUpdateBrainInput, CreateBrainInput,
SubscriptionUpdatableProperties, SubscriptionUpdatableProperties,
UpdateBrainInput,
} from "./types"; } from "./types";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -26,7 +27,7 @@ export const useBrainApi = () => {
return { return {
getBrainDocuments: async (brainId: string) => getBrainDocuments: async (brainId: string) =>
getBrainDocuments(brainId, axiosInstance), getBrainDocuments(brainId, axiosInstance),
createBrain: async (brain: CreateOrUpdateBrainInput) => createBrain: async (brain: CreateBrainInput) =>
createBrain(brain, axiosInstance), createBrain(brain, axiosInstance),
deleteBrain: async (id: string) => deleteBrain(id, axiosInstance), deleteBrain: async (id: string) => deleteBrain(id, axiosInstance),
getDefaultBrain: async () => getDefaultBrain(axiosInstance), getDefaultBrain: async () => getDefaultBrain(axiosInstance),
@ -45,7 +46,7 @@ export const useBrainApi = () => {
) => updateBrainAccess(brainId, userEmail, subscription, axiosInstance), ) => updateBrainAccess(brainId, userEmail, subscription, axiosInstance),
setAsDefaultBrain: async (brainId: string) => setAsDefaultBrain: async (brainId: string) =>
setAsDefaultBrain(brainId, axiosInstance), setAsDefaultBrain(brainId, axiosInstance),
updateBrain: async (brainId: string, brain: CreateOrUpdateBrainInput) => updateBrain: async (brainId: string, brain: UpdateBrainInput) =>
updateBrain(brainId, brain, axiosInstance), updateBrain(brainId, brain, axiosInstance),
}; };
}; };

View File

@ -2,7 +2,7 @@
import { UUID } from "crypto"; import { UUID } from "crypto";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { CreateOrUpdateBrainInput } from "@/lib/api/brain/types"; import { CreateBrainInput } from "@/lib/api/brain/types";
import { useBrainApi } from "@/lib/api/brain/useBrainApi"; import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useToast } from "@/lib/hooks"; import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/useEventTracking"; import { useEventTracking } from "@/services/analytics/useEventTracking";
@ -29,7 +29,7 @@ export const useBrainProvider = () => {
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId); const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
const createBrainHandler = async ( const createBrainHandler = async (
brain: CreateOrUpdateBrainInput brain: CreateBrainInput
): Promise<UUID | undefined> => { ): Promise<UUID | undefined> => {
const createdBrain = await createBrain(brain); const createdBrain = await createBrain(brain);
try { try {

View File

@ -4,15 +4,18 @@ import { BrainRoleType } from "@/lib/components/NavBar/components/NavItems/compo
import { Document } from "@/lib/types/Document"; import { Document } from "@/lib/types/Document";
import { useBrainProvider } from "./hooks/useBrainProvider"; import { useBrainProvider } from "./hooks/useBrainProvider";
import { Model } from "../BrainConfigProvider/types";
export type Brain = { export type Brain = {
id: UUID; id: UUID;
name: string; name: string;
documents?: Document[]; documents?: Document[];
status?: string; status?: string;
model?: string; model?: Model;
max_tokens?: string; max_tokens?: number;
temperature?: string; temperature?: number;
openai_api_key?: string;
description?: string;
}; };
export type MinimalBrainForUser = { export type MinimalBrainForUser = {