mirror of
https://github.com/StanGirard/quivr.git
synced 2024-11-27 10:20:32 +03:00
feat: allow setting public brain status to private (#1258)
* feat: refetch brains list on when new brain is added * feat: update BrainConfig type * feat: update useSettingsTab add usebrainFormState and useSettings tab * feat: add <PrivateAccessConfirmationModal/> modal * feat: update translations * feat: handle brain status change to private * feat: validate chat access * test: fix failaing tests and remove deprecated
This commit is contained in:
parent
4b88c89814
commit
a4a2d769b3
@ -160,6 +160,17 @@ class Brain(Repository):
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def delete_brain_subscribers(self, brain_id: UUID):
|
||||||
|
results = (
|
||||||
|
self.db.table("brains_users")
|
||||||
|
.delete()
|
||||||
|
.match({"brain_id": str(brain_id)})
|
||||||
|
.match({"rights": "Viewer"})
|
||||||
|
.execute()
|
||||||
|
).data
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
def delete_brain(self, brain_id: str):
|
def delete_brain(self, brain_id: str):
|
||||||
results = (
|
results = (
|
||||||
self.db.table("brains").delete().match({"brain_id": brain_id}).execute()
|
self.db.table("brains").delete().match({"brain_id": brain_id}).execute()
|
||||||
|
10
backend/repository/brain/delete_brain_users.py
Normal file
10
backend/repository/brain/delete_brain_users.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from models.settings import get_supabase_db
|
||||||
|
|
||||||
|
|
||||||
|
def delete_brain_users(brain_id: UUID) -> None:
|
||||||
|
supabase_db = get_supabase_db()
|
||||||
|
supabase_db.delete_brain_subscribers(
|
||||||
|
brain_id=brain_id,
|
||||||
|
)
|
@ -59,7 +59,7 @@ def validate_brain_authorization(
|
|||||||
user_brain = get_brain_for_user(user_id, brain_id)
|
user_brain = get_brain_for_user(user_id, brain_id)
|
||||||
if user_brain is None:
|
if user_brain is None:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
detail="You don't have permission for this brain",
|
detail="You don't have permission for this brain",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from repository.brain import (
|
|||||||
set_as_default_brain_for_user,
|
set_as_default_brain_for_user,
|
||||||
update_brain_by_id,
|
update_brain_by_id,
|
||||||
)
|
)
|
||||||
|
from repository.brain.delete_brain_users import delete_brain_users
|
||||||
from repository.brain.get_public_brains import get_public_brains
|
from repository.brain.get_public_brains import get_public_brains
|
||||||
from repository.prompt import delete_prompt_by_id, get_prompt_by_id
|
from repository.prompt import delete_prompt_by_id, get_prompt_by_id
|
||||||
|
|
||||||
@ -182,27 +183,31 @@ async def create_brain_endpoint(
|
|||||||
)
|
)
|
||||||
async def update_brain_endpoint(
|
async def update_brain_endpoint(
|
||||||
brain_id: UUID,
|
brain_id: UUID,
|
||||||
input_brain: BrainUpdatableProperties,
|
brain_to_update: BrainUpdatableProperties,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Update an existing brain with new brain configuration
|
Update an existing brain with new brain configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Remove prompt if it is private and no longer used by brain
|
# Remove prompt if it is private and no longer used by brain
|
||||||
if input_brain.prompt_id is None:
|
existing_brain = get_brain_details(brain_id)
|
||||||
existing_brain = get_brain_details(brain_id)
|
if existing_brain is None:
|
||||||
if existing_brain is None:
|
raise HTTPException(
|
||||||
raise HTTPException(
|
status_code=404,
|
||||||
status_code=404,
|
detail="Brain not found",
|
||||||
detail="Brain not found",
|
)
|
||||||
)
|
|
||||||
|
if brain_to_update.prompt_id is None:
|
||||||
prompt_id = existing_brain.prompt_id
|
prompt_id = existing_brain.prompt_id
|
||||||
if prompt_id is not None:
|
if prompt_id is not None:
|
||||||
prompt = get_prompt_by_id(prompt_id)
|
prompt = get_prompt_by_id(prompt_id)
|
||||||
if prompt is not None and prompt.status == "private":
|
if prompt is not None and prompt.status == "private":
|
||||||
delete_prompt_by_id(prompt_id)
|
delete_prompt_by_id(prompt_id)
|
||||||
|
|
||||||
update_brain_by_id(brain_id, input_brain)
|
if brain_to_update.status == "private" and existing_brain.status == "public":
|
||||||
|
delete_brain_users(brain_id)
|
||||||
|
|
||||||
|
update_brain_by_id(brain_id, brain_to_update)
|
||||||
|
|
||||||
return {"message": f"Brain {brain_id} has been updated."}
|
return {"message": f"Brain {brain_id} has been updated."}
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ from repository.chat.get_chat_history_with_notifications import (
|
|||||||
from repository.notification.remove_chat_notifications import remove_chat_notifications
|
from repository.notification.remove_chat_notifications import remove_chat_notifications
|
||||||
from repository.user_identity import get_user_identity
|
from repository.user_identity import get_user_identity
|
||||||
|
|
||||||
|
from routes.authorizations.brain_authorization import has_brain_authorization
|
||||||
|
from routes.authorizations.types import RoleEnum
|
||||||
|
|
||||||
chat_router = APIRouter()
|
chat_router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@ -167,6 +170,9 @@ async def create_chat_handler(
|
|||||||
Depends(
|
Depends(
|
||||||
AuthBearer(),
|
AuthBearer(),
|
||||||
),
|
),
|
||||||
|
Depends(
|
||||||
|
has_brain_authorization([RoleEnum.Viewer, RoleEnum.Owner, RoleEnum.Editor])
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tags=["Chat"],
|
tags=["Chat"],
|
||||||
)
|
)
|
||||||
@ -255,6 +261,9 @@ async def create_question_handler(
|
|||||||
Depends(
|
Depends(
|
||||||
AuthBearer(),
|
AuthBearer(),
|
||||||
),
|
),
|
||||||
|
Depends(
|
||||||
|
has_brain_authorization([RoleEnum.Viewer, RoleEnum.Owner, RoleEnum.Editor])
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tags=["Chat"],
|
tags=["Chat"],
|
||||||
)
|
)
|
||||||
|
@ -8,11 +8,14 @@ import Button from "@/lib/components/ui/Button";
|
|||||||
import { Chip } from "@/lib/components/ui/Chip";
|
import { Chip } from "@/lib/components/ui/Chip";
|
||||||
import { Divider } from "@/lib/components/ui/Divider";
|
import { Divider } from "@/lib/components/ui/Divider";
|
||||||
import Field from "@/lib/components/ui/Field";
|
import Field from "@/lib/components/ui/Field";
|
||||||
|
import { Radio } from "@/lib/components/ui/Radio";
|
||||||
import { TextArea } from "@/lib/components/ui/TextArea";
|
import { TextArea } from "@/lib/components/ui/TextArea";
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
||||||
import { SaveButton } from "@/shared/SaveButton";
|
import { SaveButton } from "@/shared/SaveButton";
|
||||||
|
|
||||||
|
import { PrivateAccessConfirmationModal } from "./components/PrivateAccessConfirmationModal/PrivateAccessConfirmationModal";
|
||||||
|
import { usePrivateAccessConfirmationModal } from "./components/PrivateAccessConfirmationModal/hooks/usePrivateAccessConfirmationModal";
|
||||||
import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
|
import { PublicPrompts } from "./components/PublicPrompts/PublicPrompts";
|
||||||
import { useSettingsTab } from "./hooks/useSettingsTab";
|
import { useSettingsTab } from "./hooks/useSettingsTab";
|
||||||
import { getBrainPermissions } from "../../utils/getBrainPermissions";
|
import { getBrainPermissions } from "../../utils/getBrainPermissions";
|
||||||
@ -39,166 +42,198 @@ export const SettingsTab = ({ brainId }: SettingsTabProps): JSX.Element => {
|
|||||||
pickPublicPrompt,
|
pickPublicPrompt,
|
||||||
removeBrainPrompt,
|
removeBrainPrompt,
|
||||||
accessibleModels,
|
accessibleModels,
|
||||||
|
brainStatusOptions,
|
||||||
|
status,
|
||||||
|
setValue,
|
||||||
|
dirtyFields,
|
||||||
} = useSettingsTab({ brainId });
|
} = useSettingsTab({ brainId });
|
||||||
|
const { onCancel, isPrivateAccessModalOpened, closeModal } =
|
||||||
|
usePrivateAccessConfirmationModal({
|
||||||
|
status,
|
||||||
|
setValue,
|
||||||
|
isStatusDirty: Boolean(dirtyFields.status),
|
||||||
|
});
|
||||||
|
|
||||||
const { allBrains } = useBrainContext();
|
const { allBrains } = useBrainContext();
|
||||||
|
|
||||||
const { hasEditRights, isPublicBrain } = getBrainPermissions({
|
const { hasEditRights, isOwnedByCurrentUser, isPublicBrain } =
|
||||||
brainId,
|
getBrainPermissions({
|
||||||
userAccessibleBrains: allBrains,
|
brainId,
|
||||||
});
|
userAccessibleBrains: allBrains,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<>
|
||||||
onSubmit={(e) => {
|
<form
|
||||||
e.preventDefault();
|
onSubmit={(e) => {
|
||||||
void handleSubmit(true);
|
e.preventDefault();
|
||||||
}}
|
void handleSubmit(true);
|
||||||
className="my-10 mb-0 flex flex-col items-center gap-2"
|
}}
|
||||||
ref={formRef}
|
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>
|
<div className="flex flex-row flex-1 justify-between w-full items-end">
|
||||||
<Field
|
<div>
|
||||||
label={t("brainName", { ns: "brain" })}
|
<Field
|
||||||
placeholder={t("brainNamePlaceholder", { ns: "brain" })}
|
label={t("brainName", { ns: "brain" })}
|
||||||
autoComplete="off"
|
placeholder={t("brainNamePlaceholder", { ns: "brain" })}
|
||||||
className="flex-1"
|
autoComplete="off"
|
||||||
required
|
className="flex-1"
|
||||||
disabled={!hasEditRights}
|
required
|
||||||
{...register("name")}
|
disabled={!hasEditRights}
|
||||||
/>
|
{...register("name")}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<div className="flex flex-1 items-center flex-col">
|
<div className="flex flex-1 items-center flex-col">
|
||||||
{isPublicBrain && (
|
{isPublicBrain && !isOwnedByCurrentUser && (
|
||||||
<Chip className="mb-3 bg-purple-600 text-white w-full">
|
<Chip className="mb-3 bg-purple-600 text-white w-full">
|
||||||
{t("brain:public_brain_label")}
|
{t("brain:public_brain_label")}
|
||||||
</Chip>
|
</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">
|
{isDefaultBrain ? (
|
||||||
{t("defaultBrain", { ns: "brain" })}
|
<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">
|
||||||
</div>
|
{t("defaultBrain", { ns: "brain" })}
|
||||||
) : (
|
</div>
|
||||||
<Button
|
) : (
|
||||||
variant={"secondary"}
|
<Button
|
||||||
isLoading={isSettingAsDefault}
|
variant={"secondary"}
|
||||||
onClick={() => void setAsDefaultBrainHandler()}
|
isLoading={isSettingAsDefault}
|
||||||
type="button"
|
onClick={() => void setAsDefaultBrainHandler()}
|
||||||
disabled={!hasEditRights}
|
type="button"
|
||||||
>
|
disabled={!hasEditRights}
|
||||||
{t("setDefaultBrain", { ns: "brain" })}
|
>
|
||||||
</Button>
|
{t("setDefaultBrain", { ns: "brain" })}
|
||||||
)}
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{isPublicBrain && isOwnedByCurrentUser && (
|
||||||
<TextArea
|
<div className="w-full mt-4">
|
||||||
label={t("brainDescription", { ns: "brain" })}
|
<Radio
|
||||||
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
|
items={brainStatusOptions}
|
||||||
autoComplete="off"
|
label={t("brain_status_label", { ns: "brain" })}
|
||||||
className="flex-1 m-3"
|
value={status}
|
||||||
disabled={!hasEditRights}
|
className="flex-1 justify-between w-[50%]"
|
||||||
{...register("description")}
|
{...register("status")}
|
||||||
/>
|
/>
|
||||||
<Divider text={t("modelSection", { ns: "config" })} />
|
</div>
|
||||||
<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>
|
|
||||||
<div className="flex w-full justify-end py-4">
|
|
||||||
<SaveButton disabled={!hasEditRights} 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")}
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
label={t("promptContent", { ns: "config" })}
|
|
||||||
placeholder={t("promptContentPlaceholder", { ns: "config" })}
|
|
||||||
autoComplete="off"
|
|
||||||
className="flex-1"
|
|
||||||
disabled={!hasEditRights}
|
|
||||||
{...register("prompt.content")}
|
|
||||||
/>
|
|
||||||
<div className="flex w-full justify-end py-4">
|
|
||||||
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
|
|
||||||
</div>
|
|
||||||
{promptId !== "" && (
|
|
||||||
<Button
|
|
||||||
disabled={isUpdating || !hasEditRights}
|
|
||||||
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 && (
|
|
||||||
<span className="ml-2 text-sm">
|
|
||||||
{t("updatingBrainSettings", { ns: "config" })}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
<TextArea
|
||||||
</form>
|
label={t("brainDescription", { ns: "brain" })}
|
||||||
|
placeholder={t("brainDescriptionPlaceholder", { ns: "brain" })}
|
||||||
|
autoComplete="off"
|
||||||
|
className="flex-1 m-3"
|
||||||
|
disabled={!hasEditRights}
|
||||||
|
{...register("description")}
|
||||||
|
/>
|
||||||
|
<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")}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
<div className="flex w-full justify-end py-4">
|
||||||
|
<SaveButton disabled={!hasEditRights} 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")}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
label={t("promptContent", { ns: "config" })}
|
||||||
|
placeholder={t("promptContentPlaceholder", { ns: "config" })}
|
||||||
|
autoComplete="off"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={!hasEditRights}
|
||||||
|
{...register("prompt.content")}
|
||||||
|
/>
|
||||||
|
<div className="flex w-full justify-end py-4">
|
||||||
|
<SaveButton disabled={!hasEditRights} handleSubmit={handleSubmit} />
|
||||||
|
</div>
|
||||||
|
{promptId !== "" && (
|
||||||
|
<Button
|
||||||
|
disabled={isUpdating || !hasEditRights}
|
||||||
|
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 && (
|
||||||
|
<span className="ml-2 text-sm">
|
||||||
|
{t("updatingBrainSettings", { ns: "config" })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<PrivateAccessConfirmationModal
|
||||||
|
opened={isPrivateAccessModalOpened}
|
||||||
|
onClose={onCancel}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onConfirm={closeModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import Button from "@/lib/components/ui/Button";
|
||||||
|
import { Modal } from "@/lib/components/ui/Modal";
|
||||||
|
|
||||||
|
type PrivateAccessConfirmationModalProps = {
|
||||||
|
opened: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
};
|
||||||
|
export const PrivateAccessConfirmationModal = ({
|
||||||
|
opened,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: PrivateAccessConfirmationModalProps): JSX.Element => {
|
||||||
|
const { t } = useTranslation(["brain"]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={opened} setOpen={onClose} CloseTrigger={<div />}>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t("set_brain_status_to_private_modal_title"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: t("set_brain_status_to_private_modal_description"),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between pt-10 px-10">
|
||||||
|
<Button type="button" onClick={onConfirm} variant="secondary">
|
||||||
|
{t("confirm_set_brain_status_to_private")}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" onClick={onCancel}>
|
||||||
|
{t("cancel_set_brain_status_to_private")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { BrainStatus } from "@/lib/types/brainConfig";
|
||||||
|
|
||||||
|
type UsePrivateAccessModalProps = {
|
||||||
|
status: BrainStatus;
|
||||||
|
setValue: (name: "status", value: "private" | "public") => void;
|
||||||
|
isStatusDirty: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const usePrivateAccessConfirmationModal = ({
|
||||||
|
status,
|
||||||
|
setValue,
|
||||||
|
isStatusDirty,
|
||||||
|
}: UsePrivateAccessModalProps) => {
|
||||||
|
const [isPrivateAccessModalOpened, setIsPrivateAccessModalOpened] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status === "private" && isStatusDirty) {
|
||||||
|
setIsPrivateAccessModalOpened(true);
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setIsPrivateAccessModalOpened(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
closeModal();
|
||||||
|
setValue("status", "public");
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPrivateAccessModalOpened,
|
||||||
|
closeModal,
|
||||||
|
onCancel,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./PrivateAccessConfirmationModal";
|
@ -0,0 +1,55 @@
|
|||||||
|
import { UUID } from "crypto";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import { defaultBrainConfig } from "@/lib/config/defaultBrainConfig";
|
||||||
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
|
import { BrainConfig } from "@/lib/types/brainConfig";
|
||||||
|
|
||||||
|
import { useBrainFetcher } from "../../../hooks/useBrainFetcher";
|
||||||
|
|
||||||
|
type UseBrainFormStateProps = {
|
||||||
|
brainId: UUID;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useBrainFormState = ({ brainId }: UseBrainFormStateProps) => {
|
||||||
|
const { defaultBrainId } = useBrainContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
getValues,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
reset,
|
||||||
|
formState: { dirtyFields },
|
||||||
|
} = useForm<BrainConfig>({
|
||||||
|
defaultValues: { ...defaultBrainConfig, status: undefined },
|
||||||
|
});
|
||||||
|
const { brain } = useBrainFetcher({
|
||||||
|
brainId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDefaultBrain = defaultBrainId === brainId;
|
||||||
|
const promptId = watch("prompt_id");
|
||||||
|
const openAiKey = watch("openAiKey");
|
||||||
|
const model = watch("model");
|
||||||
|
const temperature = watch("temperature");
|
||||||
|
const maxTokens = watch("maxTokens");
|
||||||
|
const status = watch("status");
|
||||||
|
|
||||||
|
return {
|
||||||
|
brain,
|
||||||
|
model,
|
||||||
|
temperature,
|
||||||
|
maxTokens,
|
||||||
|
isDefaultBrain,
|
||||||
|
promptId,
|
||||||
|
openAiKey,
|
||||||
|
dirtyFields,
|
||||||
|
status,
|
||||||
|
register,
|
||||||
|
getValues,
|
||||||
|
setValue,
|
||||||
|
reset,
|
||||||
|
};
|
||||||
|
};
|
@ -1,24 +1,21 @@
|
|||||||
/* eslint-disable complexity */
|
/* eslint-disable complexity */
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { UUID } from "crypto";
|
import { UUID } from "crypto";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||||
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
||||||
import { USER_DATA_KEY } from "@/lib/api/user/config";
|
|
||||||
import { useUserApi } from "@/lib/api/user/useUserApi";
|
|
||||||
import { defaultBrainConfig } from "@/lib/config/defaultBrainConfig";
|
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { Brain } from "@/lib/context/BrainProvider/types";
|
import { Brain } from "@/lib/context/BrainProvider/types";
|
||||||
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
||||||
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
|
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
|
import { useUserData } from "@/lib/hooks/useUserData";
|
||||||
|
import { BrainStatus } from "@/lib/types/brainConfig";
|
||||||
|
|
||||||
import { useBrainFetcher } from "../../../hooks/useBrainFetcher";
|
import { useBrainFormState } from "./useBrainFormState";
|
||||||
import { validateOpenAIKey } from "../utils/validateOpenAIKey";
|
import { validateOpenAIKey } from "../utils/validateOpenAIKey";
|
||||||
|
|
||||||
type UseSettingsTabProps = {
|
type UseSettingsTabProps = {
|
||||||
@ -33,48 +30,41 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
|||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const { setAsDefaultBrain, updateBrain } = useBrainApi();
|
const { setAsDefaultBrain, updateBrain } = useBrainApi();
|
||||||
const { fetchAllBrains, fetchDefaultBrain, defaultBrainId } =
|
const { fetchAllBrains, fetchDefaultBrain } = useBrainContext();
|
||||||
useBrainContext();
|
|
||||||
const { getPrompt, updatePrompt, createPrompt } = usePromptApi();
|
const { getPrompt, updatePrompt, createPrompt } = usePromptApi();
|
||||||
const { getUser } = useUserApi();
|
const { userData } = useUserData();
|
||||||
|
|
||||||
const { data: userData } = useQuery({
|
|
||||||
queryKey: [USER_DATA_KEY],
|
|
||||||
queryFn: getUser,
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultValues = {
|
|
||||||
...defaultBrainConfig,
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
setDefault: false,
|
|
||||||
prompt_id: "",
|
|
||||||
prompt: {
|
|
||||||
title: "",
|
|
||||||
content: "",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
brain,
|
||||||
|
dirtyFields,
|
||||||
getValues,
|
getValues,
|
||||||
watch,
|
maxTokens,
|
||||||
setValue,
|
promptId,
|
||||||
|
register,
|
||||||
reset,
|
reset,
|
||||||
formState: { dirtyFields },
|
setValue,
|
||||||
} = useForm({
|
openAiKey,
|
||||||
defaultValues,
|
model,
|
||||||
});
|
temperature,
|
||||||
const { brain } = useBrainFetcher({
|
status,
|
||||||
|
isDefaultBrain,
|
||||||
|
} = useBrainFormState({
|
||||||
brainId,
|
brainId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDefaultBrain = defaultBrainId === brainId;
|
const brainStatusOptions: {
|
||||||
const promptId = watch("prompt_id");
|
label: string;
|
||||||
const openAiKey = watch("openAiKey");
|
value: BrainStatus;
|
||||||
const model = watch("model");
|
}[] = [
|
||||||
const temperature = watch("temperature");
|
{
|
||||||
const maxTokens = watch("maxTokens");
|
label: t("private_brain_label", { ns: "brain" }),
|
||||||
|
value: "private",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("public_brain_label", { ns: "brain" }),
|
||||||
|
value: "public",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const accessibleModels = getAccessibleModels({
|
const accessibleModels = getAccessibleModels({
|
||||||
openAiKey,
|
openAiKey,
|
||||||
@ -138,7 +128,7 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
|||||||
}, [formRef.current]);
|
}, [formRef.current]);
|
||||||
|
|
||||||
const fetchPrompt = async () => {
|
const fetchPrompt = async () => {
|
||||||
if (promptId === "") {
|
if (promptId === "" || promptId === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +201,7 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
|||||||
const promptHandler = async () => {
|
const promptHandler = async () => {
|
||||||
const { prompt } = getValues();
|
const { prompt } = getValues();
|
||||||
|
|
||||||
if (dirtyFields["prompt"]) {
|
if (dirtyFields["prompt"] && promptId !== undefined) {
|
||||||
await updatePrompt(promptId, {
|
await updatePrompt(promptId, {
|
||||||
title: prompt.title,
|
title: prompt.title,
|
||||||
content: prompt.content,
|
content: prompt.content,
|
||||||
@ -348,18 +338,22 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
|
|||||||
return {
|
return {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
register,
|
register,
|
||||||
|
removeBrainPrompt,
|
||||||
|
pickPublicPrompt,
|
||||||
|
setAsDefaultBrainHandler,
|
||||||
|
setValue,
|
||||||
brain,
|
brain,
|
||||||
model,
|
model,
|
||||||
temperature,
|
temperature,
|
||||||
maxTokens,
|
maxTokens,
|
||||||
isUpdating,
|
isUpdating,
|
||||||
setAsDefaultBrainHandler,
|
|
||||||
isSettingAsDefault,
|
isSettingAsDefault,
|
||||||
isDefaultBrain,
|
isDefaultBrain,
|
||||||
formRef,
|
formRef,
|
||||||
promptId,
|
promptId,
|
||||||
removeBrainPrompt,
|
|
||||||
pickPublicPrompt,
|
|
||||||
accessibleModels,
|
accessibleModels,
|
||||||
|
brainStatusOptions,
|
||||||
|
status,
|
||||||
|
dirtyFields,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
@ -16,6 +17,8 @@ import {
|
|||||||
|
|
||||||
import SelectedChatPage from "../page";
|
import SelectedChatPage from "../page";
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({
|
vi.mock("@/lib/context/ChatProvider/ChatProvider", () => ({
|
||||||
ChatContext: ChatContextMock,
|
ChatContext: ChatContextMock,
|
||||||
ChatProvider: ChatProviderMock,
|
ChatProvider: ChatProviderMock,
|
||||||
@ -55,13 +58,15 @@ vi.mock("@tanstack/react-query", async () => {
|
|||||||
describe("Chat page", () => {
|
describe("Chat page", () => {
|
||||||
it("should render chat page correctly", () => {
|
it("should render chat page correctly", () => {
|
||||||
const { getByTestId } = render(
|
const { getByTestId } = render(
|
||||||
<ChatProviderMock>
|
<QueryClientProvider client={queryClient}>
|
||||||
<SupabaseProviderMock>
|
<ChatProviderMock>
|
||||||
<BrainProviderMock>
|
<SupabaseProviderMock>
|
||||||
<SelectedChatPage />,
|
<BrainProviderMock>
|
||||||
</BrainProviderMock>
|
<SelectedChatPage />,
|
||||||
</SupabaseProviderMock>
|
</BrainProviderMock>
|
||||||
</ChatProviderMock>
|
</SupabaseProviderMock>
|
||||||
|
</ChatProviderMock>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getByTestId("chat-page")).toBeDefined();
|
expect(getByTestId("chat-page")).toBeDefined();
|
||||||
|
41
frontend/app/chat/[chatId]/hooks/useHandleStream.ts
Normal file
41
frontend/app/chat/[chatId]/hooks/useHandleStream.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { useChatContext } from "@/lib/context";
|
||||||
|
|
||||||
|
import { ChatMessage } from "../types";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useHandleStream = () => {
|
||||||
|
const { updateStreamingHistory } = useChatContext();
|
||||||
|
|
||||||
|
const handleStream = async (
|
||||||
|
reader: ReadableStreamDefaultReader<Uint8Array>
|
||||||
|
): Promise<void> => {
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
|
||||||
|
const handleStreamRecursively = async () => {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataStrings = decoder
|
||||||
|
.decode(value)
|
||||||
|
.trim()
|
||||||
|
.split("data: ")
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
dataStrings.forEach((data) => {
|
||||||
|
const parsedData = JSON.parse(data) as ChatMessage;
|
||||||
|
updateStreamingHistory(parsedData);
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleStreamRecursively();
|
||||||
|
};
|
||||||
|
|
||||||
|
await handleStreamRecursively();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleStream,
|
||||||
|
};
|
||||||
|
};
|
@ -1,11 +1,10 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
|
||||||
import { useChatContext } from "@/lib/context/ChatProvider/hooks/useChatContext";
|
|
||||||
import { useFetch, useToast } from "@/lib/hooks";
|
import { useFetch, useToast } from "@/lib/hooks";
|
||||||
|
|
||||||
import { ChatMessage, ChatQuestion } from "../types";
|
import { useHandleStream } from "./useHandleStream";
|
||||||
|
import { ChatQuestion } from "../types";
|
||||||
|
|
||||||
interface UseChatService {
|
interface UseChatService {
|
||||||
addStreamQuestion: (
|
addStreamQuestion: (
|
||||||
@ -16,43 +15,29 @@ interface UseChatService {
|
|||||||
|
|
||||||
export const useQuestion = (): UseChatService => {
|
export const useQuestion = (): UseChatService => {
|
||||||
const { fetchInstance } = useFetch();
|
const { fetchInstance } = useFetch();
|
||||||
const { updateStreamingHistory } = useChatContext();
|
|
||||||
const { currentBrain } = useBrainContext();
|
const { currentBrain } = useBrainContext();
|
||||||
|
|
||||||
const { t } = useTranslation(["chat"]);
|
const { t } = useTranslation(["chat"]);
|
||||||
const { publish } = useToast();
|
const { publish } = useToast();
|
||||||
|
const { handleStream } = useHandleStream();
|
||||||
|
|
||||||
const handleStream = async (
|
const handleFetchError = async (response: Response) => {
|
||||||
reader: ReadableStreamDefaultReader<Uint8Array>
|
if (response.status === 429) {
|
||||||
): Promise<void> => {
|
publish({
|
||||||
const decoder = new TextDecoder("utf-8");
|
variant: "danger",
|
||||||
|
text: t("tooManyRequests", { ns: "chat" }),
|
||||||
const handleStreamRecursively = async () => {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataStrings = decoder
|
|
||||||
.decode(value)
|
|
||||||
.trim()
|
|
||||||
.split("data: ")
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
dataStrings.forEach((data) => {
|
|
||||||
try {
|
|
||||||
const parsedData = JSON.parse(data) as ChatMessage;
|
|
||||||
updateStreamingHistory(parsedData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(t("errorParsingData", { ns: "chat" }), error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await handleStreamRecursively();
|
return;
|
||||||
};
|
}
|
||||||
|
|
||||||
await handleStreamRecursively();
|
const errorMessage = (await response.json()) as { detail: string };
|
||||||
|
publish({
|
||||||
|
variant: "danger",
|
||||||
|
text: errorMessage.detail,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addStreamQuestion = async (
|
const addStreamQuestion = async (
|
||||||
@ -64,29 +49,29 @@ export const useQuestion = (): UseChatService => {
|
|||||||
Accept: "text/event-stream",
|
Accept: "text/event-stream",
|
||||||
};
|
};
|
||||||
const body = JSON.stringify(chatQuestion);
|
const body = JSON.stringify(chatQuestion);
|
||||||
console.log("Calling API...");
|
|
||||||
try {
|
try {
|
||||||
const response = await fetchInstance.post(
|
const response = await fetchInstance.post(
|
||||||
`/chat/${chatId}/question/stream?brain_id=${currentBrain?.id ?? ""}`,
|
`/chat/${chatId}/question/stream?brain_id=${currentBrain?.id ?? ""}`,
|
||||||
body,
|
body,
|
||||||
headers
|
headers
|
||||||
);
|
);
|
||||||
|
if (!response.ok) {
|
||||||
|
void handleFetchError(response);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.body === null) {
|
if (response.body === null) {
|
||||||
throw new Error(t("resposeBodyNull", { ns: "chat" }));
|
throw new Error(t("resposeBodyNull", { ns: "chat" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(t("receivedResponse"), response);
|
|
||||||
await handleStream(response.body.getReader());
|
await handleStream(response.body.getReader());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (axios.isAxiosError(error) && error.response?.status === 429) {
|
publish({
|
||||||
publish({
|
variant: "danger",
|
||||||
variant: "danger",
|
text: String(error),
|
||||||
text: t("tooManyRequests", { ns: "chat" }),
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(t("errorCallingAPI", { ns: "chat" }), error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { PUBLIC_BRAINS_KEY } from "@/lib/api/brain/config";
|
||||||
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
|
||||||
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
|
||||||
import { USER_DATA_KEY } from "@/lib/api/user/config";
|
import { USER_DATA_KEY } from "@/lib/api/user/config";
|
||||||
@ -14,7 +15,7 @@ import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
|
|||||||
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
|
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
|
||||||
import { useToast } from "@/lib/hooks";
|
import { useToast } from "@/lib/hooks";
|
||||||
import { BrainStatus } from "@/lib/types/brainConfig";
|
import { BrainStatus } from "@/lib/types/brainConfig";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
export const useAddBrainModal = () => {
|
export const useAddBrainModal = () => {
|
||||||
@ -30,6 +31,7 @@ export const useAddBrainModal = () => {
|
|||||||
setIsPublicAccessConfirmationModalOpened,
|
setIsPublicAccessConfirmationModalOpened,
|
||||||
] = useState(false);
|
] = useState(false);
|
||||||
const { getUser } = useUserApi();
|
const { getUser } = useUserApi();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const brainStatusOptions: {
|
const brainStatusOptions: {
|
||||||
label: string;
|
label: string;
|
||||||
@ -61,7 +63,14 @@ export const useAddBrainModal = () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const { register, getValues, reset, watch, setValue } = useForm({
|
const {
|
||||||
|
register,
|
||||||
|
getValues,
|
||||||
|
reset,
|
||||||
|
watch,
|
||||||
|
setValue,
|
||||||
|
formState: { dirtyFields },
|
||||||
|
} = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,7 +86,7 @@ export const useAddBrainModal = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "public") {
|
if (status === "public" && dirtyFields.status) {
|
||||||
setIsPublicAccessConfirmationModalOpened(true);
|
setIsPublicAccessConfirmationModalOpened(true);
|
||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
@ -145,6 +154,9 @@ export const useAddBrainModal = () => {
|
|||||||
variant: "success",
|
variant: "success",
|
||||||
text: t("brainCreated", { ns: "brain" }),
|
text: t("brainCreated", { ns: "brain" }),
|
||||||
});
|
});
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [PUBLIC_BRAINS_KEY],
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (axios.isAxiosError(err) && err.response?.status === 429) {
|
if (axios.isAxiosError(err) && err.response?.status === 429) {
|
||||||
publish({
|
publish({
|
||||||
|
@ -12,4 +12,11 @@ export const defaultBrainConfig: BrainConfig = {
|
|||||||
supabaseUrl: undefined,
|
supabaseUrl: undefined,
|
||||||
prompt_id: undefined,
|
prompt_id: undefined,
|
||||||
status: "private",
|
status: "private",
|
||||||
|
prompt: {
|
||||||
|
title: "",
|
||||||
|
content: "",
|
||||||
|
},
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
setDefault: false,
|
||||||
};
|
};
|
||||||
|
@ -33,14 +33,26 @@ export const useBrainProvider = () => {
|
|||||||
);
|
);
|
||||||
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
|
const currentBrain = allBrains.find((brain) => brain.id === currentBrainId);
|
||||||
|
|
||||||
|
const fetchAllBrains = useCallback(async () => {
|
||||||
|
setIsFetchingBrains(true);
|
||||||
|
try {
|
||||||
|
const brains = await getBrains();
|
||||||
|
setAllBrains(brains);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
setIsFetchingBrains(false);
|
||||||
|
}
|
||||||
|
}, [getBrains]);
|
||||||
|
|
||||||
const createBrainHandler = useCallback(
|
const createBrainHandler = useCallback(
|
||||||
async (brain: CreateBrainInput): Promise<UUID | undefined> => {
|
async (brain: CreateBrainInput): Promise<UUID | undefined> => {
|
||||||
const createdBrain = await createBrain(brain);
|
const createdBrain = await createBrain(brain);
|
||||||
try {
|
try {
|
||||||
setAllBrains((prevBrains) => [...prevBrains, createdBrain]);
|
|
||||||
setCurrentBrainId(createdBrain.id);
|
setCurrentBrainId(createdBrain.id);
|
||||||
|
|
||||||
void track("BRAIN_CREATED");
|
void track("BRAIN_CREATED");
|
||||||
|
void fetchAllBrains();
|
||||||
|
|
||||||
return createdBrain.id;
|
return createdBrain.id;
|
||||||
} catch {
|
} catch {
|
||||||
@ -50,7 +62,7 @@ export const useBrainProvider = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[createBrain, publish, track]
|
[createBrain, fetchAllBrains, publish, track]
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteBrainHandler = useCallback(
|
const deleteBrainHandler = useCallback(
|
||||||
@ -68,18 +80,6 @@ export const useBrainProvider = () => {
|
|||||||
[deleteBrain, publish, track]
|
[deleteBrain, publish, track]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchAllBrains = useCallback(async () => {
|
|
||||||
setIsFetchingBrains(true);
|
|
||||||
try {
|
|
||||||
const brains = await getBrains();
|
|
||||||
setAllBrains(brains);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
} finally {
|
|
||||||
setIsFetchingBrains(false);
|
|
||||||
}
|
|
||||||
}, [getBrains]);
|
|
||||||
|
|
||||||
const fetchDefaultBrain = useCallback(async () => {
|
const fetchDefaultBrain = useCallback(async () => {
|
||||||
const userDefaultBrain = await getDefaultBrain();
|
const userDefaultBrain = await getDefaultBrain();
|
||||||
if (userDefaultBrain !== undefined) {
|
if (userDefaultBrain !== undefined) {
|
||||||
|
18
frontend/lib/hooks/useUserData.ts
Normal file
18
frontend/lib/hooks/useUserData.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { USER_DATA_KEY } from "../api/user/config";
|
||||||
|
import { useUserApi } from "../api/user/useUserApi";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
|
export const useUserData = () => {
|
||||||
|
const { getUser } = useUserApi();
|
||||||
|
|
||||||
|
const { data: userData } = useQuery({
|
||||||
|
queryKey: [USER_DATA_KEY],
|
||||||
|
queryFn: getUser,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
userData,
|
||||||
|
};
|
||||||
|
};
|
@ -1,5 +1,3 @@
|
|||||||
import { UUID } from "crypto";
|
|
||||||
|
|
||||||
export const brainStatuses = ["private", "public"] as const;
|
export const brainStatuses = ["private", "public"] as const;
|
||||||
|
|
||||||
export type BrainStatus = (typeof brainStatuses)[number];
|
export type BrainStatus = (typeof brainStatuses)[number];
|
||||||
@ -14,8 +12,15 @@ export type BrainConfig = {
|
|||||||
anthropicKey?: string;
|
anthropicKey?: string;
|
||||||
supabaseUrl?: string;
|
supabaseUrl?: string;
|
||||||
supabaseKey?: string;
|
supabaseKey?: string;
|
||||||
prompt_id?: UUID;
|
prompt_id?: string;
|
||||||
status: BrainStatus;
|
status: BrainStatus;
|
||||||
|
prompt: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
setDefault: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BrainConfigContextType = {
|
export type BrainConfigContextType = {
|
||||||
|
@ -39,5 +39,10 @@
|
|||||||
"public_brain_subscribe_button_label":"Subscribe",
|
"public_brain_subscribe_button_label":"Subscribe",
|
||||||
"public_brain_subscription_success_message":"You have successfully subscribed to the brain",
|
"public_brain_subscription_success_message":"You have successfully subscribed to the brain",
|
||||||
"public_brain_last_update_label":"Last update",
|
"public_brain_last_update_label":"Last update",
|
||||||
"public_brain_already_subscribed_button_label":"Subscribed"
|
"public_brain_already_subscribed_button_label":"Subscribed",
|
||||||
|
"set_brain_status_to_private_modal_title":"Are you sure you want to set this as <span class='text-purple-800'>Private</span>?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description":"Every Quivr users won't be able to use this brain anymore and they won't see it in the brain library.",
|
||||||
|
"confirm_set_brain_status_to_private":"Yes, set as private",
|
||||||
|
"cancel_set_brain_status_to_private":"No, keep it public"
|
||||||
|
|
||||||
}
|
}
|
@ -39,5 +39,9 @@
|
|||||||
"public_brain_subscribe_button_label": "Suscribirse",
|
"public_brain_subscribe_button_label": "Suscribirse",
|
||||||
"public_brain_subscription_success_message": "Te has suscrito con éxito al cerebro",
|
"public_brain_subscription_success_message": "Te has suscrito con éxito al cerebro",
|
||||||
"public_brain_last_update_label": "Última actualización",
|
"public_brain_last_update_label": "Última actualización",
|
||||||
"public_brain_already_subscribed_button_label": "Ya suscrito"
|
"public_brain_already_subscribed_button_label": "Ya suscrito",
|
||||||
|
"set_brain_status_to_private_modal_title": "¿Estás seguro de que quieres establecer esto como <span class='text-purple-800'>Privado</span>?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description": "Los usuarios de Quivr ya no podrán utilizar este cerebro y no lo verán en la biblioteca de cerebros.",
|
||||||
|
"confirm_set_brain_status_to_private": "Sí, establecer como privado",
|
||||||
|
"cancel_set_brain_status_to_private": "No, mantenerlo público"
|
||||||
}
|
}
|
@ -39,5 +39,9 @@
|
|||||||
"public_brain_subscribe_button_label": "S'abonner",
|
"public_brain_subscribe_button_label": "S'abonner",
|
||||||
"public_brain_subscription_success_message": "Vous vous êtes abonné avec succès au cerveau",
|
"public_brain_subscription_success_message": "Vous vous êtes abonné avec succès au cerveau",
|
||||||
"public_brain_last_update_label": "Dernière mise à jour",
|
"public_brain_last_update_label": "Dernière mise à jour",
|
||||||
"public_brain_already_subscribed_button_label": "Abonné"
|
"public_brain_already_subscribed_button_label": "Abonné",
|
||||||
|
"set_brain_status_to_private_modal_title": "Êtes-vous sûr de vouloir définir ceci comme <span class='text-purple-800'>Privé</span>?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description": "Les utilisateurs de Quivr ne pourront plus utiliser ce cerveau et ne le verront plus dans la bibliothèque des cerveaux.",
|
||||||
|
"confirm_set_brain_status_to_private": "Oui, définir comme privé",
|
||||||
|
"cancel_set_brain_status_to_private": "Non, le laisser public"
|
||||||
}
|
}
|
@ -39,5 +39,9 @@
|
|||||||
"public_brain_subscribe_button_label": "Inscrever-se",
|
"public_brain_subscribe_button_label": "Inscrever-se",
|
||||||
"public_brain_subscription_success_message": "Você se inscreveu com sucesso no cérebro",
|
"public_brain_subscription_success_message": "Você se inscreveu com sucesso no cérebro",
|
||||||
"public_brain_last_update_label": "Última atualização",
|
"public_brain_last_update_label": "Última atualização",
|
||||||
"public_brain_already_subscribed_button_label": "Inscrito"
|
"public_brain_already_subscribed_button_label": "Inscrito",
|
||||||
|
"set_brain_status_to_private_modal_title": "Tem a certeza de que deseja definir isto como <span class='text-purple-800'>Privado</span>?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description": "Os utilizadores do Quivr não poderão mais utilizar este cérebro e não o verão na biblioteca de cérebros.",
|
||||||
|
"confirm_set_brain_status_to_private": "Sim, definir como privado",
|
||||||
|
"cancel_set_brain_status_to_private": "Não, mantê-lo público"
|
||||||
}
|
}
|
@ -39,5 +39,9 @@
|
|||||||
"public_brain_subscribe_button_label": "Подписаться",
|
"public_brain_subscribe_button_label": "Подписаться",
|
||||||
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
||||||
"public_brain_last_update_label": "Последнее обновление",
|
"public_brain_last_update_label": "Последнее обновление",
|
||||||
"public_brain_already_subscribed_button_label": "Вы уже подписаны"
|
"public_brain_already_subscribed_button_label": "Вы уже подписаны",
|
||||||
|
"set_brain_status_to_private_modal_title": "Вы уверены, что хотите установить это как <span class='text-purple-800'>Частное</span>?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description": "Пользователи Quivr больше не смогут использовать этот мозг, и они не увидят его в библиотеке мозгов.",
|
||||||
|
"confirm_set_brain_status_to_private": "Да, установить как частное",
|
||||||
|
"cancel_set_brain_status_to_private": "Нет, оставить общедоступным"
|
||||||
}
|
}
|
@ -39,5 +39,9 @@
|
|||||||
"public_brain_subscribe_button_label": "订阅",
|
"public_brain_subscribe_button_label": "订阅",
|
||||||
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
"public_brain_subscription_success_message": "Вы успешно подписались на мозг",
|
||||||
"public_brain_last_update_label": "Последнее обновление",
|
"public_brain_last_update_label": "Последнее обновление",
|
||||||
"public_brain_already_subscribed_button_label": "已订阅"
|
"public_brain_already_subscribed_button_label": "已订阅",
|
||||||
|
"set_brain_status_to_private_modal_title": "您确定要将此设置为<span class='text-purple-800'>私有</span>吗?<br/><br/>",
|
||||||
|
"set_brain_status_to_private_modal_description": "Quivr的用户将无法再使用此大脑,并且不会在大脑库中看到它。",
|
||||||
|
"confirm_set_brain_status_to_private": "是的,设为私有",
|
||||||
|
"cancel_set_brain_status_to_private": "不,保持为公开"
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user