feat: allow user to unsubscribe from a brain (#1254)

* feat: update translations

* feat: add <DeleteOrUnsubscribeConfirmationModal />

* test(DeleteOrUnsubscribeConfirmationModal): update tests

* feat: redirect to /brains-management on invalid brain id

* refactor: move delete_brain_user to delete_brain_users

* feat: add /POST '/brains/{brain_id}/subscribe'

* feat: handle public brain unsubscription
This commit is contained in:
Mamadou DICKO 2023-09-25 14:22:59 +02:00 committed by GitHub
parent 3043f3acd0
commit 1643b54b7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 356 additions and 164 deletions

View File

@ -2,12 +2,13 @@ from typing import Any, List, Optional
from uuid import UUID
from logger import get_logger
from models.databases.supabase.supabase import SupabaseDB
from models.settings import get_supabase_client, get_supabase_db
from pydantic import BaseModel
from supabase.client import Client
from utils.vectors import get_unique_files_from_vector_ids
from models.databases.supabase.supabase import SupabaseDB
from models.settings import get_supabase_client, get_supabase_db
logger = get_logger(__name__)
@ -75,11 +76,11 @@ class Brain(BaseModel):
def delete_brain(self, user_id):
results = self.supabase_db.delete_brain_user_by_id(user_id, self.id) # type: ignore
if len(results.data) == 0:
if len(results) == 0:
return {"message": "You are not the owner of this brain."}
else:
self.supabase_db.delete_brain_vector(self.id) # type: ignore
self.supabase_db.delete_brain_user(self.id) # type: ignore
self.supabase_db.delete_brain_users(self.id) # type: ignore
self.supabase_db.delete_brain(self.id) # type: ignore
def create_brain_vector(self, vector_id, file_sha1):

View File

@ -23,7 +23,7 @@ class Repository(ABC):
pass
@abstractmethod
def delete_brain_user(self, brain_id: str):
def delete_brain_users(self, brain_id: str):
pass
@abstractmethod

View File

@ -127,14 +127,18 @@ class Brain(Repository):
)
return response.data
def delete_brain_user_by_id(self, user_id, brain_id):
def delete_brain_user_by_id(
self,
user_id: UUID,
brain_id: UUID,
):
results = (
self.db.table("brains_users")
.select("*")
.match({"brain_id": brain_id, "user_id": user_id, "rights": "Owner"})
.delete()
.match({"brain_id": str(brain_id), "user_id": str(user_id)})
.execute()
)
return results
return results.data
def delete_brain_vector(self, brain_id: str):
results = (
@ -146,7 +150,7 @@ class Brain(Repository):
return results
def delete_brain_user(self, brain_id: str):
def delete_brain_users(self, brain_id: str):
results = (
self.db.table("brains_users")
.delete()

View File

@ -0,0 +1,11 @@
from uuid import UUID
from models.settings import get_supabase_db
def delete_brain_user(user_id: UUID, brain_id: UUID) -> None:
supabase_db = get_supabase_db()
supabase_db.delete_brain_user_by_id(
user_id=user_id,
brain_id=brain_id,
)

View File

@ -12,6 +12,7 @@ from repository.brain import (
get_brain_for_user,
update_brain_user_rights,
)
from repository.brain.delete_brain_user import delete_brain_user
from repository.brain_subscription import (
SubscriptionInvitationService,
resend_invitation_email,
@ -403,3 +404,37 @@ async def subscribe_to_brain_handler(
raise HTTPException(status_code=400, detail=f"Error adding user to brain: {e}")
return {"message": "You have successfully subscribed to the brain"}
@subscription_router.post(
"/brains/{brain_id}/unsubscribe",
tags=["Subscription"],
)
async def unsubscribe_from_brain_handler(
brain_id: UUID, current_user: UserIdentity = Depends(get_current_user)
):
"""
Unsubscribe from a brain
"""
if not current_user.email:
raise HTTPException(status_code=400, detail="UserIdentity email is not defined")
brain = get_brain_by_id(brain_id)
if brain is None:
raise HTTPException(status_code=404, detail="Brain not found")
if brain.status != "public":
raise HTTPException(
status_code=403,
detail="You cannot subscribe to this brain without invitation",
)
# check if user is already subscribed to brain
user_brain = get_brain_for_user(current_user.id, brain_id)
if user_brain is None:
raise HTTPException(
status_code=403,
detail="You are not subscribed to this brain",
)
delete_brain_user(user_id=current_user.id, brain_id=brain_id)
return {"message": "You have successfully unsubscribed from the brain"}

View File

@ -3,35 +3,34 @@ import { Content, List, Root } from "@radix-ui/react-tabs";
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { BrainTabTrigger, KnowledgeTab, PeopleTab } from "./components";
import ConfirmationDeleteModal from "./components/Modals/ConfirmationDeleteModal";
import { DeleteOrUnsubscribeConfirmationModal } from "./components/Modals/DeleteOrUnsubscribeConfirmationModal";
import { SettingsTab } from "./components/SettingsTab/SettingsTab";
import { useBrainManagementTabs } from "./hooks/useBrainManagementTabs";
import { getBrainPermissions } from "./utils/getBrainPermissions";
export const BrainManagementTabs = (): JSX.Element => {
const { t } = useTranslation(["translation", "config", "delete_brain"]);
const { t } = useTranslation([
"translation",
"config",
"delete_or_unsubscribe_from_brain",
]);
const {
selectedTab,
setSelectedTab,
brainId,
handleDeleteBrain,
isDeleteModalOpen,
setIsDeleteModalOpen,
handleUnsubscribeOrDeleteBrain,
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
hasEditRights,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
} = useBrainManagementTabs();
const { allBrains } = useBrainContext();
if (brainId === undefined) {
return <div />;
}
const { hasEditRights, isOwnedByCurrentUser } = getBrainPermissions({
brainId,
userAccessibleBrains: allBrains,
});
return (
<Root
className="flex flex-col w-full h-full shadow-md dark:shadow-primary/25 hover:shadow-xl transition-shadow rounded-xl overflow-hidden bg-white dark:bg-black border border-black/10 dark:border-white/25 p-4 md:p-10"
@ -78,19 +77,33 @@ export const BrainManagementTabs = (): JSX.Element => {
</div>
<div className="flex justify-center mt-4">
<Button
disabled={!isOwnedByCurrentUser}
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteModalOpen(true)}
>
{t("deleteButton", { ns: "delete_brain" })}
</Button>
{isOwnedByCurrentUser ? (
<Button
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
>
{t("deleteButton", { ns: "delete_or_unsubscribe_from_brain" })}
</Button>
) : (
<Button
className="px-8 md:px-20 py-2 bg-red-500 text-white rounded-md"
onClick={() => setIsDeleteOrUnsubscribeModalOpened(true)}
>
{t("unsubscribeButton", {
ns: "delete_or_unsubscribe_from_brain",
})}
</Button>
)}
</div>
<ConfirmationDeleteModal
isOpen={isDeleteModalOpen}
setOpen={setIsDeleteModalOpen}
onDelete={handleDeleteBrain}
<DeleteOrUnsubscribeConfirmationModal
isOpen={isDeleteOrUnsubscribeModalOpened}
setOpen={setIsDeleteOrUnsubscribeModalOpened}
onConfirm={() => void handleUnsubscribeOrDeleteBrain()}
isOwnedByCurrentUser={isOwnedByCurrentUser}
isDeleteOrUnsubscribeRequestPending={
isDeleteOrUnsubscribeRequestPending
}
/>
</Root>
);

View File

@ -1,46 +0,0 @@
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
type ConfirmationDeleteModalProps = {
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
onDelete: () => void;
};
const ConfirmationDeleteModal = ({
isOpen,
setOpen,
onDelete,
}: ConfirmationDeleteModalProps): JSX.Element => {
const { t } = useTranslation(["delete_brain"]);
return (
<Modal
desc={t("deleteConfirmQuestion")}
isOpen={isOpen}
setOpen={setOpen}
Trigger={<div />}
CloseTrigger={
<Button className="self-end" data-testid="return-button">
{t("returnButton")}
</Button>
}
>
<div>
<div className="flex justify-center mt-6">
<Button
data-testid="delete-brain"
className="px-4 py-2 bg-red-500 text-white rounded-md"
onClick={onDelete}
>
{t("deleteConfirmYes")}
</Button>
</div>
</div>
</Modal>
);
};
export default ConfirmationDeleteModal;

View File

@ -0,0 +1,55 @@
import { useTranslation } from "react-i18next";
import Button from "@/lib/components/ui/Button";
import { Modal } from "@/lib/components/ui/Modal";
type DeleteOrUnsubscribeConfirmationModalProps = {
isOpen: boolean;
setOpen: (isOpen: boolean) => void;
onConfirm: () => void;
isOwnedByCurrentUser: boolean;
isDeleteOrUnsubscribeRequestPending: boolean;
};
export const DeleteOrUnsubscribeConfirmationModal = ({
isOpen,
setOpen,
onConfirm,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
}: DeleteOrUnsubscribeConfirmationModalProps): JSX.Element => {
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
return (
<Modal
desc={
isOwnedByCurrentUser
? t("deleteConfirmQuestion")
: t("unsubscribeConfirmQuestion")
}
isOpen={isOpen}
setOpen={setOpen}
Trigger={<div />}
CloseTrigger={
<Button className="self-end" data-testid="return-button">
{t("returnButton")}
</Button>
}
>
<div>
<div className="flex justify-center mt-6">
<Button
data-testid="delete-brain"
className="px-4 py-2 bg-red-500 text-white rounded-md"
onClick={onConfirm}
isLoading={isDeleteOrUnsubscribeRequestPending}
>
{isOwnedByCurrentUser
? t("deleteConfirmYes")
: t("unsubscribeButton")}
</Button>
</div>
</div>
</Modal>
);
};

View File

@ -1,9 +1,9 @@
import { render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import ConfirmationDeleteModal from "../ConfirmationDeleteModal";
import { DeleteOrUnsubscribeConfirmationModal } from "../DeleteOrUnsubscribeConfirmationModal";
describe("ConfirmationDeleteModal", () => {
describe("DeleteOrUnsubscribeConfirmationModal", () => {
const isOpen = true;
const setOpen = vi.fn();
const onDelete = vi.fn();
@ -14,10 +14,10 @@ describe("ConfirmationDeleteModal", () => {
it("should render delete modal", () => {
const { getByTestId } = render(
<ConfirmationDeleteModal
<DeleteOrUnsubscribeConfirmationModal
isOpen={isOpen}
setOpen={setOpen}
onDelete={onDelete}
onConfirm={onDelete}
/>
);
expect(getByTestId("modal-description")).toBeDefined();
@ -27,10 +27,10 @@ describe("ConfirmationDeleteModal", () => {
it("should call onDelete when delete-brain is clicked", () => {
const { getByTestId } = render(
<ConfirmationDeleteModal
<DeleteOrUnsubscribeConfirmationModal
isOpen={isOpen}
setOpen={setOpen}
onDelete={onDelete}
onConfirm={onDelete}
/>
);

View File

@ -7,7 +7,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { getBrainDataKey } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { usePromptApi } from "@/lib/api/prompt/usePromptApi";
import { USER_DATA_KEY } from "@/lib/api/user/config";
@ -19,6 +18,7 @@ import { defineMaxTokens } from "@/lib/helpers/defineMaxTokens";
import { getAccessibleModels } from "@/lib/helpers/getAccessibleModels";
import { useToast } from "@/lib/hooks";
import { useBrainFetcher } from "../../../hooks/useBrainFetcher";
import { validateOpenAIKey } from "../utils/validateOpenAIKey";
type UseSettingsTabProps = {
@ -32,7 +32,7 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
const [isSettingAsDefault, setIsSettingAsDefault] = useState(false);
const { publish } = useToast();
const formRef = useRef<HTMLFormElement>(null);
const { setAsDefaultBrain, getBrain, updateBrain } = useBrainApi();
const { setAsDefaultBrain, updateBrain } = useBrainApi();
const { fetchAllBrains, fetchDefaultBrain, defaultBrainId } =
useBrainContext();
const { getPrompt, updatePrompt, createPrompt } = usePromptApi();
@ -65,10 +65,8 @@ export const useSettingsTab = ({ brainId }: UseSettingsTabProps) => {
} = useForm({
defaultValues,
});
const { data: brain } = useQuery({
queryKey: [getBrainDataKey(brainId)],
queryFn: () => getBrain(brainId),
const { brain } = useBrainFetcher({
brainId,
});
const isDefaultBrain = defaultBrainId === brainId;

View File

@ -0,0 +1,38 @@
import { useQuery } from "@tanstack/react-query";
import { UUID } from "crypto";
import { useRouter } from "next/navigation";
import { getBrainDataKey } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
type UseBrainFetcherProps = {
brainId?: UUID;
};
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainFetcher = ({ brainId }: UseBrainFetcherProps) => {
const { getBrain } = useBrainApi();
const router = useRouter();
const fetchBrain = async () => {
try {
if (brainId === undefined) {
return undefined;
}
return await getBrain(brainId);
} catch (error) {
router.push("/brains-management");
}
};
const { data: brain } = useQuery({
queryKey: [getBrainDataKey(brainId!)],
queryFn: fetchBrain,
enabled: brainId !== undefined,
});
return {
brain,
};
};

View File

@ -1,19 +1,26 @@
import { useQuery } from "@tanstack/react-query";
import { UUID } from "crypto";
import { useParams, useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getBrainDataKey } from "@/lib/api/brain/config";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
import { useSubscriptionApi } from "@/lib/api/subscription/useSubscriptionApi";
import { useBrainContext } from "@/lib/context/BrainProvider/hooks/useBrainContext";
import { useToast } from "@/lib/hooks";
import { useEventTracking } from "@/services/analytics/june/useEventTracking";
import { BrainManagementTab } from "../types";
import { getBrainPermissions } from "../utils/getBrainPermissions";
import { getTargetedTab } from "../utils/getTargetedTab";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export const useBrainManagementTabs = () => {
const [selectedTab, setSelectedTab] =
useState<BrainManagementTab>("settings");
const { allBrains } = useBrainContext();
const [
isDeleteOrUnsubscribeRequestPending,
setIsDeleteOrUnsubscribeRequestPending,
] = useState(false);
useEffect(() => {
const targetedTab = getTargetedTab();
@ -21,39 +28,70 @@ export const useBrainManagementTabs = () => {
setSelectedTab(targetedTab);
}
}, []);
const { getBrain } = useBrainApi();
const { deleteBrain, setCurrentBrainId } = useBrainContext();
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
const { track } = useEventTracking();
const { publish } = useToast();
const { unsubscribeFromBrain } = useSubscriptionApi();
const { deleteBrain, setCurrentBrainId, fetchAllBrains } = useBrainContext();
const [
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
] = useState(false);
const router = useRouter();
const params = useParams();
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
const brainId = params?.brainId as UUID | undefined;
const { data: brain } = useQuery({
queryKey: [getBrainDataKey(brainId!)],
queryFn: () => getBrain(brainId!),
enabled: brainId !== undefined,
const { hasEditRights, isOwnedByCurrentUser } = getBrainPermissions({
brainId,
userAccessibleBrains: allBrains,
});
const handleDeleteBrain = () => {
const handleUnSubscription = async () => {
if (brainId === undefined) {
return;
}
void deleteBrain(brainId);
setCurrentBrainId(null);
router.push("/brains-management");
setIsDeleteModalOpen(false);
await unsubscribeFromBrain(brainId);
void track("UNSUBSCRIBE_FROM_BRAIN");
publish({
variant: "success",
text: t("successfully_unsubscribed"),
});
};
const handleUnsubscribeOrDeleteBrain = async () => {
if (brainId === undefined) {
return;
}
setIsDeleteOrUnsubscribeRequestPending(true);
try {
if (!isOwnedByCurrentUser) {
await handleUnSubscription();
} else {
await deleteBrain(brainId);
}
setCurrentBrainId(null);
setIsDeleteOrUnsubscribeModalOpened(false);
void fetchAllBrains();
router.push("/brains-management");
} catch (error) {
console.error("Error deleting brain: ", error);
} finally {
setIsDeleteOrUnsubscribeRequestPending(false);
}
};
return {
selectedTab,
setSelectedTab,
brainId,
handleDeleteBrain,
isDeleteModalOpen,
setIsDeleteModalOpen,
brain,
handleUnsubscribeOrDeleteBrain,
isDeleteOrUnsubscribeModalOpened,
setIsDeleteOrUnsubscribeModalOpened,
hasEditRights,
isOwnedByCurrentUser,
isDeleteOrUnsubscribeRequestPending,
};
};

View File

@ -16,7 +16,7 @@ const BrainsManagement = (): JSX.Element => {
return (
<div className="flex justify-center mt-5 w-full">
<div className="bg-blue-100 border border-blue-400 text-blue-700 px-4 py-3 rounded relative max-w-md h-fit">
<p>{ t("selectBrain") }</p>
<p>{t("selectBrain")}</p>
</div>
</div>
);

View File

@ -67,3 +67,13 @@ export const subscribeToBrain = async (
return subscribedToBrain;
};
export const unsubscribeFromBrain = async (
brainId: UUID,
axiosInstance: AxiosInstance
): Promise<{ message: string }> =>
(
await axiosInstance.post<{ message: string }>(
`/brains/${brainId}/unsubscribe`
)
).data;

View File

@ -7,6 +7,7 @@ import {
declineInvitation,
getInvitation,
subscribeToBrain,
unsubscribeFromBrain,
} from "./subscription";
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -22,5 +23,7 @@ export const useSubscriptionApi = () => {
getInvitation(brainId, axiosInstance),
subscribeToBrain: async (brainId: UUID) =>
subscribeToBrain(brainId, axiosInstance),
unsubscribeFromBrain: async (brainId: UUID) =>
unsubscribeFromBrain(brainId, axiosInstance),
};
};

View File

@ -3,7 +3,7 @@
import brain_en from "../../../public/locales/en/brain.json";
import chat_en from "../../../public/locales/en/chat.json";
import config_en from "../../../public/locales/en/config.json";
import delete_brain_en from "../../../public/locales/en/deleteBrain.json";
import delete_brain_en from "../../../public/locales/en/deleteOrUnsubscribeFromBrain.json";
import explore_en from "../../../public/locales/en/explore.json";
import invitation_en from "../../../public/locales/en/invitation.json";
import login_en from "../../../public/locales/en/login.json";
@ -17,7 +17,7 @@ import user_en from "../../../public/locales/en/user.json";
import brain_es from "../../../public/locales/es/brain.json";
import chat_es from "../../../public/locales/es/chat.json";
import config_es from "../../../public/locales/es/config.json";
import delete_brain_es from "../../../public/locales/es/deleteBrain.json";
import delete_brain_es from "../../../public/locales/es/deleteOrUnsubscribeFromBrain.json";
import explore_es from "../../../public/locales/es/explore.json";
import invitation_es from "../../../public/locales/es/invitation.json";
import login_es from "../../../public/locales/es/login.json";
@ -31,7 +31,7 @@ import user_es from "../../../public/locales/es/user.json";
import brain_fr from "../../../public/locales/fr/brain.json";
import chat_fr from "../../../public/locales/fr/chat.json";
import config_fr from "../../../public/locales/fr/config.json";
import delete_brain_fr from "../../../public/locales/fr/deleteBrain.json";
import delete_brain_fr from "../../../public/locales/fr/deleteOrUnsubscribeFromBrain.json";
import explore_fr from "../../../public/locales/fr/explore.json";
import invitation_fr from "../../../public/locales/fr/invitation.json";
import login_fr from "../../../public/locales/fr/login.json";
@ -45,7 +45,7 @@ import user_fr from "../../../public/locales/fr/user.json";
import brain_ptbr from "../../../public/locales/pt-br/brain.json";
import chat_ptbr from "../../../public/locales/pt-br/chat.json";
import config_ptbr from "../../../public/locales/pt-br/config.json";
import delete_brain_ptbr from "../../../public/locales/pt-br/deleteBrain.json";
import delete_brain_ptbr from "../../../public/locales/pt-br/deleteOrUnsubscribeFromBrain.json";
import explore_ptbr from "../../../public/locales/pt-br/explore.json";
import invitation_ptbr from "../../../public/locales/pt-br/invitation.json";
import login_ptbr from "../../../public/locales/pt-br/login.json";
@ -59,7 +59,7 @@ import user_ptbr from "../../../public/locales/pt-br/user.json";
import brain_ru from "../../../public/locales/ru/brain.json";
import chat_ru from "../../../public/locales/ru/chat.json";
import config_ru from "../../../public/locales/ru/config.json";
import delete_brain_ru from "../../../public/locales/ru/deleteBrain.json";
import delete_brain_ru from "../../../public/locales/ru/deleteOrUnsubscribeFromBrain.json";
import explore_ru from "../../../public/locales/ru/explore.json";
import invitation_ru from "../../../public/locales/ru/invitation.json";
import login_ru from "../../../public/locales/ru/login.json";
@ -73,7 +73,7 @@ import user_ru from "../../../public/locales/ru/user.json";
import brain_zh_cn from "../../../public/locales/zh-cn/brain.json";
import chat_zh_cn from "../../../public/locales/zh-cn/chat.json";
import config_zh_cn from "../../../public/locales/zh-cn/config.json";
import delete_brain_zh_cn from "../../../public/locales/zh-cn/deleteBrain.json";
import delete_brain_zh_cn from "../../../public/locales/zh-cn/deleteOrUnsubscribeFromBrain.json";
import explore_zh_cn from "../../../public/locales/zh-cn/explore.json";
import invitation_zh_cn from "../../../public/locales/zh-cn/invitation.json";
import login_zh_cn from "../../../public/locales/zh-cn/login.json";
@ -89,7 +89,7 @@ export type Translations = {
brain: typeof import("../../../public/locales/en/brain.json");
chat: typeof import("../../../public/locales/en/chat.json");
config: typeof import("../../../public/locales/en/config.json");
delete_brain: typeof import("../../../public/locales/en/deleteBrain.json");
delete_or_unsubscribe_from_brain: typeof import("../../../public/locales/en/deleteOrUnsubscribeFromBrain.json");
explore: typeof import("../../../public/locales/en/explore.json");
invitation: typeof import("../../../public/locales/en/invitation.json");
login: typeof import("../../../public/locales/en/login.json");
@ -125,7 +125,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_en,
upload: upload_en,
user: user_en,
delete_brain: delete_brain_en,
delete_or_unsubscribe_from_brain: delete_brain_en,
},
es: {
brain: brain_es,
@ -140,7 +140,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_es,
upload: upload_es,
user: user_es,
delete_brain: delete_brain_es,
delete_or_unsubscribe_from_brain: delete_brain_es,
},
fr: {
brain: brain_fr,
@ -155,7 +155,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_fr,
upload: upload_fr,
user: user_fr,
delete_brain: delete_brain_fr,
delete_or_unsubscribe_from_brain: delete_brain_fr,
},
ptbr: {
brain: brain_ptbr,
@ -170,7 +170,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_ptbr,
upload: upload_ptbr,
user: user_ptbr,
delete_brain: delete_brain_ptbr,
delete_or_unsubscribe_from_brain: delete_brain_ptbr,
},
ru: {
brain: brain_ru,
@ -185,7 +185,7 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_ru,
upload: upload_ru,
user: user_ru,
delete_brain: delete_brain_ru,
delete_or_unsubscribe_from_brain: delete_brain_ru,
},
zh_cn: {
brain: brain_zh_cn,
@ -200,6 +200,6 @@ export const resources: Record<SupportedLanguages, Translations> = {
updatePassword: updatePassword_zh_cn,
upload: upload_zh_cn,
user: user_zh_cn,
delete_brain: delete_brain_zh_cn,
delete_or_unsubscribe_from_brain: delete_brain_zh_cn,
},
} as const;

View File

@ -1,6 +1,7 @@
/* eslint-disable max-lines */
import { UUID } from "crypto";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { CreateBrainInput } from "@/lib/api/brain/types";
import { useBrainApi } from "@/lib/api/brain/useBrainApi";
@ -18,6 +19,7 @@ export const useBrainProvider = () => {
const { createBrain, deleteBrain, getBrains, getDefaultBrain } =
useBrainApi();
const { getPublicPrompts } = usePromptApi();
const { t } = useTranslation(["delete_or_unsubscribe_from_brain"]);
const [allBrains, setAllBrains] = useState<MinimalBrainForUser[]>([]);
const [currentBrainId, setCurrentBrainId] = useState<null | UUID>(null);
@ -60,7 +62,7 @@ export const useBrainProvider = () => {
void track("DELETE_BRAIN");
publish({
variant: "success",
text: "Brain deleted",
text: t("successfully_deleted"),
});
},
[deleteBrain, publish, track]

View File

@ -1,6 +0,0 @@
{
"deleteButton": "Delete Brain",
"deleteConfirmQuestion": "Are you sure you want to delete this brain? This action cannot be undone.",
"deleteConfirmYes": "Yes, delete this brain",
"returnButton": "Return"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "Delete Brain",
"deleteConfirmQuestion": "Are you sure you want to delete this brain? This action cannot be undone.",
"deleteConfirmYes": "Yes, delete this brain",
"returnButton": "Return",
"unsubscribeButton": "Unsubscribe from brain",
"unsubscribeConfirmQuestion": "Are you sure you want to unsubscribe from this brain?",
"unsubscribeConfirmYes": "Yes, unsubscribe from this brain",
"successfully_deleted": "Brain successfully deleted.",
"successfully_unsubscribed": "You have successfully unsubscribed from this brain."
}

View File

@ -1,6 +0,0 @@
{
"deleteButton": "Borrar el cerebro",
"deleteConfirmQuestion": "¿Estás seguro de que quieres borrar el cerebro? No podrás recuperarlo.",
"deleteConfirmYes": "Sí, borrar el cerebro",
"returnButton": "Vuelta"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "Borrar el cerebro",
"deleteConfirmQuestion": "¿Estás seguro de que quieres borrar el cerebro? No podrás recuperarlo.",
"deleteConfirmYes": "Sí, borrar el cerebro",
"returnButton": "Vuelta",
"unsubscribeButton": "Cancelar suscripción del cerebro",
"unsubscribeConfirmQuestion": "¿Estás seguro de que quieres darte de baja de este cerebro?",
"unsubscribeConfirmYes": "Sí, cancelar la suscripción de este cerebro",
"successfully_deleted": "Cerebro eliminado con éxito.",
"successfully_unsubscribed": "Te has desinscrito con éxito de este cerebro."
}

View File

@ -1,6 +0,0 @@
{
"deleteButton": "Supprimer le cerveau",
"deleteConfirmQuestion": "Êtes-vous sûr de vouloir supprimer le cerveau? Cette action est irréversible!",
"deleteConfirmYes": "Oui, supprimer",
"returnButton": "Retour"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "Supprimer le cerveau",
"deleteConfirmQuestion": "Êtes-vous sûr de vouloir supprimer le cerveau? Cette action est irréversible!",
"deleteConfirmYes": "Oui, supprimer",
"returnButton": "Retour",
"unsubscribeButton": "Se désabonner du cerveau",
"unsubscribeConfirmQuestion": "Êtes-vous sûr de vouloir vous désabonner de ce cerveau ?",
"unsubscribeConfirmYes": "Oui, se désabonner de ce cerveau",
"successfully_deleted": "Cerveau supprimé avec succès.",
"successfully_unsubscribed": "Vous vous êtes désabonné avec succès de ce cerveau."
}

View File

@ -1,6 +0,0 @@
{
"deleteButton": "Excluir Cérebro",
"deleteConfirmQuestion": "Você tem certeza de que deseja excluir este cérebro? Essa ação não pode ser desfeita.",
"deleteConfirmYes": "Sim, excluir este cérebro",
"returnButton": "Retornar"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "Excluir Cérebro",
"deleteConfirmQuestion": "Você tem certeza de que deseja excluir este cérebro? Essa ação não pode ser desfeita.",
"deleteConfirmYes": "Sim, excluir este cérebro",
"returnButton": "Retornar",
"unsubscribeButton": "Cancelar inscrição do cérebro",
"unsubscribeConfirmQuestion": "Você tem certeza de que deseja cancelar a inscrição deste cérebro?",
"unsubscribeConfirmYes": "Sim, cancelar a inscrição deste cérebro",
"successfully_deleted": "Cérebro excluído com sucesso.",
"successfully_unsubscribed": "Você se desinscreveu com sucesso deste cérebro."
}

View File

@ -1,6 +0,0 @@
{
"deleteButton": "Удалить мозг",
"deleteConfirmQuestion": "Вы уверены, что хотите удалить этот мозг? Это действие нельзя отменить.",
"deleteConfirmYes": "Да, удалить этот мозг",
"returnButton": "Вернуться"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "Удалить мозг",
"deleteConfirmQuestion": "Вы уверены, что хотите удалить этот мозг? Это действие нельзя отменить.",
"deleteConfirmYes": "Да, удалить этот мозг",
"returnButton": "Вернуться",
"unsubscribeButton": "Отписаться от мозга",
"unsubscribeConfirmQuestion": "Вы уверены, что хотите отписаться от этого мозга?",
"unsubscribeConfirmYes": "Да, отписаться от этого сознания",
"successfully_deleted": "Мозг успешно удален.",
"successfully_unsubscribed": "Вы успешно отписались от этого мозга."
}

View File

@ -1,6 +0,0 @@
{
"deleteButton": "删除大脑",
"deleteConfirmQuestion": "您确定要删除这个大脑吗?此操作无法撤销!",
"deleteConfirmYes": "是的,我要删除",
"returnButton": "返回"
}

View File

@ -0,0 +1,11 @@
{
"deleteButton": "删除大脑",
"deleteConfirmQuestion": "您确定要删除这个大脑吗?此操作无法撤销!",
"deleteConfirmYes": "是的,我要删除",
"returnButton": "返回",
"unsubscribeButton": "取消关注大脑",
"unsubscribeConfirmQuestion": "您确定要取消订阅此大脑吗?",
"unsubscribeConfirmYes": "是的,取消订阅此大脑",
"successfully_deleted": "成功删除大脑。",
"successfully_unsubscribed": "您已成功取消订阅此大脑。"
}